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

onl*_*mas 2 c# dll appdomain

我正在为外部应用程序开发一个模块,它是一个加载的 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);
Run Code Online (Sandbox Code Playgroud)

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

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

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

该进程无法访问文件 test.dll,因为它正被另一个进程使用

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

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

Jam*_*oux 11

这不是 .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 的文章。

这里讨论插件的可卸载性。

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

2021-9-12 更新

现成的插件库

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


She*_*iny 5

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

例子:

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

ShadowCopyFiles 属性将导致 .NET 运行时将“MyAppDomainBin”文件夹中的 dll 复制到缓存位置,以免锁定该路径中的文件。相反,缓存的文件被锁定。有关更多信息,请参阅有关卷影复制程序集的文章

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

_appDomain.CreateInstanceAndUnwrap("MyAssemblyName", "MyNameSpace.MyClass");
Run Code Online (Sandbox Code Playgroud)

但是,这非常重要CreateInstanceAndUnwrap如果您的类不继承自MarshalByRefObject. 所以基本上你通过创建一个应用程序域什么也没做。

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

例如:

public interface IMyInterface
{
    void DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

然后在主应用程序和动态加载的程序集项目中添加对包含接口的程序集的引用。并让您的类实现接口,并从MarshalByRefObject. 例子:

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

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

var myObj = (IMyInterface)_appDomain.CreateInstanceAndUnwrap("MyAssemblyName", "MyNameSpace.MyClass");
Run Code Online (Sandbox Code Playgroud)

现在您可以调用对象上的方法,.NET 运行时将使用远程处理将调用转发到其他域。它将使用序列化来序列化参数并从两个域返回值。因此,请确保您在参数和返回值中使用的类都标有[Serializable]Attribute。或者他们可以继承,MarshalByRefObject在这种情况下,您传递的是跨域引用。

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

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

并在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",
    });
}
Run Code Online (Sandbox Code Playgroud)

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

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