Today I had an issue with assemblies in an AppDomain. What I tried to do was to create a new AppDomain, load assemblies into that AppDomain, analyze the types in these assemblies, and unload the AppDomain afterwards. Sounds like straight-forward, but I overlooked a little details and ended up with these assemblies being loaded into my main AppDomain and me not being able to unload them. So to begin with, here’s the some (faulty) code:

internal interface IAssemblyAnalyzer
{
    string[] Analyze(string assemblyPath);
}

internal class AssemblyAnalyzer : MarshalByRefObject, IAssemblyAnalyzer
{
    public string[] Analyze(string assemblyName)
    {
        var assembly = AppDomain.CurrentDomain.Load(assemblyName);
        return assembly.GetTypes().Select(t => t.FullName).ToArray();
    }
}

internal class Program
{
    private static void Main(string[] args)
    {
        var appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, new AppDomainSetup
            {
                ApplicationBase = @"c:\tmp",
                PrivateBinPath = @"c:\tmp",
                ShadowCopyFiles = "true"
            });

        var type = typeof (AssemblyAnalyzer);
        var analyzer = (IAssemblyAnalyzer) appDomain.CreateInstanceFromAndUnwrap(type.Assembly.Location, type.FullName);
        var result = analyzer.Analyze("MunirHusseini.MysteriousAssembly");

        Console.WriteLine(string.Join(@"\r\n", result));

        AppDomain.Unload(appDomain);
    }
}

In the code above, the assembly “MunirHusseini.MysteriousAssembly” is loaded into a new AppDomain. In that domain, all types inside the assembly are read-out and their names are returned. At the end, the AppDomain gets unloaded and with it all assemblies that were loaded into that assembly. Or don’t they? In my case, they didn’t. The assembly “MunirHusseini.MysteriousAssembly” that is located in “c:\tmp” will be locked until the process is shut down.

The trick here was to modify the AppDomainSetup and set the LoaderOptimization to LoaderOptimization.MultiDomainHost. This makes sure that no assemblies that are loaded into (or inside) the new AppDomain will be shared across AppDomain boundaries (except for assemblies that are in the GAC).

var appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, new AppDomainSetup
    {
        ApplicationBase = @"c:\tmp",
        PrivateBinPath = @"c:\tmp",
        ShadowCopyFiles = "true",
        LoaderOptimization = LoaderOptimization.MultiDomainHost
    });

So for best separation of AppDomains, do the following:

  1. Types called across AppDomain boundaries must inherit MarshalByRefObject.
  2. Types called across AppDomain boundaries must be called via an interface (IAssemblyAnalyzer in this sample).
  3. The property LoaderOptimization must be set to LoaderOptimization.MultiDomainHost.

Items 1 and 2 prevent that the type that is called across AppDomain boundaries will be loaded into the calling AppDomain, while item 3 prevents that any assemblies that are loaded from inside the new AppDomain will be loaded into the calling AppDomain.