You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于.NET示例利用AssemblyLoadContext卸载托管DLL并实现版本替换

基于AssemblyLoadContext实现程序集卸载与DLL替换

问题场景

想要实现完整的插件生命周期:加载DLL(v1.0)→执行代码→卸载DLL→替换为新版本DLL(v1.1)→重新加载并执行更新代码。但当前基于微软插件示例实现的代码无法真正释放DLL,无法在不关闭主应用的情况下删除或覆盖目标文件,程序集被存储在全局List<Assembly>中。

现有代码问题

当前代码无法卸载的核心原因:

  • 全局List<Assembly>和IEnumerable<ICommand>持有了程序集及插件实例的强引用,导致GC无法回收对应的AssemblyLoadContext
  • 创建PluginLoadContext后未保存引用,无法主动调用Unload()方法触发卸载

以下是现有实现代码:

全局变量与加载流程

static IEnumerable<ICommand> Plugs = Enumerable.Empty<ICommand>();
static readonly List<Assembly> PluginAssemblies = new();
static readonly List<string> PluginPath = new();

PluginPath.Add("C:\\...\\Plugin1.dll");
PluginPath.Add("C:\\...\\Plugin2.dll");
PluginPath.Add("C:\\...\\Plugin3.dll");
PluginPath.Add("C:\\...\\Plugin4.dll");

PluginPath.ForEach(P => { PluginAssemblies.Add(LoadPlugin(P)); });

Plugs = Plugs.Concat(PluginAssemblies.SelectMany(A => CreateCommands(A)));

LoadPlugin方法

static Assembly LoadPlugin(string relativePath)
{
    string root = Path.GetFullPath(Path.Combine(
        Path.GetDirectoryName(
            Path.GetDirectoryName(
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));

    string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
    Console.WriteLine($"Loading commands from: {pluginLocation}");
    PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
    return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
}

CreateCommands方法

static IEnumerable<ICommand> CreateCommands(Assembly assembly)
{
    int count = 0;

    foreach (Type type in assembly.GetTypes())
    {
        if (typeof(ICommand).IsAssignableFrom(type))
        {
            ICommand result = Activator.CreateInstance(type) as ICommand;
            if (result != null)
            {
                count++;
                yield return result;
            }
        }
    }

    if (count == 0)
    {
        string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName));
        throw new ApplicationException(
            $"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" +
            $"Available types: {availableTypes}");
    }
}

PluginLoadContext类

using System;
using System.Reflection;
using System.Runtime.Loader;

namespace AppWithPlugin
{
    class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }

            return IntPtr.Zero;
        }
    }
}

解决方案

1. 重构存储结构,保存上下文引用

创建容器类统一管理插件的上下文、程序集和实例,避免分散的强引用:

public class PluginContainer
{
    public PluginLoadContext LoadContext { get; set; }
    public Assembly Assembly { get; set; }
    public List<ICommand> Commands { get; set; }
}

// 替换原全局变量
static IEnumerable<ICommand> Plugs = Enumerable.Empty<ICommand>();
static readonly List<PluginContainer> PluginContainers = new();
static readonly List<string> PluginPath = new();

2. 修改加载逻辑,保存完整插件信息

调整LoadPlugin方法,返回包含上下文的容器,并更新加载流程:

static PluginContainer LoadPlugin(string pluginPath)
{
    string pluginLocation = Path.GetFullPath(pluginPath);
    Console.WriteLine($"Loading commands from: {pluginLocation}");
    var loadContext = new PluginLoadContext(pluginLocation);
    var assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
    var commands = CreateCommands(assembly).ToList();
    
    return new PluginContainer
    {
        LoadContext = loadContext,
        Assembly = assembly,
        Commands = commands
    };
}

// 更新加载流程
PluginPath.ForEach(p => 
{
    var container = LoadPlugin(p);
    PluginContainers.Add(container);
});

Plugs = PluginContainers.SelectMany(c => c.Commands);

3. 实现完整卸载逻辑

卸载时必须释放所有相关强引用,并触发Unload和GC回收:

static void UnloadAllPlugins()
{
    // 1. 清空全局命令引用,释放插件实例
    Plugs = Enumerable.Empty<ICommand>();
    
    // 2. 遍历容器,调用Unload并清空内部引用
    foreach (var container in PluginContainers)
    {
        container.LoadContext.Unload();
        container.Commands.Clear();
        container.Assembly = null;
        container.LoadContext = null;
    }
    
    // 3. 清空容器列表
    PluginContainers.Clear();
    
    // 4. 强制触发GC,加速上下文回收(按需使用)
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

4. 验证与排查

卸载后若仍无法操作DLL,需检查:

  • 是否有事件注册未取消(插件实例订阅了主应用的事件)
  • 是否存在静态变量持有插件类型或实例的引用
  • 是否有未处理的异常导致实例被保留在内存中

完整生命周期示例

// 加载v1.0插件
LoadPlugins();
// 执行插件逻辑
ExecutePluginCommands();
// 卸载插件
UnloadAllPlugins();
// 替换DLL文件(确保文件操作权限)
ReplacePluginDlls();
// 加载v1.1插件
LoadPlugins();
// 执行更新后的逻辑
ExecutePluginCommands();

内容的提问来源于stack exchange,提问作者Tyler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 16:37:34