Application domain
In the .NET Framework, an application domain (often abbreviated as AppDomain) is an isolated environment within a process where managed applications execute. It serves as a logical boundary for loading and running assemblies, providing isolation for security, reliability, and versioning without the overhead of separate operating system processes.[1] AppDomains enable multiple applications to run concurrently in the same process while preventing faults in one from affecting others, supporting features like individual assembly unloading and configurable policies. The common language runtime (CLR) creates and manages AppDomains, which are fundamental to executing managed code. Introduced in the initial .NET Framework release, AppDomains were deprecated in .NET Core and later versions (starting with .NET 5 in 2020), replaced by other isolation models such as containers or processes; as of .NET 9 in 2024, the System.AppDomain class remains available but is not recommended for new development.[1][2]Definition and Fundamentals
Definition
An application domain, often abbreviated as AppDomain, serves as a lightweight, logical boundary within a single operating system process that hosts the execution of managed code in the .NET Framework, an implementation of the Common Language Infrastructure (CLI). It acts as a unit of isolation, enabling multiple applications to run concurrently without the overhead of separate processes, while providing mechanisms for security, reliability, and resource management.[1][3] Key characteristics of an application domain include its role as a boundary for type safety, where verified, type-safe code execution prevents memory access violations and ensures that types loaded into one domain remain distinct from those in another. It facilitates fault isolation by confining exceptions and errors to the originating domain, thereby enhancing overall system reliability without requiring process termination. Additionally, application domains support the hosting of multiple such boundaries within a single process, allowing for efficient resource sharing at the process level while maintaining logical separation.[1][4] The terminology and conceptual foundation of application domains originate from the CLI specifications, particularly Partition IV on the CLI Execution Model in the ECMA-335 standard, where they are defined as a mechanism of the Virtual Execution System (VES) to isolate applications running in the same OS process and form remoting boundaries for cross-domain communication via proxies.[3][4] This isolation contributes to fault tolerance, such as by restricting static fields and data access to a single domain to prevent interference.[1]Purpose and Benefits
Application domains in the .NET Framework serve as lightweight isolation units within a single operating system process, primarily to enable code isolation that prevents one application from adversely affecting others, facilitate simplified deployment by allowing assembly unloading without restarting the entire process, and enhance security through evidence-based permission grants determined at load time.[1][5] By providing these boundaries, application domains allow the common language runtime (CLR) to manage multiple executing applications securely and efficiently, scoping versioning policies and resource access per domain without the full overhead of separate processes.[1] Key benefits include reduced resource overhead compared to traditional OS processes, as application domains share process-wide elements like JIT-compiled code and the CLR instance while maintaining isolated heaps and execution contexts, which minimizes memory duplication and improves performance for multi-application scenarios.[1] Fault isolation ensures that unhandled exceptions or failures in one domain terminate only that domain, preserving the stability of others within the same process, thereby enhancing overall reliability.[1] Additionally, this architecture supports scalability by enabling the hosting of numerous applications in a single process, dramatically increasing server capacity without proportional increases in system resources.[1] Practical use cases demonstrate these advantages effectively. In web hosting environments like Internet Information Services (IIS), application domains isolate ASP.NET applications within worker processes, allowing multiple sites to run concurrently with independent lifecycles and failure containment, which optimizes resource utilization for high-traffic servers.[6] For plugin architectures in desktop applications, such as those in WPF or custom add-in systems, application domains enable the dynamic loading and unloading of third-party components in sandboxed environments, supporting extensibility without risking the host application's integrity.[7] Similarly, server-side scripting isolation benefits from this model by executing untrusted scripts in restricted domains, leveraging evidence like assembly signatures to enforce granular permissions and prevent unauthorized resource access.[8][5]History and Evolution
Introduction in .NET Framework
Application domains debuted with the .NET Framework 1.0 as a fundamental feature of the Common Language Runtime (CLR), providing a lightweight isolation mechanism for managed code execution within a single operating system process.[1] This design allowed developers to run multiple applications or components in isolated boundaries, offering benefits in security, fault tolerance, and resource management without the overhead of separate processes.[1] In the early .NET era, application domains played a crucial role in enabling side-by-side execution of assemblies with differing versions, directly mitigating the "DLL hell" issues inherited from Win32 development, where overwriting shared dynamic-link libraries could break dependent applications.[9] By supporting version-specific loading and unloading of code, they ensured compatibility and stability in multi-version environments, a significant advancement for enterprise software deployment.[1] Key milestones included their seamless integration with ASP.NET upon the .NET Framework's 2002 release, where application domains facilitated the isolation of web applications and controls within IIS processes, enhancing scalability and security for server-side execution.[1] Concurrently, initial support for .NET Remoting leveraged application domains to enable inter-domain object communication via proxies, laying the groundwork for distributed applications in the CLR.[10]Changes in .NET Core and Later
With the release of .NET Core in 2016, application domains underwent significant changes as part of the shift to a modular, cross-platform runtime. The core Common Language Runtime (CLR) in .NET Core removed support for creating multiple application domains within a single process, primarily because they were deemed resource-expensive and unnecessary in the new process-per-app model, which relies on operating system processes for isolation.[11] Instead, .NET Core enforces a single application domain per process, limiting the AppDomain class to basic functionality without isolation, unloading, or security boundaries.[2] Legacy code targeting .NET Framework continues to receive full support in that runtime, but .NET Core applications attempting to create additional domains encounter exceptions, with compatibility achieved only through limited API surface exposure for migration purposes.[11] Beginning with .NET 5 in 2020, which unified .NET Framework and .NET Core into a single platform, the emphasis on a single application domain per process persisted, promoting alternatives for partial isolation and dynamic loading. The AssemblyLoadContext class emerged as the primary replacement for scenarios requiring assembly unloading or contextual loading, enabling collectible contexts that support unloading without the overhead of full domains.[11][2] This adaptation facilitates cross-platform deployment, as the simplified model aligns with non-Windows environments like Linux, where traditional domain features such as shadow copying were already challenging or unsupported, favoring OS-level isolation via processes or containers.[11] As of 2025, in .NET 9 and later versions, application domains remain fully deprecated for creation and advanced features, with attempts to instantiate new domains throwing PlatformNotSupportedException to enforce migration. Official guidance recommends transitioning to AssemblyLoadContext for dynamic assembly management and using separate processes or containerization for isolation needs, ensuring compatibility across platforms without the legacy runtime's complexities.[12][11]Architecture and Lifecycle
Creation and Management
Application domains in the .NET Framework are instantiated programmatically using theAppDomain.CreateDomain method, which requires specifying a unique name for the domain, optional evidence for security policy evaluation, and an AppDomainSetup object to configure aspects like the application base directory and configuration file location.[12][13] The runtime automatically creates a default application domain as the initial entry point upon process initialization, serving as the primary environment for the application's entry assembly.[1]
Child domains, in contrast to the default domain, are explicitly created within the same process to enable isolation for different components or plugins, allowing multiple isolated execution environments to coexist.[1] The default domain cannot be unloaded during the process lifetime, whereas child domains support dynamic creation and termination to manage resources efficiently.[14]
The lifecycle of an application domain commences either at process startup for the default domain or via the CreateDomain call for child domains, and concludes through unloading initiated by the AppDomain.Unload method on a specific domain instance.[15] Unloading marks the domain for shutdown, halting new thread entry, allowing existing threads to complete or exit, triggering finalization of managed objects within the domain, and reclaiming domain-specific resources such as memory allocations.[16] This process ensures graceful termination without affecting other domains in the process, though domain-neutral assemblies loaded into multiple domains remain in memory until process exit.[16]
To monitor and respond to domain lifecycle events, developers can subscribe to notifications like the DomainUnload event, which is raised on an unspecified thread just before unloading begins, enabling cleanup of domain-specific resources or logging without identifying the specific unloading domain in the handler.[17] Properties such as the domain name and evidence, set during creation, provide identifiers and security context for management and are covered in detail under Core Properties.
In terms of management, application domains operate within a shared threading model where the process's threads are not bound exclusively to a single domain; instead, threads can cross domain boundaries to execute code in different contexts, maintaining isolation through separate memory and security boundaries while querying the current domain via Thread.GetDomain.[1][18] This model supports concurrent execution across domains without dedicated per-domain threads, enhancing resource efficiency in multi-domain scenarios.[1]
Loading Assemblies and Execution
In the .NET Framework, assemblies are loaded into an application domain using methods provided by theSystem.Reflection.[Assembly](/page/Assembly) and System.AppDomain classes, ensuring that code execution occurs within the domain's isolated boundaries. The primary method for loading an assembly into the current application domain is Assembly.Load(), which accepts the assembly's display name or a byte array containing the assembly's raw data, allowing the runtime to resolve and load the assembly from the global assembly cache (GAC), probing paths, or specified locations. For loading from a specific file path outside standard probing directories, Assembly.LoadFrom() is used, which places the assembly in the load-from context to maintain path information for dependency resolution while preserving domain isolation by preventing cross-context type identity conflicts. These loading operations are domain-specific, meaning assemblies loaded in one domain are not automatically available in others, which enforces isolation and allows for separate type resolution without affecting the broader process.[19][20][21]
To further enhance isolation, particularly for inspection without execution, assemblies can be loaded into a reflection-only context using Assembly.ReflectionOnlyLoad() or Assembly.ReflectionOnlyLoadFrom(), introduced in .NET Framework 2.0. This context permits metadata examination via reflection but prohibits code execution, with each application domain maintaining its own independent reflection-only loads to avoid interference across domains. A key feature supporting seamless updates is shadow copying, enabled via the AppDomainSetup.ShadowCopyFiles property set to true during domain creation, which copies assemblies from their original location in the application base directory or subdirectories to a temporary cache path before loading. This mechanism unlocks the original files, allowing updates without requiring an application domain restart, though it applies only to non-GAC assemblies and can impact startup performance due to the copying overhead.[19][22][23]
Once loaded, managed code executes within the application domain's context, where the common language runtime (CLR) handles just-in-time (JIT) compilation and type resolution scoped exclusively to that domain. Assemblies can be loaded as domain-neutral, sharing JIT-compiled code across domains for efficiency (controlled by the runtime host or LoaderOptimizationAttribute), or non-domain-neutral, compiling separately per domain to support unloading, though domain-neutral assemblies cannot be individually unloaded without unloading the entire domain. Type resolution during execution relies on the domain's assembly probing paths and configuration, ensuring that references to types or methods are resolved locally; failure to locate dependencies may result in a FileNotFoundException. Static fields in domain-neutral assemblies are replicated per domain to maintain isolation, preventing unintended cross-domain state sharing, while application domain-specific state, such as configuration settings and loaded assemblies, remains confined to the domain's lifecycle.[1][24][25]
Resource management during execution ties closely to the domain's boundaries, with objects allocated on domain-bound heaps that enable targeted cleanup upon domain unloading. The CLR's garbage collector operates process-wide but coordinates pauses per application domain, allowing collections of domain-specific objects without necessarily suspending the entire process, which supports efficient memory isolation for multi-domain scenarios. Assemblies remain loaded until the containing application domain is unloaded via AppDomain.Unload(), at which point associated resources, including JIT-compiled code for non-domain-neutral assemblies, are reclaimed, though domain-neutral code persists if shared with other domains.[1][26]
Properties and Configuration
Core Properties
The core properties of an application domain in the .NET Framework provide essential metadata and attributes that define its identity, configuration, and runtime state. These properties are primarily exposed through theAppDomain class, enabling developers to query and inspect domain details during execution without altering the domain itself.[27]
Key properties include the Id, which returns a unique integer identifier for the domain within its hosting process, ensuring distinct referencing across multiple domains.[28] The FriendlyName property supplies a human-readable string name for the domain, often set during creation to facilitate identification in logs or debugging.[29] Evidence captures security-related information associated with the domain, such as the originating site or zone, which informs policy decisions. Additionally, BaseDirectory specifies the root path from which the assembly loader probes for dependencies, typically aligning with the application's installation directory.[30]
Dynamic properties allow runtime access to the active domain context. The static CurrentDomain property retrieves the AppDomain instance for the thread's current execution environment, serving as the primary entry point for domain-specific operations. IsDefaultAppDomain is a boolean that indicates whether the domain is the process's initial, default one created by the common language runtime (CLR). The SetupInformation property returns an AppDomainSetup object encapsulating configuration details like application paths and loader optimization settings, which can be queried but are set during domain initialization (see Configuration Mechanisms for alteration methods).
These properties collectively support monitoring and introspection of application domains at runtime, aiding in tasks such as diagnostics, resource management, and multi-domain orchestration within a single process.[1]
Configuration Mechanisms
Application domains in the .NET Framework can be configured through XML-based configuration files such as app.config for individual applications or machine.config for system-wide settings. These files allow specification of custom application domain managers via the<appDomainManagerAssembly> element under the <runtime> section, which defines the assembly providing the domain manager for the default application domain; for example, <appDomainManagerAssembly value="AdMgrExample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6856bccf150f00b3" /> paired with a corresponding <appDomainManagerType> element enables customized initialization and management.[31] Additionally, the <probing> element within <assemblyBinding> configures assembly resolution by listing subdirectories under the application base for searching private assemblies, such as <probing privatePath="bin;bin2\subbin" />, which directs the runtime to probe paths like the application's bin folder and its subfolders during loading.[32]
API-based configuration is facilitated by the AppDomainSetup class, which provides properties to tailor the environment when creating a new domain via AppDomain.CreateDomain. Key properties include ApplicationBase, which sets the root directory (e.g., a file path like "f:\work\development\latest") for assembly probing and influences the BaseDirectory property of the resulting domain; CachePath, specifying a shadow copy location for assemblies to enable updates without domain restarts; and ConfigurationFile, naming a dedicated XML file (e.g., a .config extension) containing domain-specific settings like binding rules.[33][34] The LoaderOptimization property further controls assembly sharing, with options like SingleDomain for process-wide sharing to optimize performance in single-domain scenarios or MultiDomain for domain-specific loading to enhance isolation in multi-domain hosts.[35] These settings are passed during domain creation and apply only to the new domain, inheriting minimally from the parent (e.g., just the application base).
Runtime adjustments to configuration are possible through methods like AppDomain.ApplyPolicy, which dynamically applies assembly binding policies—such as version redirects—to an assembly name string, returning the post-policy display name for use in loading; this is particularly useful for resolving version conflicts at runtime without restarting the domain, as in scenarios where reflection-only loads bypass standard policy application.[36] For instance, it can enforce redirects defined in configuration files or publisher policies in the global assembly cache, ensuring compatibility with updated assemblies.[37]
In .NET Core and later versions, traditional AppDomain configuration mechanisms like AppDomainSetup and custom domain creation are not supported, as the AppDomain implementation is limited and does not provide isolation or unloading; instead, assembly loading and configuration rely on alternatives such as AssemblyLoadContext for scoped loading, with overall application settings managed via the generic IConfiguration system using JSON files like appsettings.json.[2][38]
Inter-Domain Communication
Communication Methods
Communication between application domains in .NET relies on techniques that enable data exchange and method invocation across isolation boundaries, primarily through the .NET Remoting infrastructure. Cross-domain calls are facilitated via proxies, which act as intermediaries allowing clients in one domain to invoke methods on objects in another without direct access. Objects derived from theMarshalByRefObject class are accessed remotely using these transparent proxies, where method calls are serialized and forwarded to the target domain for execution.[39] Similarly, context-bound objects, which inherit from ContextBoundObject, support transparent invocation by enforcing context-specific behaviors while leveraging proxies for cross-domain access. For passing data, serializable objects are marshaled by value, creating copies in the receiving domain to avoid shared state issues.[10]
A key challenge in these methods is handling non-serializable types, which cannot be directly marshaled by value and often require custom proxies or implementation of interfaces like ISerializable to enable transmission. Additionally, serialization introduces performance overhead, as it involves binary formatting and deserialization of objects, potentially consuming significant CPU and memory resources, especially for large or complex data structures.[10] This overhead can impact latency in high-frequency communication scenarios, making marshal-by-reference preferable for stateful objects to minimize data transfer.[10]
Basic patterns for inter-domain communication include synchronous calls, where a proxy directly invokes a method and blocks until completion, ensuring straightforward execution flow. For non-blocking operations, asynchronous callbacks utilize delegates, allowing the caller to invoke methods via BeginInvoke on a proxy and handle results through callbacks or polling with EndInvoke, thus improving responsiveness in multi-domain applications.[40] These patterns are implemented through the specific remoting framework detailed in subsequent sections.
Remoting and Channels
.NET Remoting, introduced prior to Windows Communication Foundation (WCF), provides a framework for communication between application domains within the same process or across processes and machines in the .NET Framework.[10] It relies on channels to transport messages, with TcpChannel utilizing the TCP protocol and binary formatting for efficient, binary-serialized data transmission, while HttpChannel employs HTTP and SOAP formatting for web-compatible, XML-based serialization.[10] Channels are registered per application domain using ChannelServices, ensuring unique names and ports (e.g., "tcp" for TcpChannel on port 3412), and support both client and server operations for sending and receiving remote calls.[10] Configuration of .NET Remoting is typically handled through application configuration files such as .config or web.config, which define channel properties, port assignments, object URIs, and activation modes like SingleCall (per-request instances), Singleton (shared instance), or client-activated objects.[10] Key components include MarshalByRefObject, a base class for objects intended for remote access, which enables proxy creation and method invocation across boundaries without value serialization.[10] Sinks provide extension points in the message chain for custom processing, such as formatter sinks for serialization or transport sinks for protocol handling.[10] Lifetime services manage remote object duration through a leasing model overseen by a LeaseManager, where objects receive a default five-minute lease renewable for two minutes via sponsor callbacks to prevent premature garbage collection.[10] As of .NET 5.0 and later, Remoting APIs such as GetLifetimeService and InitializeLifetimeService are marked obsolete, issuing SYSLIB0010 compile-time warnings, with runtime calls throwing PlatformNotSupportedException due to the framework's reliance on unsupported application domains.[41] .NET Remoting is unavailable in .NET 6 and subsequent versions, considered a legacy technology confined to the .NET Framework, with Microsoft recommending migration to alternatives like WCF for SOAP services or gRPC for modern inter-process communication.[11]Security and Isolation
Isolation Features
Application domains in the .NET Framework provide isolation through several key mechanisms that enforce boundaries between executing code units within a single process. One primary feature is the separation of application bases, where each domain maintains its own root directory for loading assemblies and configuration files, specified via theAppDomainSetup class during domain creation. This allows independent resolution of modules and resources without interference from other domains.[1]
Threads in application domains exhibit flexible affinity, as they are not bound exclusively to a single domain; instead, a thread can execute code across multiple domains by switching contexts without the need to spawn new threads. Developers can query a thread's current domain using the Thread.GetDomain method to manage such transitions. Complementing this, fault isolation ensures that exceptions thrown within a domain—such as those from unhandled errors in managed code—do not propagate to other domains, provided the code is type-safe, thereby enhancing overall application reliability.[1][42]
Heap and state isolation further delineate boundaries by allocating separate managed heaps per domain, which prevents direct memory access between domains. Static variables are maintained independently in each domain, avoiding shared state that could lead to inconsistencies, while module resolution operates within the domain's scoped application base to isolate assembly loading. Cross-domain object access is mediated through proxies derived from MarshalByRefObject or by marshaling copies of serializable objects, ensuring that resources in one domain remain inaccessible to others without explicit mechanisms.[1][43]
Despite these features, application domains have limitations in isolation, particularly with shared native resources such as file handles or domain-neutral assemblies, which can persist across domains and potentially leak if not carefully managed, as they load into the process-wide space and cannot be unloaded independently. Access to static data across domains may also incur performance overhead due to the need for separate instances per domain.[1]
Security Contexts
In application domains, security contexts primarily revolve around Code Access Security (CAS), a mechanism that enforces permissions based on the evidence associated with loaded assemblies rather than user identity. Evidence, collected by the assembly loader at runtime, includes attributes such as the code's origin (e.g., URL, site, or zone) or digital signatures (e.g., strong name or publisher certificate), which inform the policy resolution process to grant permissions like full trust or partial trust sets.[44] These permissions are applied at the application domain level, allowing distinct trust levels for code within isolated domains while preventing unauthorized access to system resources.[45] The SecurityManager class oversees this process, resolving permissions through a hierarchical policy structure spanning enterprise, machine, and application domain levels, where permissions from each level are intersected to form the effective grant set for the domain.[44] Demand checks, initiated by methods like CodeAccessPermission.Demand(), trigger stack walks that traverse the call stack from the current frame upward, verifying that every caller in the domain possesses the required permissions; failure results in a SecurityException.[44] This ensures that partial trust code, such as assemblies from untrusted internet sources, cannot escalate privileges beyond their evidence-derived boundaries. Application domains also support per-domain principal objects to represent user or thread identity for role-based security, configured via AppDomain.SetPrincipalPolicy to attach principals (e.g., WindowsPrincipal for Windows authentication) to threads executing within the domain.[46] These principals encapsulate identity and roles, complementing CAS by enabling identity-aware decisions, such as authorizing actions based on user group membership. CAS was deprecated in the .NET Framework 4.0 (released in 2010), with its policy model disabled by default in favor of a simplified security transparency approach that relies on code annotations for trust levels rather than runtime permission grants.[47] This legacy mode can be re-enabled for compatibility, but CAS features were fully removed in .NET Core and subsequent .NET versions, prompting migration to modern models like declarative security attributes and operating system-level isolation.[48]Relation to Managed Code
Execution Environment
In the .NET Framework, the Common Language Runtime (CLR) hosts the execution of managed code within application domains, serving as lightweight isolation units that enable secure, versioned, and unloadable processing boundaries inside a single operating system process.[1] The CLR manages the loading of assemblies, verification of type safety, and just-in-time (JIT) compilation of intermediate language (IL) code to native machine instructions, ensuring that code executes within the specified domain's constraints.[49] In the .NET Framework, JIT compilation in application domains is scoped to optimize both sharing and isolation: domain-neutral assemblies, such as those with compatible security grants, are compiled once and shared across multiple domains to conserve memory, while non-domain-neutral assemblies undergo JIT compilation separately in each domain they load into, facilitating independent unloading.[1] This approach supports multiple programming languages through the platform-agnostic IL format, where compilers for languages like C#, Visual Basic, and F# generate IL code that the CLR verifies and compiles uniformly, promoting interoperability without direct native code dependencies.[49] Application domains share the host process's threads, allowing threads to freely cross domain boundaries without creating new ones per domain; a thread executes in the domain of the currently active context, tracked via methods likeThread.GetDomain().[1] Synchronization relies on context flows, where properties such as culture information propagate with the thread across domains—set via Thread.CurrentCulture or the domain's default CultureInfo.DefaultThreadCurrentCulture—ensuring consistent behavior in multithreaded scenarios without inherent domain-level locking primitives.[1] In COM-interop contexts, apartment states (STA or MTA) influence thread affinity, but .NET synchronization mechanisms like Monitor or lock operate at the process level, coordinated through flowing contexts to prevent race conditions across domains.[50]
In the .NET Framework, error handling in application domains is domain-specific, with exceptions like FileNotFoundException arising during cross-domain calls if required metadata is unavailable in the target domain, enforcing isolation.[1] The CLR provides first-chance exception notifications via the AppDomain.FirstChanceException event, alerting handlers before the runtime searches the call stack for catch blocks, allowing early intervention in managed code.[51] In the .NET Framework, for unhandled exceptions, the AppDomain.UnhandledException event fires, enabling logging or cleanup before the domain unloads, though the process may continue if other domains remain active.[52]
Garbage Collection and Resources
In the .NET Framework, the garbage collector operates at the process level, managing a shared managed heap that all application domains within the process utilize for object allocation and reclamation.[53] This shared structure ensures efficient memory management across domains but requires careful handling to avoid cross-domain references that could prevent cleanup. In the .NET Framework, although collections are not performed independently per domain, the runtime supports application domain resource monitoring (ARM), which attributes managed memory allocations and survival rates to specific domains following full, blocking garbage collections—such as those initiated byGC.Collect().[54] ARM also tracks CPU time consumed by each domain, excluding time spent in blocked or sleeping threads, to help diagnose resource imbalances in multi-domain applications.[54]
In the .NET Framework, when an application domain is unloaded via AppDomain.Unload, objects instantiated within it lose reachability from other domains or the process's root set, rendering them eligible for garbage collection.[55] The unload process aborts any managed threads executing in the domain and invokes finalizers for objects with them using a dedicated finalizer thread, distinct from routine GC finalization.[56] This step ensures deterministic cleanup for domain-specific objects before memory reclamation, though the unload itself does not block for a full collection; developers typically follow it with GC.Collect() and GC.WaitForPendingFinalizers() to trigger a generation-spanning collection and complete finalizer execution promptly.[57] Generation heaps remain shared, so collections reclaim domain-specific garbage alongside process-wide debris, potentially compacting the heap to mitigate fragmentation.
Application domains own and isolate certain managed resources, such as garbage collector handles (GCHandle), which can be enumerated and analyzed per domain using debugging extensions like SOS.dll to detect leaks or excessive pinning.[58] Timers created via System.Threading.Timer and event handlers registered within a domain execute callbacks in that domain's context, binding their lifetime to the domain and allowing cleanup upon unload without affecting other domains. Shared unmanaged resources, however—such as native handles or COM objects—must be explicitly disposed using the IDisposable pattern, as the garbage collector cannot reclaim them automatically and cross-domain sharing risks leaks if not handled.[59]
For high-throughput scenarios involving multiple application domains, the runtime's server garbage collection mode offers optimizations by dedicating one heap per logical processor and enabling concurrent collections to minimize pauses.[60] This mode is configured process-wide via the application's app.config file (e.g., <gcServer enabled="true"/>), but its benefits scale effectively in domain-heavy workloads by distributing collection efforts across processors, reducing latency for domain-unload-induced cleanups.[61]
In .NET Core and later versions, AppDomain functionality is limited to a single domain per process without support for isolation or unloading; for related features, see the "Changes in .NET Core and Later" section.[2]