如何动态加载和卸载(重新加载).dll 程序集

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

我正在为外部应用程序开发一个模块,它是一个加载的dll。

但是为了开发,您必须重新启动应用程序才能看到代码的结果。

我们构建了一段从 startassembly 动态加载 dll 的代码:

开始组装

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

实际上我们有一个解决方案,其中包含一个静态的 startassembly 和一个动态调用的测试程序集,这允许我们在运行时交换程序集。

问题 这段代码每次都会加载一个新的dll,并在程序集名称的末尾搜索最高版本。例如test02.dll 将被加载而不是 test01.dll,因为应用程序同时锁定了 startassemly.dll 和 test01.dll。现在我们必须一直编辑属性 > 程序集名称。

我想在主应用程序仍在运行时构建一个新的 dll。但是现在我收到消息

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

我了解到您可以使用 AppDomains 卸载 .dll,但问题是我不知道如何正确卸载 AppDomain 以及在哪里执行此操作。

目标是每次重新打开 window 时都必须重新加载新的 test.dll(通过从主应用程序单击按钮)。

您在发布的代码中尝试做的是卸载默认应用程序域,如果未指定另一个域,您的程序将 运行 在该域中。您可能想要的是加载一个新的应用程序域,将程序集加载到该新的应用程序域中,然后在用户销毁页面时卸载新的应用程序域。

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

上面的参考页应该为您提供所有这些的工作示例。

这是加载和卸载 AppDomain 的示例。
在我的示例中,我有 2 个 Dll:DynDll.dll 和 DynDll1.dll.
两个 Dll 都有相同的 class DynDll.Class 和一个方法 运行(需要 MarshalByRefObject):

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

现在您可以创建动态 AppDomain 并加载程序集:

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);
}

这里有一个想法,不是直接加载DDL(原样),而是让应用重命名它,然后加载重命名的ddl(例如test01_active.dll)。然后,只需在加载程序集之前检查原始文件 (test01.dll),如果存在,只需删除当前文件 (test01_active.dll),然后重命名更新版本,然后重新加载它,依此类推。

这里有一段代码展示了这个想法:

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

您不能卸载单个程序集,但可以卸载 Appdomain。这意味着您需要创建一个应用程序域并在应用程序域中加载程序集。

示例:

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

ShadowCopyFiles 属性 将导致 .NET 运行时将“MyAppDomainBin”文件夹中的 dll 复制到缓存位置,以免锁定该路径中的文件。相反,缓存的文件被锁定。有关详细信息,请参阅有关 Shadow Copying Assemblies

的文章

现在假设您有一个要在要卸载的程序集中使用的 class。在您的主应用程序域中,您调用 CreateInstanceAndUnwrap 来获取对象的实例

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

然而,这非常重要,如果您的 class 不继承自 MarshalByRefObject。所以基本上你通过创建一个应用程序域没有任何收获。

要解决此问题,请创建一个包含由 class 实现的接口的第 3 个程序集。

例如:

public interface IMyInterface
{
    void DoSomething();
}

然后在主应用程序和动态加载程序集项目中添加对包含接口的程序集的引用。并让您的 class 实现接口,并继承自 MarshalByRefObject。示例:

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

并获取对您的对象的引用:

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

现在您可以调用对象上的方法,.NET 运行时将使用 Remoting 将调用转发到其他域。它将使用序列化来序列化传入和传出两个域的参数和 return 值。因此,请确保您在参数和 return 值中使用的 classes 标记有 [Serializable] 属性。或者它们可以从 MarshalByRefObject 继承,在这种情况下,您传递的是参考跨域。

要让您的应用程序监视对文件夹的更改,您可以设置一个 FileSystemWatcher 来监视对文件夹“MyAppDomainBin”的更改

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

然后在 Folder_Changed 处理程序中卸载 appdomain 并重新加载它

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",
    });
}

然后当您替换您的 DLL 时,在“MyAppDomainBin”文件夹中,您的应用程序域将被卸载,并创建一个新的。您的旧对象引用将无效(因为它们引用卸载的应用程序域中的对象),您将需要创建新对象。

最后一点:.NET Core 或 .NET (.NET 5+) 的未来版本不支持 AppDomains 和 .NET Remoting。在那些版本中,分离是通过创建单独的进程而不是应用程序域来实现的。并使用某种消息传递库在进程之间进行通信。

不是 .NET Core 3 和 .NET 5+ 的前进方向

此处的一些答案假定使用 .NET Framework。在 .NET Core 3 和 .NET 5+ 中,在同一进程中加载​​程序集(能够卸载它们)的正确方法是使用 AssemblyLoadContext。使用 AppDomain 作为隔离程序集的方式严格适用于 .NET Framework。

.NET Core 3 和 5+,为您提供两种加载动态程序集(并可能卸载)的可能方法:

  1. 加载另一个进程并在那里加载您的动态程序集。然后使用您选择的 IPC 消息传递系统在进程之间发送消息。
  2. 使用AssemblyLoadContext在同一进程中加载​​它们。请注意,作用域不在流程中提供任何类型的安全隔离或边界。换句话说,在单独的上下文中加载的代码仍然能够调用同一进程中其他上下文中的其他代码。如果你想隔离代码,因为你希望加载你不能完全信任的程序集,那么你需要在一个完全独立的进程中加载​​它并依赖 IPC。

一篇解释 AssemblyLoadContext 的文章is here

讨论了插件的可卸载性 here

许多想要动态加载 DLL 的人都对插件模式感兴趣。 MSDN 实际上在这里涵盖了这个特定的实现: https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support

2021-9-12 更新

现成的插件库

我使用以下库来加载插件。它对我来说非常有效: https://github.com/natemcmaster/DotNetCorePlugins