Rico Suter's blog.
 


For one of my projects, I had to load external assemblies and query the exported types (i.e. the public classes) using reflection. Everything worked fine, except that the .DLL files were being locked until the application was terminated.

The problem is, that once you load an assembly into the main AppDomain using Assembly.LoadFrom(), it stays loaded and you cannot unload the assembly. Also, loading an assembly into the AppDomain where your other code is running might have unexpected side effects. To avoid all these problems, we need to do the work in a secondary, isolated AppDomain. An AppDomain is like another process in your application with its own set of loaded assemblies and types. To communicate from one to the other AppDomain, we have to use .NET remoting or another IPC mechanism - for example WCF over named pipes. In the samples in this article, I will use .NET remoting by inheriting from MarshalByRefObject.

First, we implement a reusable, generic class which executes code in a new, isolated AppDomain. As you can see in the source code below, the class creates an AppDomain; then instanciates an object of the generic type inside this AppDomain. In the Dispose() method, the AppDomain is unloaded and all file locks on the assemblies are released.

public sealed class AppDomainIsolation<T> : IDisposable 
    where T : MarshalByRefObject
{
    private AppDomain _domain;
    private readonly T _object;

    public AppDomainIsolation(string assemblyDirectory)
    {
        var setup = new AppDomainSetup
        {
            ShadowCopyFiles = "true",
            ApplicationBase = assemblyDirectory, 
            ConfigurationFile = GetConfigurationPath(assemblyDirectory)
        };

        _domain = AppDomain.CreateDomain("AppDomainIsolation:" + Guid.NewGuid(), null, setup);

        var type = typeof(T);
        _object = (T)_domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
    }

    public T Object
    {
        get { return _object; }
    }

    public void Dispose()
    {
        if (_domain != null)
        {
            AppDomain.Unload(_domain);
            _domain = null;
        }
    }

    public string GetConfigurationPath(string assemblyDirectory)
    {
        var config = Path.Combine(assemblyDirectory, "App.config");
        if (File.Exists(config))
            return config;

        config = Path.Combine(assemblyDirectory, "Web.config");
        if (File.Exists(config))
            return config;

        return config; 
    }
}

The next step is to implement the worker class which will be instantiated in the new AppDomain by the AppDomainIsolation class. The methods in this class will then be executed in the second, isolated AppDomain. The sample worker class looks as follows:

public class ExportedTypesWorker : MarshalByRefObject
{
    public string[] GetExportedTypes(string assemblyPath)
    {
        var assembly = Assembly.LoadFrom(assemblyPath);
        return assembly.ExportedTypes
            .Select(t => t.FullName)
            .OrderBy(t => t)
            .ToArray();
    }
}

Now, the two classes AppDomainIsolation and ExportedTypesWorker can be used to get the exported types of an assembly via an isolated AppDomain. The parameter assemblyDirectory of the AppDomainIsolation class specifies where to look dependent DLLs:

public string[] GetExportedTypes(string assemblyPath)
{
    using (var loader = new AppDomainIsolation<ExportedTypesWorker>(Path.GetDirectoryName(assemblyPath)))
    {
        return loader.Object.GetExportedTypes(assemblyPath);
    }
}

In this sample, we only use strings as input and output parameters. “Complex” objects must either derive from MarshalByRefObject or be marked as Serializable.



Discussion