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

5

8

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).

Primero answered 27/8, 2020 at 13:3 Comment(1)
Is this to be a dev grade code? e.g. You will not reload your DLL in production without a restart, but for dev purposes, anything goes?Owens
L
9

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.

Lynnelynnea answered 3/9, 2020 at 17:42 Comment(0)
B
12

Not the way forward in .NET Core 3 and .NET 5+

Some of the answers here assume working with .NET Framework. In .NET Core 3 and .NET 5+, the correct way to load assemblies (with ability to unload them) in the same process is with AssemblyLoadContext. Using AppDomain as a way to isolate assemblies is strictly for .NET Framework.

.NET Core 3 and 5+, give you two possible ways to load dynamic assemblies (and potentially unload):

  1. Load another process and load your dynamic assemblies there. Then use an IPC messaging system of your choosing to send messages between the processes.
  2. Use AssemblyLoadContext to load them in the same process. Note that the scope does NOT provide any kind of security isolation or boundaries within the process. In other words, code loaded in a separate context is still able to invoke other code in other contexts within the same process. If you want to isolate the code because you expect to be loading assemblies that you can't fully trust, then you need to load it in a completely separate process and rely on IPC.

An article explaining AssemblyLoadContext is here.

Plugin unloadability discussed here.

Many people who want to dynamically load DLLs are interested in the Plugin pattern. The MSDN actually covers this particular implementation here: https://learn.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support

2021-9-12 UPDATE

Off-the-Shelf Library for Plugins

I use the following library for plugin loading. It has worked extremely well for me: https://github.com/natemcmaster/DotNetCorePlugins

Bryonbryony answered 14/2, 2021 at 20:37 Comment(3)
I appreciate your input! For me there were 1 big restriction: the main application was not under my control, we only wrote a plugin for it.Primero
The "trick" is to figure out a way that will work with BOTH Framework 4.x AND with .NET 6.0 and beyond.... There are applications running the full framework that need dynamic assemblies, and while scheduled to be migrated, are not ready for migration yet.....Socher
.NET Standard was part of an effort to move to .NET Core, which is a dramatic departure from .NET Framework. .NET Core completely changes the runtime assembly space. I don't think you're going to find any trick that would work for both spaces. Also if you're still running legacy stuff in a heterogenous runtime with a mix of Core and Framework 4.x in 2022, you're in dire need of a migration initiative. You're only going to acrue more technical debt as time goes on.Bryonbryony
L
9

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.

Lynnelynnea answered 3/9, 2020 at 17:42 Comment(0)
A
2

what you're trying to do in the code you posted is unload the default app domain which your program will run in if another isn't specified. What you're probably wanting is to load a new app domain, load the assembly into that new app domain, and then unloaded the new app domain when the user destroys the page.

https://learn.microsoft.com/en-us/dotnet/api/system.appdomain?view=netframework-4.7

the reference page above should give you a working example of all of this.

Afloat answered 27/8, 2020 at 14:5 Comment(0)
F
1

Here is an example for loading and unloading an AppDomain.
In my example I have 2 Dll's: DynDll.dll and DynDll1.dll.
Both Dll's have the same class DynDll.Class and a method Run (MarshalByRefObject is required):

public class Class : MarshalByRefObject
{
    public int Run()
    {
        return 1; //DynDll1 return 2
    }
}

Now you can create a dynamic AppDomain and load a Assembly:

AppDomain loDynamicDomain = null;
try
{
    //FullPath to the Assembly
    string lsAssemblyPath = string.Empty;
    if (this.mbLoad1)
        lsAssemblyPath = Path.Combine(Application.StartupPath, "DynDll1.dll");
    else
        lsAssemblyPath = Path.Combine(Application.StartupPath, "DynDll.dll");
    this.mbLoad1 = !this.mbLoad1;

    //Create a new Domain
    loDynamicDomain = AppDomain.CreateDomain("DynamicDomain");
    //Load an Assembly and create an instance DynDll.Class
    //CreateInstanceFromAndUnwrap needs the FullPath to your Assembly
    object loDynClass = loDynamicDomain.CreateInstanceFromAndUnwrap(lsAssemblyPath, "DynDll.Class");
    //Methode Info Run
    MethodInfo loMethodInfo = loDynClass.GetType().GetMethod("Run");
    //Call Run from the instance
    int lnNumber = (int)loMethodInfo.Invoke(loDynClass, new object[] { });
    Console.WriteLine(lnNumber.ToString());
}
finally
{
    if (loDynamicDomain != null)
        AppDomain.Unload(loDynamicDomain);
}
Finder answered 1/9, 2020 at 11:44 Comment(0)
N
1

Here is an idea, instead of loading the DDL directly (as is), let the application rename it, then load the renamed ddl (e.g. test01_active.dll). Then, just check for the original file (test01.dll) before loading the assembly and if exists, just delete the current one(test01_active.dll) and then rename the updated version then reload it, and so on.

Here is a code shows the idea :

const string assemblyDirectoryPath = "C:\\bin";
const string assemblyFileNameSuffix = "_active";

var assemblyCurrentFileName     = "test01_active.dll";
var assemblyOriginalFileName    = "test01.dll";

var originalFilePath = Path.Combine(assemblyDirectoryPath, assemblyOriginalFileName);
var currentFilePath  = Path.Combine(assemblyDirectoryPath, assemblyCurrentFileName);

if(File.Exists(originalFilePath))
{
    File.Delete(currentFilePath);
    File.Move(originalFilePath, currentFilePath);
}

Assembly asm = Assembly.LoadFrom(currentFilePath);
Type type = asm.GetType("Test.Program");
MethodInfo methodInfo = type.GetMethod("Run");
object[] parametersArray = new object[] { };
var result = methodInfo.Invoke(methodInfo, parametersArray);
Neelyneeoma answered 2/9, 2020 at 23:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.