.NET / SQL / Enterprise Engineering

.NET Plugin Host Architecture for a Windows Desktop Application

Report summary

Architecture synthesis. A production Windows desktop host should use a tiered plugin model rather than one universal plugin boundary. The default should be out-of-process plugins over Windows named pipes , supervised by the host, with a small stable contract assembly that defines manifest semantics,

Status
Research archive item
Category
.NET / SQL / Enterprise Engineering
Length
4,664 words
Reading time
22 minutes
Report type
architecture

Key topics

  • .NET / SQL / Enterprise Engineering
  • .NET
  • SQL
  • Enterprise Engineering
  • C#
  • Python
  • Runtime
  • NuGet
  • Semantic Systems

Research provenance

Archive status
Research archive item
Content identity
sha256:98c12354c9292fd6690a1bc2b76c5961ae595afb0d279562e0c34896bd3904ae

For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.

Source availability: 69 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.

This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.

Full report

On this page

Executive recommendation

Architecture synthesis. A production Windows desktop host should use a tiered plugin model rather than one universal plugin boundary. The default should be out-of-process plugins over Windows named pipes, supervised by the host, with a small stable contract assembly that defines manifest semantics, lifecycle, capabilities, operation envelopes, and diagnostics. A narrower exception should permit trusted, managed-only, low-risk plugins to run in-process inside a collectible AssemblyLoadContext when they need very low-latency calls or direct access to host-managed object graphs. A separately installed Windows service or broker should be reserved for capabilities that must outlive the UI process, run with a different identity, perform privileged operations, or serve multiple client processes.

Platform fact. In current .NET, AssemblyLoadContext is the supported mechanism for managed dependency isolation and potential unloadability; AssemblyDependencyResolver is the standard resolver for plugin dependencies and native libraries; Microsoft.Extensions.DependencyInjection is the current built-in DI container model; System.Composition remains available as a composition library; named pipes are a first-class Windows IPC primitive; and Windows App SDK deployment guidance distinguishes packaged, packaged-with-external-location, and unpackaged desktop apps, with special constraints around single-file publishing and writable locations.

Platform fact. An AssemblyLoadContext is not a security sandbox. Microsoft’s secure-coding guidance states that Code Access Security and security transparency are not supported as a security boundary for partially trusted code, and explicitly advises against loading code of unknown origin without alternative measures such as virtualization, AppContainers, or OS users and permissions. Microsoft’s AssemblyLoadContext guidance also states that dynamic dependencies are not “binary isolation”; they are isolated only by name resolution.

Architecture synthesis. The decision-ready default is therefore:

Trust tier A
Built-in features and first-party static modules
-> direct references + host DI
-> no plugin packaging needed

Trust tier B
Trusted managed plugin, no native payload, low crash risk, must be low-latency
-> in-process
-> shared contract assembly in Default ALC
-> implementation and private deps in collectible ALC
-> host-mediated least-authority services only

Trust tier C
Third-party plugin, Python sidecar, native/GPU libs, heavyweight model/tooling, crash-prone or dependency-heavy
-> out-of-process worker
-> Windows named pipes as default local transport
-> host supervisor, restart policy, quotas, quarantine, signed receipts

Trust tier D
Needs separate identity, privileged operations, background longevity, shared machine-wide broker responsibilities
-> separately installed Windows service or broker
-> explicit admin/service lifecycle, separate deployment and observability

Proposal. Treat the process boundary as the default reliability boundary and a different Windows principal/AppContainer/VM as the default security boundary. That distinction should remain explicit in architecture and documentation.

Scope, assumptions, zero-access declaration, and source method

Zero-access declaration. This report does not access, inspect, run, or infer any private product code, binaries, manifests, APIs, logs, screenshots, or internal documents. All examples use neutral names such as Example.Plugin and are proposals only.

Assumption. The host is a modern Windows desktop application, likely C#/.NET and potentially WinUI 3, but the private implementation is unknown. The owner-supplied context is treated only as requirements context, not evidence.

Source method. This report prioritizes Microsoft Learn and related Microsoft documentation for .NET loading, dependency resolution, unloadability, diagnostics, hosting, Windows services, Windows App SDK deployment, MSIX behavior, IPC, security guidance, testing, and publishing. NuGet package pages are used only to confirm the current availability of System.Composition and DI packages. The Semantic Versioning specification and NuGet versioning documentation are used for version-contract guidance.

Current-platform applicability. The platform guidance used here is applicable to current .NET desktop applications that use the .NET runtime’s current loading model, Windows IPC primitives, and Windows App SDK deployment models. The most material current constraints are: AssemblyLoadContext replaces the old AppDomain isolation story in modern .NET; Native AOT does not support dynamic loading; trimming and reflection need deliberate annotation; packaged WinUI 3 apps do not support PublishSingleFile; and packaged apps should not assume their install location is writable for plugin installation.

Obsolete guidance flagged. .NET Framework AppDomain unloading, legacy CAS policy, and old .NET Framework assembly-loading advice remain historically informative but are not the recommended basis for a modern .NET desktop plugin host. Microsoft documents CAS as unsupported or obsolete in modern .NET.

Architecture synthesis. The table below scores each required option on a 1–5 scale, where 5 is most favorable for production plugin hosting on that metric. For startup cost, call latency, deployment complexity, and plugin-developer burden, a higher score means lower cost/latency/complexity/burden. The scores are not product facts; they are synthesis grounded in the cited runtime, deployment, IPC, and security guidance.

OptionIsolationSecurity boundaryUnloadabilityDependency isolationNative dependency supportStartup costCall latencyStreamingCancellationCrash containmentDeployment complexityDiagnosticsTestabilityPlugin-dev burden
Direct in-process references11114554315345
Reflection + shared contract assembly11124454314444
MEF / System.Composition11124453213333
Collectible AssemblyLoadContext21343444323443
Out-of-process .NET worker over named pipes43555345553553
Out-of-process worker over local sockets / loopback43555335553553
Out-of-process worker over standard input/output32555233454344
Windows service / separately installed broker54555125551552

Architecture synthesis. Direct in-process references score highest for startup cost, latency, and developer ease because they rely on ordinary static composition and host DI. They score lowest for isolation, security, unloadability, and crash containment because everything shares one process, one address space, and one failure domain. They fit only built-in or tightly controlled first-party extensions, not an optional enterprise plugin ecosystem.

Architecture synthesis. Reflection plus a shared contract assembly improves optional discovery and keeps the host contract small, but it still shares the same process and failure domain. It gains a modest dependency-isolation score only if discovery is paired with careful loading discipline. By itself, reflection does not solve version conflicts, native collisions, or trust. It is useful as a discovery mechanism, not as the full isolation model. Trimming guidance also makes broad dynamic reflection a maintainability and deployment risk unless deliberately annotated.

Architecture synthesis. MEF / System.Composition remains available and supported as a composition library, but it is still a composition story rather than an isolation story. It does not create a security boundary, does not itself solve unloadability, and tends to impose attribute/convention coupling that becomes awkward when the plugin boundary must also cover sidecars, capability declarations, and operational receipts. It is appropriate only if the host explicitly wants attribute-based composition for trusted in-proc modules.

Architecture synthesis. Collectible AssemblyLoadContext is the best supported in-process isolation mechanism for modern .NET. It materially improves dependency isolation and can support unload—but only cooperatively. It does not create a security boundary, and unload can be blocked by references, threads, registered waits, and custom AssemblyLoadContext fields that retain plugin objects. Native support is only moderate because native dependency collisions, unmanaged handles, and plugin-specific native search behavior complicate both load and unload. Use it for trusted, managed, low-risk plugins only.

Architecture synthesis. Out-of-process workers over named pipes are the best default for a Windows desktop host. Named pipes are a Windows-native IPC primitive, support duplex communication, and integrate with Windows security. This option scores highly for isolation, unloadability, dependency isolation, native support, crash containment, diagnostics, testability, streaming, and cancellation. It loses some points on startup cost and plugin-developer burden because a child process and IPC protocol are real complexity, but that is usually the right enterprise trade.

Architecture synthesis. Local sockets or loopback are nearly as strong as named pipes for process isolation and streaming. They are often the right strategic choice only when cross-platform parity, existing socket-based stacks, or non-Windows sidecars outweigh the Windows-specific advantages of named pipes. On a Windows-only desktop application, they score slightly worse for security ergonomics and latency because named pipes align better with local IPC and ACL-based access control.

Architecture synthesis. Standard input/output works especially well for short-lived tools and some language-neutral sidecars because ProcessStartInfo makes launch and stream redirection straightforward. But it is weaker for sustained streaming, multiplexing, and diagnostics, and Microsoft explicitly documents deadlock hazards when redirected streams are read synchronously or in the wrong order. It is viable for CLI-style helpers, not the best default for a heavily supervised, long-running plugin fleet.

Architecture synthesis. A Windows service or separately installed broker is the strongest operational boundary here, but it is the most complex deployment and operational model. It is justified only when the capability must run outside the user session, under a different identity, with service recovery semantics, or as a shared machine-wide facility. It should not be the default extensibility shape for ordinary optional desktop plugins.

Recommended host boundary. The recommended reference architecture is:

+--------------------------------------------------------------+
| Example.Host.UI process                                      |
|  - Core local features                                       |
|  - Plugin catalog + manifest validator                       |
|  - Receipt store + policy engine + quarantine list           |
|  - Contract assembly loaded in Default ALC                   |
|  - Optional in-proc trusted plugin zone via collectible ALC  |
|  - IPC supervisor + health monitor + resource attribution    |
+----------------------+----------------------+-----------------+
                       |                      |
                 named pipes             named pipes
                       |                      |
        +--------------+----+       +---------+----------------+
        | Example.Plugin.Worker |     | Example.Plugin.Python   |
        | (.NET sidecar)        |     | (Python or other stack) |
        | plugin-specific deps  |     | language-neutral IPC    |
        | native libs allowed   |     | bounded contract only   |
        +-----------------------+     +-------------------------+
                       |
                optional broker RPC
                       |
        +--------------+----------------------------------------+
        | Example.Plugin.Broker.Service                         |
        | Windows service / separate principal / privileged ops |
        +------------------------------------------------------+

Reliability boundary versus security boundary. A process boundary materially improves reliability because it contains crashes, allows hard kill and restart, isolates dependency graphs, and makes resource attribution tractable. A process boundary becomes a meaningful security boundary only when combined with OS-enforced separation such as separate users, AppContainer, virtualization, restricted ACLs, and narrow IPC permissions. Modern .NET does not provide an in-process partial-trust sandbox for plugin code.

Explicit exception rules. Force a plugin out of process if any of the following apply: it is third-party or untrusted; it includes Python or other non-.NET runtime payloads; it brings native DLLs, GPU runtimes, or heavy inference/tool dependencies; it performs network or remote-connectivity work that may hang; it has a history of crashes or long startup; or it must be resource-limited and restartable without jeopardizing the host. Reserve in-process collectible ALC for trusted managed-only plugins whose value depends on low-latency calls and whose unload path passes automated leak and unload tests. Reserve separate services for elevated, machine-wide, or always-on capabilities.

C# contract proposal, lifecycle state machine, and dependency isolation

Proposal. The host should expose one small, stable contract assembly loaded in the default AssemblyLoadContext, and every in-process plugin should reference only that contract assembly plus its own private dependencies. Shared abstractions must stay in the shared contract assembly so that type identity is stable across load contexts. Microsoft’s AssemblyLoadContext guidance explicitly recommends the shared-dependency pattern and warns that types with the same name in different contexts are different types.

Proposal. The base contract should prefer interfaces for lifecycle, immutable records for manifest and result data, cooperative cancellation via CancellationToken, async disposal via IAsyncDisposable, and message envelopes for out-of-process interoperability. For streaming, use IAsyncEnumerable<T> or a bounded channel abstraction only where streaming is truly needed; for cross-process interoperability, keep the wire contract to stable message envelopes rather than .NET events or arbitrary callbacks. Microsoft documents CancellationToken, IAsyncDisposable, and System.Threading.Channels as the standard primitives for these concerns.

Proposal and sample. The following code is illustrative, not production-ready, but is intended to be compilable in principle and sufficiently concrete for contract drafting.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;

namespace Example.Plugin.Abstractions;

/// <summary>
/// Immutable plugin identity and semantic contract version.
/// </summary>
public sealed record PluginIdentity
{
    [Display(Name = "plugin")]
    public required string PluginId { get; init; }

    [Display(Name = "display name")]
    public required string DisplayName { get; init; }

    [Display(Name = "publisher")]
    public required string Publisher { get; init; }

    [Display(Name = "plugin version")]
    public required string PluginVersion { get; init; }

    [Display(Name = "contract version")]
    public required string ContractVersion { get; init; }
}

/// <summary>
/// Stable manifest projection consumed by the host after validation.
/// </summary>
public sealed record PluginManifest
{
    [Display(Name = "identity")]
    public required PluginIdentity Identity { get; init; }

    [Display(Name = "entry point")]
    public required string EntryPoint { get; init; }

    [Display(Name = "runtime")]
    public required PluginRuntimeKind Runtime { get; init; }

    [Display(Name = "capabilities")]
    public required IReadOnlyList<CapabilityDeclaration> Capabilities { get; init; }

    [Display(Name = "permissions")]
    public required IReadOnlyList<PermissionDeclaration> Permissions { get; init; }

    [Display(Name = "settings schema")]
    public string? SettingsSchemaJson { get; init; }

    [Display(Name = "minimum host version")]
    public string? MinimumHostVersion { get; init; }

    [Display(Name = "maximum host version")]
    public string? MaximumHostVersion { get; init; }

    [Display(Name = "supported architectures")]
    public required IReadOnlyList<string> SupportedArchitectures { get; init; }

    [Display(Name = "supported runtimes")]
    public required IReadOnlyList<string> SupportedRuntimeIdentifiers { get; init; }
}

public enum PluginRuntimeKind
{
    InProcessManaged = 0,
    OutOfProcessDotNet = 1,
    OutOfProcessPython = 2,
    ExternalService = 3
}

public sealed record CapabilityDeclaration
{
    [Display(Name = "name")]
    public required string Name { get; init; }

    [Display(Name = "version")]
    public required string Version { get; init; }

    [Display(Name = "description")]
    public string? Description { get; init; }
}

public sealed record PermissionDeclaration
{
    [Display(Name = "name")]
    public required string Name { get; init; }

    [Display(Name = "required")]
    public required bool Required { get; init; }

    [Display(Name = "reason")]
    public string? Reason { get; init; }
}

/// <summary>
/// Least-authority services exposed by the host.
/// The plugin must not receive IServiceProvider or internal host objects.
/// </summary>
public interface IPluginHostContext
{
    /// <summary>
    /// Gets the plugin identity approved by the host.
    /// </summary>
    PluginIdentity Identity { get; }

    /// <summary>
    /// Gets the operation deadline clock for deterministic timeout calculation.
    /// </summary>
    TimeProvider TimeProvider { get; }

    /// <summary>
    /// Gets a read-only host policy snapshot relevant to the plugin.
    /// </summary>
    HostPolicySnapshot Policy { get; }

    /// <summary>
    /// Writes a structured diagnostic record through the host pipeline.
    /// </summary>
    /// <param name="record">The diagnostic record to emit.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask WriteDiagnosticAsync(
        PluginDiagnosticRecord record,
        CancellationToken cancellationToken);

    /// <summary>
    /// Gets a scoped settings store owned by the plugin.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<IPluginSettingsStore> GetSettingsStoreAsync(
        CancellationToken cancellationToken);

    /// <summary>
    /// Reports progress for the current lifecycle or operation activity.
    /// </summary>
    /// <param name="update">The progress update.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask ReportProgressAsync(
        PluginProgressUpdate update,
        CancellationToken cancellationToken);
}

public sealed record HostPolicySnapshot
{
    [Display(Name = "network egress allowed")]
    public required bool NetworkEgressAllowed { get; init; }

    [Display(Name = "max concurrency")]
    public required int MaxConcurrency { get; init; }

    [Display(Name = "startup timeout")]
    public required TimeSpan StartupTimeout { get; init; }

    [Display(Name = "stop timeout")]
    public required TimeSpan StopTimeout { get; init; }

    [Display(Name = "operation timeout")]
    public required TimeSpan DefaultOperationTimeout { get; init; }
}

public interface IPluginSettingsStore
{
    /// <summary>
    /// Reads the current plugin settings blob.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<string?> ReadJsonAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Replaces the current plugin settings blob atomically.
    /// </summary>
    /// <param name="json">The new JSON settings payload.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask WriteJsonAsync(string json, CancellationToken cancellationToken);
}

/// <summary>
/// Lifecycle contract for an in-process plugin.
/// </summary>
public interface IPlugin : IAsyncDisposable
{
    /// <summary>
    /// Performs fast static validation before configuration or startup.
    /// No background work should begin here.
    /// </summary>
    /// <param name="context">The least-authority host context.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginValidationResult> ValidateAsync(
        IPluginHostContext context,
        CancellationToken cancellationToken);

    /// <summary>
    /// Applies configuration and validates plugin-owned settings.
    /// </summary>
    /// <param name="context">The least-authority host context.</param>
    /// <param name="settingsJson">The JSON settings payload owned by the plugin.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginConfigureResult> ConfigureAsync(
        IPluginHostContext context,
        string? settingsJson,
        CancellationToken cancellationToken);

    /// <summary>
    /// Allocates resources needed before the plugin is ready.
    /// No externally visible work should begin until StartAsync succeeds.
    /// </summary>
    /// <param name="context">The least-authority host context.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginInitializeResult> InitializeAsync(
        IPluginHostContext context,
        CancellationToken cancellationToken);

    /// <summary>
    /// Starts externally visible work.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginStartResult> StartAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Returns the current health state and recent diagnostics summary.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginHealthReport> GetHealthAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Temporarily suspends new work if supported.
    /// </summary>
    /// <param name="reason">The suspend reason.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginSuspendResult> SuspendAsync(
        string reason,
        CancellationToken cancellationToken);

    /// <summary>
    /// Resumes work after a successful suspend.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginResumeResult> ResumeAsync(CancellationToken cancellationToken);

    /// <summary>
    /// Stops externally visible work and releases active operations cooperatively.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    ValueTask<PluginStopResult> StopAsync(CancellationToken cancellationToken);
}

public enum PluginHealthStatus
{
    Unknown = 0,
    Ready = 1,
    Degraded = 2,
    Failed = 3
}

public sealed record PluginDiagnosticRecord
{
    [Display(Name = "timestamp")]
    public required DateTimeOffset TimestampUtc { get; init; }

    [Display(Name = "severity")]
    public required string Severity { get; init; }

    [Display(Name = "code")]
    public required string Code { get; init; }

    [Display(Name = "message")]
    public required string Message { get; init; }

    [Display(Name = "data")]
    public IReadOnlyDictionary<string, string>? Data { get; init; }
}

public sealed record PluginProgressUpdate
{
    [Display(Name = "activity")]
    public required string Activity { get; init; }

    [Display(Name = "percent")]
    public int? Percent { get; init; }

    [Display(Name = "message")]
    public string? Message { get; init; }
}

public sealed record PluginHealthReport
{
    [Display(Name = "status")]
    public required PluginHealthStatus Status { get; init; }

    [Display(Name = "summary")]
    public required string Summary { get; init; }

    [Display(Name = "diagnostics")]
    public required IReadOnlyList<PluginDiagnosticRecord> Diagnostics { get; init; }
}

public abstract record ContractResult
{
    [Display(Name = "success")]
    public required bool Success { get; init; }

    [Display(Name = "code")]
    public string? Code { get; init; }

    [Display(Name = "message")]
    public string? Message { get; init; }
}

public sealed record PluginValidationResult : ContractResult;
public sealed record PluginConfigureResult : ContractResult;
public sealed record PluginInitializeResult : ContractResult;
public sealed record PluginStartResult : ContractResult;
public sealed record PluginSuspendResult : ContractResult;
public sealed record PluginResumeResult : ContractResult;
public sealed record PluginStopResult : ContractResult;

/// <summary>
/// Typed operation envelope suitable for in-proc and out-of-proc projection.
/// </summary>
/// <typeparam name="TPayload">The payload type.</typeparam>
public sealed record OperationRequest<TPayload>
{
    [Display(Name = "operation")]
    public required string Operation { get; init; }

    [Display(Name = "request")]
    public required string RequestId { get; init; }

    [Display(Name = "payload")]
    public required TPayload Payload { get; init; }

    [Display(Name = "deadline")]
    public DateTimeOffset? DeadlineUtc { get; init; }
}

/// <summary>
/// Typed operation result envelope.
/// </summary>
/// <typeparam name="TPayload">The payload type.</typeparam>
public sealed record OperationResult<TPayload> : ContractResult
{
    [Display(Name = "request")]
    public required string RequestId { get; init; }

    [Display(Name = "payload")]
    public TPayload? Payload { get; init; }
}

/// <summary>
/// Optional unload tracking record held by the host, not the plugin.
/// </summary>
public sealed record PluginUnloadTracking
{
    [Display(Name = "plugin")]
    public required string PluginId { get; init; }

    [Display(Name = "unload requested")]
    public required DateTimeOffset UnloadRequestedUtc { get; init; }

    [Display(Name = "unloaded")]
    public DateTimeOffset? UnloadedUtc { get; init; }

    [Display(Name = "completed")]
    public required bool Completed { get; init; }
}

Proposal. For out-of-process plugins, project the same contract into a neutral wire envelope such as JSON messages with operation names, request IDs, deadline UTC, structured errors, capability claims, and diagnostic events. Avoid exposing arbitrary host callbacks, raw host service providers, or broad object references across the boundary. Use a capability-based host context where each service is explicit and revocable. This minimizes authority leakage and avoids type identity and object-lifetime coupling across boundaries.

Lifecycle state machine. The legal states and transitions should be:

StateAllowed transitions
DiscoveredRejected, Compatible, Disabled
RejectedRemoved
CompatibleInstalled, Disabled
InstalledConfigured, Disabled, Updating, Removed
ConfiguredInitializing, Disabled, Updating, Removed
InitializingReady, Degraded, Failed, Disabled, Stopping
ReadyDegraded, Stopping, Failed, Updating, Disabled
DegradedReady, Stopping, Failed, Quarantined, Disabled
DisabledInstalled, Removed
StoppingInstalled, Failed, Quarantined, Removed
FailedInstalled, Quarantined, Disabled, Removed
QuarantinedDisabled, Updating, Removed
UpdatingRollback, Installed, Removed
RollbackInstalled, Disabled, Removed
Removednone

Proposal. Timeout and crash transitions are mandatory: initialization timeout forces Initializing -> Failed and may escalate to Quarantined; shutdown timeout forces Stopping -> Failed for in-proc and Stopping -> Installed only after verified stop for out-of-proc; worker crash transitions Ready/Degraded/Initializing -> Failed, increments failure counters, and may escalate to Quarantined; bad update transitions Updating -> Rollback -> Installed with the last-known-good receipt retained.

Loading and dependency algorithm. The host-side algorithm should be:

function InstallPlugin(packagePath):
    manifest = parseManifest(packagePath)
    validateSchema(manifest)
    canonicalRoot = canonicalizeAndValidateInstallRoot(manifest)
    verifyPublisherIdentityAndReceipt(manifest)
    verifyHashes(packagePath, manifest)
    verifyArchitecture(manifest, hostArchitecture)
    verifyRuntimeCompatibility(manifest, hostRuntime)
    verifyContractVersionRange(manifest, hostContractVersion)
    stagePath = createContentAddressedStagingDirectory()
    extractPackageAtomically(packagePath, stagePath)
    fsyncAndSeal(stagePath)
    commitInstallReceipt(stagePath, manifest)
    moveStageIntoCatalog(stagePath)
    return Installed

function ActivatePlugin(installPath):
    manifest = loadCommittedManifest(installPath)
    if manifest.Runtime == InProcessManaged:
        alc = new CollectibleAlc(mainAssemblyPath)
        resolver = new AssemblyDependencyResolver(mainAssemblyPath)
        configure alc.Load(name):
            if name == SharedContractAssemblyName:
                return SharedContractAssemblyFromDefaultContext
            path = resolver.ResolveAssemblyToPath(name)
            if path != null: return alc.LoadFromAssemblyPath(path)
            return null
        configure alc.LoadUnmanagedDll(name):
            path = resolver.ResolveUnmanagedDllToPath(name)
            if path != null: return loadNative(path)
            return IntPtr.Zero
        create instance via reflected entry point
        run ValidateAsync within timeout
        run ConfigureAsync within timeout
        run InitializeAsync within timeout
        if health gate passes:
            run StartAsync within timeout
            mark Ready or Degraded
        else:
            StopAsync if needed
            mark Failed
    else if manifest.Runtime in {OutOfProcessDotNet, OutOfProcessPython}:
        launch child process from committed path
        establish named pipe channel with constrained ACL
        perform handshake:
            verify plugin identity
            verify contract version
            verify capability declaration echo
            verify startup receipt hash
        send Validate/Configure/Initialize/Start sequence with deadlines
        if health gate passes:
            mark Ready or Degraded
        else:
            terminate child if cooperative stop fails
            mark Failed
    else if manifest.Runtime == ExternalService:
        verify service registration + version + identity + ACL
        mark Installed/Ready only after handshake

function DeactivatePlugin(handle):
    request cooperative stop with deadline
    wait for inflight operations to drain
    dispose transport/resources
    if in-proc:
        drop strong references
        call alc.Unload()
        poll weak reference + GC until completion or timeout
        record leaks if unload incomplete
    if out-of-proc and process alive after deadline:
        kill process
    archive stop diagnostics
    return final state

function RemovePlugin(installPath):
    ensure plugin disabled and not loaded
    tombstone catalog entry
    delete owned data according to policy
    delete binaries from committed path
    delete receipts only after successful removal

Platform fact. This algorithm matches the documented model: AssemblyDependencyResolver resolves both managed and unmanaged dependencies from the component’s dependency graph; AssemblyLoadContext can override managed and unmanaged load behavior; a collectible AssemblyLoadContext unloads only after references, threads, and handles no longer pin it; and Native AOT is incompatible with the host-side dynamic loading model.

Dependency and native-library isolation guidance. A host should never rely on default-context probing for private plugin dependencies. Each plugin’s main assembly path should seed its own AssemblyDependencyResolver, and the host should deliberately share only the stable contract assembly and any intentionally shared framework dependencies from the default context. This minimizes type identity mismatches and version cross-talk. Microsoft’s guidance explicitly notes that each AssemblyLoadContext can hold only one version per simple name, supports sharing via returned Assembly references, and warns that same-name types across contexts are not the same type.

Failure modes to design around. The most important dependency/isolation hazards are: default load-context leakage; LoadFrom-style path ambiguity; type identity mismatches; unmanaged library search surprises; and unload blocked by references, background threads, registered waits, or custom AssemblyLoadContext fields that hold plugin objects. Microsoft documents all of these patterns.

Reliability, observability, recovery, and failure catalog

Architecture synthesis. The host must assume that some plugins will hang, misbehave, leak resources, produce malformed outputs, or enter restart loops. The containment model should therefore include timeouts, circuit breakers, quarantine, safe mode, last-known-good rollback, and deterministic receipts for install/update/remove. In-proc plugins can only be stopped cooperatively and then disabled; out-of-proc workers can additionally be killed and restarted under policy.

Proposal. Recommended operational rules:

  • Startup: validate -> configure -> initialize -> health gate -> start, each with its own deadline.
  • Health policy: repeated degraded or failed health within a rolling window triggers disable or quarantine.
  • Restart policy: exponential backoff with a maximum attempts budget; after that, quarantine.
  • Safe mode: launch the host with all optional plugins disabled after repeated global startup failures.
  • Rollback: updates are staged side-by-side; activation switches only after successful validation and health gate.
  • Data ownership: plugin-owned settings and state live in a plugin-specific store keyed by plugin identity and versioned by plugin-defined migration steps.
  • Minimum host guarantee: every core host feature that does not require a plugin remains operational when all plugins are disabled or broken.

Platform fact. Structured logging through ILogger is the standard performant logging abstraction in .NET, and .NET diagnostic tools and counters support health and performance investigation. The Process API can attribute child-process CPU time, memory, threads, handles, and exit information to out-of-proc plugins.

Proposal. Logging must be plugin-scoped and data-minimized. Every log event should carry at least: plugin ID, plugin version, publisher, installation receipt ID, lifecycle state, process ID if any, request ID, operation name, severity, and coarse resource counters. Do not log raw prompts, credentials, private files, or unrelated host logs by default. The plugin host should support redaction hooks, field allowlists, and per-capability sensitivity labels before records enter central telemetry. This follows Microsoft’s structured logging guidance while narrowing exposure to only plugin-relevant records.

Failure and containment catalog.

Failure modeDetectionContainmentRecovery
Constructor failure / activation exceptionActivation exception surfaced during instance creationMark Failed; do not retry immediatelyRequire operator action or updated package
Missing managed dependencyResolver returns null or load exceptionReject load before lifecycle startsFix package or compatibility metadata
Conflicting managed versionType mismatch or wrong shared/default-context bindingDisable package; keep host aliveAdjust shared contract policy or repackage
Native-load failureResolveUnmanagedDllToPath fails, P/Invoke load failsPrefer out-of-proc for native-heavy plugin; in-proc fail fastRepackage with correct RID/arch assets
Hung initializationDeadline exceeded in initialize/startFor out-of-proc: kill worker; for in-proc: disable and require restart if neededBackoff + quarantine after repeats
Unobserved/background exceptionProcess exit, health failure, logged unhandled exceptionQuarantine after threshold; isolate plugin scopeRestart if out-of-proc and policy allows
Background thread leakALC weak reference never dies after unloadDisable plugin version; block update/remove until host restart if necessaryFix plugin; add unload regression test
Callback after stopHost receives message after stop token / session closedDrop late messages; count as protocol violationQuarantine after repeated violations
Process crashChild exit code / broken pipeKeep host alive, mark plugin failedRestart with backoff or quarantine
Malformed responseProtocol deserialization or schema validation errorKill/quarantine offending sidecarRequire fixed plugin build
Resource exhaustionChild CPU/memory/handles exceed policySuspend, stop, or kill plugin onlyBackoff + operator diagnostics
Update failureHash/signature mismatch or health-gate failure after switch attemptRoll back to last-known-goodKeep old version active; preserve receipts

Platform fact. These controls are directly supported by the documented primitives: timeouts and cancellation through .NET cancellation tokens; child-process start/stop/exit monitoring through Process; asynchronous stream handling for stdio-based workers; and diagnostic counters/ports for deeper investigation.

Packaging, Windows deployment constraints, developer SDK, automated tests, and phased adoption

Platform fact. MSIX gives packaged desktop apps a clean install/uninstall model, package identity, and automatic updates, but packaged apps do not have a normal “write into my install directory” story for plugin binaries. Windows documents that packaged apps redirect writes in AppData to private per-user app locations and that writes to the MSIX package/container itself fail. Windows App SDK deployment guidance also distinguishes packaged apps, packaged apps with external location, and unpackaged apps, and notes that PublishSingleFile support for WinUI 3 requires unpackaged + self-contained; packaged apps do not support that model.

Architecture synthesis. Therefore, a packaged desktop host should install optional plugins into a host-managed writable location outside the immutable package payload, typically a per-user plugin directory under AppData or another explicit external location under host control. The host itself may be MSIX-packaged, but optional plugin payloads should not be modeled as in-place mutations of the package. In enterprise scenarios, MSIX modification packages are a valid distribution shape for organization-managed add-ons, but they are not the best general-purpose lifecycle for fast-moving optional plugins that need independent quarantine, receipts, and side-by-side rollback.

Platform fact. Single-file publishing can explicitly exclude files from embedding, trimming is incompatible with some reflection-heavy patterns, and Native AOT does not support dynamic loading. A plugin host should therefore avoid Native AOT and should treat trimming very carefully. A plugin worker may use self-contained or even Native AOT deployment only if its own runtime model is static and AOT-compatible.

Platform fact. Runtime identifiers determine native and architecture-specific asset selection, and publishing with a RID includes native dependencies as needed. This makes RID and architecture declarations mandatory manifest fields for plugin validation, especially when plugins carry native payloads.

Proposal. The plugin developer SDK should ship as a neutral package set:

  • Example.Plugin.Abstractions for the stable contract.
  • Example.Plugin.TestHost for local harness execution.
  • Example.Plugin.Analyzers to forbid host-internals references, reflection over forbidden namespaces, thread leaks after stop, broad P/Invoke without declarations, and direct service-provider access.
  • Example.Plugin.Templates for a managed in-proc plugin, a .NET sidecar worker, and a Python sidecar skeleton.
  • Example.Plugin.Manifest.Schema for manifest validation and example receipts.

Proposal. Submission or admission tests should include, at minimum: manifest schema validation; semantic contract version compatibility; architecture/RID validation; lifecycle timeout handling; cancellation compliance; structured log conformance; malformed settings rejection; repeated start/stop idempotency; in-proc unload success where applicable; protocol fuzzing for out-of-proc workers; and policy enforcement for declared capabilities and permissions. Use SemVer for the public contract versioning story, but remember that NuGet package versions and assembly versions have different roles in .NET.

Proposal. The automated-test architecture should have these layers:

  • Unit tests for manifest parser, policy engine, version-range matcher, path canonicalization, and lifecycle state machine.
  • Contract tests run against every plugin package using the same abstract lifecycle suite.
  • Integration tests for install, start, health, stop, update, rollback, disable, uninstall.
  • Process-isolation tests for child crash, broken pipe, malformed protocol, resource ceilings, restart backoff.
  • Unload tests for collectible ALC plugins using weak-reference verification and forced-GC loops.
  • Compatibility tests for contract version N/N+1, host downgrade semantics, plugin downgrade, publisher mismatch.
  • Chaos tests for delayed responses, partial writes, corrupt manifests, dependency absence, and stop-time races.
  • Package tests for MSIX-packaged host scenarios, unpackaged scenarios, writable-location behavior, and side-by-side update safety.

Proposal. Adversarial fixture plugins should include:

  • Example.Plugin.Good.Minimal
  • Example.Plugin.Bad.MissingDependency
  • Example.Plugin.Bad.NativeLoadFailure
  • Example.Plugin.Bad.HungInitialize
  • Example.Plugin.Bad.BackgroundThreadLeak
  • Example.Plugin.Bad.CallbackAfterStop
  • Example.Plugin.Bad.VersionMismatch
  • Example.Plugin.Bad.MalformedOutput
  • Example.Plugin.Bad.CrashLoop
  • Example.Plugin.Bad.ResourceHog
  • Example.Plugin.Bad.PermissionMismatch

Platform fact. Current .NET testing guidance supports automation through dotnet test, and MSTest’s current analyzers explicitly encourage cooperative cancellation and flowing the test cancellation token to async operations. FakeTimeProvider is the supported deterministic-time primitive for testing time-sensitive behavior.

Proposal. Deterministic tests should cover version matching, state transitions, cancellation propagation, timeout calculations, retry policies, quarantine thresholds, and receipt hashing. Windows-specific integration infrastructure is still required for named-pipe ACLs, Process resource attribution, Windows service behavior, packaged-app writable-path behavior, and MSIX/update interactions. AssemblyLoadContext unload tests are conceptually cross-platform but should be part of the Windows CI lane as well because the host target is Windows desktop.

Phased adoption plan. A safe staged rollout is:

Phase one. First-party static modules only. Use direct references and host DI. Establish manifest schema, receipts, and lifecycle state machine before opening extensibility.

Phase two. Trusted first-party managed plugins in collectible ALC. Add unload tests, strict shared-contract discipline, and disable/quarantine controls. Do not onboard third parties yet.

Phase three. First-party and closely held partner sidecars out of process over named pipes. Add supervisor, health gates, restart policy, and per-plugin diagnostics/resource attribution. This is the point where Python sidecars become acceptable.

Phase four. Signed partner plugins with curated admission. Default all third-party plugins to out-of-proc. Require capability declarations, publisher identity, receipts, revocation/quarantine, and package compatibility tests. Strong names alone are insufficient for security or trust.

Phase five. Optional service brokers for privileged or always-on capabilities only after revocation, operator controls, and observability are mature. Broad third-party in-proc extensibility should remain either disallowed or highly exceptional.

Open implementation questions, limitations, and sources

Unknown. The private product’s actual deployment mode—packaged, unpackaged, or packaged with external location—is not known, and that materially affects plugin install paths, update UX, and single-file options.

Unknown. The actual trust model for plugin publishers is not known. Whether enterprise policy requires Authenticode/MSIX signing, certificate pinning, allowlists, revocation feeds, or an internal package repository remains an implementation decision. Microsoft guidance supports package signing and publisher identity, but the concrete trust chain is product-specific.

Unknown. The required degree of cross-language portability is not known. A Windows-only product should favor named pipes for local sidecars, but an explicit cross-platform future might justify sockets as the strategic IPC abstraction despite weaker Windows-local ergonomics.

Limitation. Some scoring in the comparison matrix is necessarily architectural synthesis rather than explicit vendor scoring. The cited sources establish the underlying runtime, IPC, deployment, unloadability, and security facts; the numeric prioritization is a design judgment derived from them.

Limitation. This report intentionally avoids product-specific namespaces, package formats, version numbers, and implementation claims. All class names, package names, and process diagrams are proposals.

Sources ledger. The table below uses citation links in place of raw URLs so each source remains directly navigable while complying with response-format constraints.

Source linkTitlePublisherVersion or dateAccess dateClaim supportedPlatform applicabilityLimitations
Create a .NET Core application with pluginsCreate a .NET Core application with pluginsMicrosoft Learn2026-02-042026-07-12Custom AssemblyLoadContext + AssemblyDependencyResolver is the supported plugin-loading patternModern .NET desktop hostTutorial-level, not a production blueprint
About AssemblyLoadContextAbout System.Runtime.Loader.AssemblyLoadContextMicrosoft Learn2026-03-112026-07-12ALC versioning rules, shared dependencies, isolation limits, type identity caveatsModern .NET desktop hostConceptual guidance, no full operational lifecycle
How to use and debug assembly unloadability in .NETHow to use and debug assembly unloadability in .NETMicrosoft Learn2026-03-192026-07-12Collectible ALC unload is cooperative; threads, references, handles, waits can block unloadIn-proc collectible plugin zoneFocused on unload, not on plugin UX or packaging
Secure coding guidelines for .NETSecure coding guidelines for .NETMicrosoft Learn2021-09-152026-07-12No in-proc partial-trust security boundary; use OS users/AppContainers/virtualization insteadAll modern .NET deploymentsHigh-level security guidance
CAS APIs obsoleteBreaking change: Most code access security APIs are obsoleteMicrosoft Learn.NET 5 compatibility note2026-07-12CAS is unsupported legacy technology in modern .NETAll modern .NET deploymentsCompatibility guidance, not plugin-specific
Dependency injection overviewDependency injection in .NETMicrosoft Learn2026-01-282026-07-12Built-in DI container is current supported composition modelHost internals and built-insNot an isolation mechanism by itself
MEF docsManaged Extensibility Framework / System.CompositionMicrosoft Learn / NuGet2026-03-04 / current package page2026-07-12MEF remains available as composition technologyTrusted in-proc compositionNot a security or process-isolation story
Named pipes APINamedPipeServerStream / Named pipes IPCMicrosoft Learncurrent API / 2026-05-072026-07-12Named pipes are duplex IPC and align with Windows securityWindows local IPCNot a complete RPC protocol design
gRPC IPC over named pipesInter-process communication with gRPC and Named pipesMicrosoft Learn2025-08-08 / current2026-07-12.NET supports local IPC over named pipes; ACL-based security is availableOut-of-proc .NET worker on WindowsRequires .NET-supported transport stack
Process I/O docsRedirectStandardOutput / BeginOutputReadLineMicrosoft Learncurrent API2026-07-12Stdio redirection can deadlock if handled incorrectly; async reads are required for safetyCLI-style or sidecar stdio workersFocused on stream behavior, not higher-level protocol semantics
Windows service docsCreate Windows Service using BackgroundServiceMicrosoft Learn2025-10-22 / 2026-04-272026-07-12Separate Windows services are the right model for long-running service-style capabilitiesSeparate broker/service tierDoes not address desktop plugin UX directly
Native library loadingNative library loading / unmanaged loading algorithmMicrosoft Learn2023-09-09 / 2021-10-272026-07-12Native probing and unmanaged load behavior must be deliberateNative-heavy pluginsLower-level runtime behavior, not packaging policy
Single-file deploymentCreate a single file for application deploymentMicrosoft Learn2026-03-252026-07-12Files can be excluded from single-file bundling; RID-specific deployment appliesUnpackaged or generic .NET deploymentNot all Windows desktop packaging modes support it
Trimming guidanceKnown trimming incompatibilities / Fixing trim warningsMicrosoft Learn2023-11-10 / 2025-11-19 / 2026-01-082026-07-12Dynamic reflection-heavy plugin hosts require trimming cautionPlugin discovery and host deploymentGuidance is broad, not plugin-host specific
Native AOT overviewNative AOT deployment overviewMicrosoft Learn2026-01-082026-07-12Native AOT prohibits dynamic loading; unsuitable for a dynamic plugin hostHost deployment decisionsAOT workers may still be possible if static
Windows App SDK deployment overviewWindows App SDK deployment overview / Packaging overviewMicrosoft Learn2026-05-29 / 2026-06-202026-07-12Packaged/unpackaged choices affect single-file, servicing, and plugin install pathsWinUI 3 / Windows desktop hostWindows App SDK specific
Packaged desktop behaviorUnderstanding how packaged desktop apps run on Windows / PSF filesystem write permissionMicrosoft Learn2025-09-09 / 2023-01-182026-07-12Packaged apps should not assume mutable install locations; AppData writes are redirectedMSIX-packaged hostMore about package behavior than plugin design
MSIX overview and modification packagesWhat is MSIX? / Modification packagesMicrosoft Learn2026-04-15 / 2022-04-012026-07-12MSIX provides clean updates; modification packages can represent add-onsEnterprise packaging strategyModification packages are not the whole plugin lifecycle
MSIX signing guidanceSign your MSIX package / Create a certificate for package signingMicrosoft Learn2026-04-19 / 2025-04-22 / 2026-04-152026-07-12Publisher identity and package signing are available trust anchorsPlugin package/repository policyTrust pipeline details remain product-specific
Strong naming guidanceStrong-named assemblies / Enhanced strong namingMicrosoft Learn2025-10-20 / 2022-08-232026-07-12Strong names identify assemblies but are not a security boundary or publisher-trust substituteAll managed plugin packagesOften misunderstood as trust model
Logging and diagnosticsLogging in C# and .NET / dotnet-counters / diagnostic port / Process APIMicrosoft Learn2026-02-04 / 2025-10-02 / 2025-11-22 / current API2026-07-12Structured logs and child-process metrics are available for attribution and diagnosticsHost observability and operationsMany tools are operator-level, not embedded-product APIs
Testing guidanceFakeTimeProvider / dotnet test / MSTest cooperative cancellationMicrosoft Learn2026-03-02 / 2026-01-20 / 2026-02-04 / 2026-04-02 / current2026-07-12Deterministic clocks and cooperative cancellation should be standard in plugin test harnessesAutomated test strategyFramework-agnostic patterns still need local harness design
Versioning referencesSemantic Versioning 2.0.0 / NuGet package versioning / .NET library versioningSemVer / Microsoft Learncurrent / 2025-01-21 / 2025-01-29 / 2026-04-082026-07-12Public contract versioning should use SemVer-aware policy, with NuGet and assembly versions understood separatelyContract and package compatibilityRequires product-specific version-range policy

Final recommendation. The architecture choice is clear: use named-pipe sidecars as the default plugin boundary, permit collectible ALC only for trusted managed low-risk plugins, and reserve Windows services for privileged or always-on broker functions. That is the best current-platform balance of extensibility, unloadability, dependency isolation, diagnostics, and enterprise reliability on Windows desktop .NET.