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

Posted

技术标签:

【中文标题】如何动态加载和卸载(重新加载).dll 程序集【英文标题】:How to dynamically load and unload (reload) a .dll assembly 【发布时间】:2020-08-27 13:03:38 【问题描述】:

我正在为外部应用程序开发一个模块,这是一个加载的 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。但是现在我收到了消息

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

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

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

【问题讨论】:

这是开发级代码吗?例如如果不重新启动,您不会在生产中重新加载您的 DLL,但出于开发目的,一切正常吗? 【参考方案1】:

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

示例:

var appDomain = AppDomain.CreateDomain("MyAppDomain", null, new AppDomainSetup

    ApplicationName = "MyAppDomain",
    ShadowCopyFiles = "true",
    PrivateBinPath = "MyAppDomainBin",
);

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

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

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

但是,这非常重要,如果您的类不继承自 MarshalByRefObjectCreateInstanceAndUnwrap 的“Unwrap”部分将导致程序集加载到您的主应用程序域中。所以基本上你通过创建一个应用域一事无成。

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

例如:

public interface IMyInterface

    void DoSomething();

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

public class MyClass : MarshalByRefObject, IMyInterface

    public void DoSomething()
    
        Console.WriteLine("Doing something.");
    

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

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

现在您可以调用对象上的方法,.NET 运行时将使用 Remoting 将调用转发到另一个域。它将使用序列化来序列化参数并从两个域返回值。因此,请确保您在参数和返回值中使用的类都标有[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。在那些版本中,分离是通过创建单独的进程而不是应用程序域来实现的。并使用某种消息传递库在进程之间进行通信。

【讨论】:

【参考方案2】:

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

这里的一些答案假设使用 .NET Framework。在 .NET Core 3 和 .NET 5+ 中,在同一进程中加载​​程序集(能够卸载它们)的正确方法是使用 AssemblyLoadContext。将 AppDomain 用作隔离程序集的方法仅限于 .NET Framework。

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

    加载另一个进程并在那里加载您的动态程序集。然后使用您选择的 IPC 消息系统在进程之间发送消息。 使用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

【讨论】:

感谢您的意见!对我来说有一个很大的限制:主应用程序不在我的控制之下,我们只为它写了一个插件。【参考方案3】:

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

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

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

【讨论】:

【参考方案4】:

这是加载和卸载 AppDomain 的示例。 在我的示例中,我有 2 个 Dll:DynDll.dll 和 DynDll1.dll。 两个 Dll 都有相同的类 DynDll.Class 和一个方法 Run(需要 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);

【讨论】:

【参考方案5】:

这是一个想法,而不是直接加载 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);

【讨论】:

以上是关于如何动态加载和卸载(重新加载).dll 程序集的主要内容,如果未能解决你的问题,请参考以下文章

C#.Net 如何动态加载与卸载程序集(.dll或者.exe)

C#动态加载dll 时程序集的卸载问题

C#.Net 如何动态加载与卸载程序集(.dll或者.exe)3---- 动态加载Assembly应用程序

vb.net编程,如何使用 appdomain 实现某进程DLL动态加载和卸载?

C#中如何动态加载和卸载DLL

将 c# 程序集动态加载和卸载到 appdomain