Search code examples
c#dllappdomain

How to dynamically load and unload (reload) a .dll assembly


I'm developing a module for an external application, which is a dll that is loaded.

However in order to develop, you have to restart the application to see the results of your code.

We have built a piece of code that loads a dll dynamically from a startassembly:

startassembly

var dllfile = findHighestAssembly(); // this works but omitted for clarity
Assembly asm = Assembly.LoadFrom(dllFile);
Type type = asm.GetType("Test.Program");
MethodInfo methodInfo = type.GetMethod("Run");
object[] parametersArray = new object[] { };
var result = methodInfo.Invoke(methodInfo, parametersArray);

Effectively we have a solution with a startassembly which will be static and a test assembly which will be invoked dynamically, which allows us to swap the assembly during runtime.

The problem This piece of code will load a new dll every time and search for the highest version at the end of the assembly name. e.g. test02.dll will be loaded instead of test01.dll, because the application locks both startassemly.dll as well as test01.dll. Now we have to edit the properties > assembly name all the time.

I want to build a new dll while the main application still runs. However for now I get the message

The process cannot access the file test.dll because it is being used by another process

I have read that you can unload a .dll using AppDomains however the problem is that I don't know how to properly unload an AppDomain and where to do this.

The goal is to have to reload the new test.dll everytime the window is re-opened (by a button click from the main application).


Solution

  • Edit: This answer applies to .NET Framework 4 and earlier versions. .NET core 1,2 and 3 and .NET 5 or later do not support the concept of AppDomains and remoting.

    You cannot unload a single assembly, but you can unload an Appdomain. This means you need to create an app domain and load the assembly in the App domain.

    Exmaple:

    var appDomain = AppDomain.CreateDomain("MyAppDomain", null, new AppDomainSetup
    {
        ApplicationName = "MyAppDomain",
        ShadowCopyFiles = "true",
        PrivateBinPath = "MyAppDomainBin",
    });
    

    ShadowCopyFiles property will cause the .NET Framework runtime to copy dlls in "MyAppDomainBin" folder to a cache location so as not to lock the files in that path. Instead the cached files are locked. For more information refer to article about Shadow Copying Assemblies

    Now let's say you have an class you want to use in the assembly you want to unload. In your main app domain you call CreateInstanceAndUnwrap to get an instance of the object

    _appDomain.CreateInstanceAndUnwrap("MyAssemblyName", "MyNameSpace.MyClass");
    

    However, and this is very important, "Unwrap" part of CreateInstanceAndUnwrap will cause the assembly to be loaded in your main app domain if your class does not inherit from MarshalByRefObject. So basically you achieved nothing by creating an app domain.

    To solve this problem, create a 3rd Assembly containing an Interface that is implemented by your class.

    For example:

    public interface IMyInterface
    {
        void DoSomething();
    }
    

    Then add reference to the assembly containing the interface in both your main application and your dynamically loaded assembly project. And have your class implement the interface, and inherit from MarshalByRefObject. Example:

    public class MyClass : MarshalByRefObject, IMyInterface
    {
        public void DoSomething()
        {
            Console.WriteLine("Doing something.");
        }
    }
    

    And to get a reference to your object:

    var myObj = (IMyInterface)_appDomain.CreateInstanceAndUnwrap("MyAssemblyName", "MyNameSpace.MyClass");
    

    Now you can call methods on your object, and .NET Runtime will use Remoting to forward the call to the other domain. It will use Serialization to serialize the parameters and return values to and from both domains. So make sure your classes used in parameters and return values are marked with [Serializable] Attribute. Or they can inherit from MarshalByRefObject in which case the you are passing a reference across domains.

    To have your application monitor changes to the folder, you can setup a FileSystemWatcher to monitor changes to the folder "MyAppDomainBin"

    var watcher = new FileSystemWatcher(Path.GetFullPath(Path.Combine(".", "MyAppDomainBin")))
    {
        NotifyFilter = NotifyFilters.LastWrite,
    };
    watcher.EnableRaisingEvents = true;
    watcher.Changed += Folder_Changed;
    

    And in the Folder_Changed handler unload the appdomain and reload it again

    private static async void Watcher_Changed(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine("Folder changed");
        AppDomain.Unload(_appDomain);
        _appDomain = AppDomain.CreateDomain("MyAppDomain", null, new AppDomainSetup
        {
            ApplicationName = "MyAppDomain",
            ShadowCopyFiles = "true",
            PrivateBinPath = "MyAppDomainBin",
        });
    }
    

    Then when you replace your DLL, in "MyAppDomainBin" folder, your application domain will be unloaded, and a new one will be created. Your old object references will be invalid (since they reference objects in an unloaded app domain), and you will need to create new ones.

    A final note: AppDomains and .NET Remoting are not supported in .NET Core or future versions of .NET (.NET 5+). In those version, separation is achieved by creating separate processes instead of app domains. And using some sort of messaging library to communicate between processes.