Technical Implementation of Recursively Loading Assemblies with All References into AppDomain

Dec 11, 2025 · Programming · 7 views · 7.8

Keywords: AppDomain | Assembly Loading | MarshalByRefObject

Abstract: This article delves into how to load assemblies and all their dependencies recursively into a new AppDomain in the .NET environment. By analyzing common FileNotFoundException errors, it explains the assembly loading mechanism in detail and provides a solution based on the best answer using MarshalByRefObject proxy classes. The content covers AppDomain creation, assembly resolution strategies, limitations of automatic dependency loading, and technical details of handling assemblies in non-standard paths via the LoadFile method. It also discusses applicable scenarios for different loading methods, offering practical guidance for managing assemblies in complex dependency environments.

Assembly Loading Mechanism and AppDomain Fundamentals

In the .NET framework, assemblies are the basic units of code deployment and version control. When loading assemblies into an isolated execution environment, AppDomain provides a runtime container with isolation. However, when loading an assembly into a new AppDomain, its dependencies are not automatically resolved, often leading to FileNotFoundException errors indicating "could not load file or assembly or one of its dependencies."

Traditional loading methods like domain.Load(AssemblyName.GetAssemblyName(path)) only attempt to load the specified assembly without handling its reference chain. Even manually traversing references and loading them, such as Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies(), may fail due to recursive dependencies or path issues. For example, a reference tree like MyDll.dll → Microsoft.Office.Interop.Excel.dll → Microsoft.Vbe.Interop.dll → Office.dll → stdole.dll requires a complete resolution strategy.

Using MarshalByRefObject Proxy Classes

The core solution involves executing loading operations in the target AppDomain through proxy classes to avoid passing assembly instances back to the calling domain. Proxy classes inheriting MarshalByRefObject enable communication across domains, ensuring loading logic completes within the target domain.

The following code example demonstrates how to create a proxy class and load an assembly:

class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

In this method, CreateInstanceAndUnwrap instantiates the proxy class in the target domain, and the GetAssembly method uses LoadFile to load the assembly. Note that the proxy class assembly must be accessible to both domains, typically achieved by sharing the ApplicationBase.

Selection of Assembly Loading Methods and Dependency Handling

Choosing the correct loading method is crucial. LoadFrom triggers the assembly resolver to search in the GAC or the current application's bin folder, potentially causing FileNotFound exceptions. In contrast, LoadFile directly loads arbitrary files but requires manual handling of dependencies. Using LoadFile in proxy classes avoids path conflicts, but developers must ensure all dependent assemblies are in accessible locations.

For complex reference trees, dependencies need to be loaded recursively. While the proxy class method simplifies this process, if dependency paths differ, adjust the AppDomain's search path. For example, specify a dll search path when creating the domain: AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true), enabling the resolver to automatically load dependencies from that path.

Practical Recommendations and Common Issues

In practice, it is advisable to keep the ApplicationBase unchanged unless specific path requirements exist. This ensures the new domain loads only necessary assemblies, reducing errors. If dependency loading fails, check the proxy class implementation and path settings. Additionally, using ReflectionOnlyLoadFrom can retrieve reference information without loading the assembly, aiding in building dependency trees.

In summary, executing loading via MarshalByRefObject proxy classes in the target AppDomain, combined with the LoadFile method, effectively loads assemblies and their references recursively. This approach enhances code isolation and maintainability, suitable for scenarios like plugin systems and dynamic module loading.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.