基于 AssemblyLoadContext 的 .NET 插件化架构设计与实现

在现代 .NET 应用(如 SaaS、IDE、游戏服务端等)中,"插件化"是实现扩展能力的核心手段。而从 .NET Core 开始,官方推荐使用 AssemblyLoadContext 替代旧的 AppDomain 来实现隔离与动态加载。

本文将从 原理 → 架构 → 实现 → 进阶优化,带你一步一步构建一个可用于生产的插件系统。


一、为什么要用 AssemblyLoadContext?

在 .NET Framework 中,我们用 AppDomain 做隔离:

复制代码
AppDomain.CreateDomain("PluginDomain");

但在 .NET Core / .NET 5+ 中:

AppDomain 已不再支持隔离加载

官方推荐:AssemblyLoadContext


AssemblyLoadContext 的核心能力

  • ✔ 动态加载 DLL

  • ✔ 支持卸载(Collectible)

  • ✔ 解决依赖冲突(不同插件不同版本)

  • ✔ 隔离类型上下文


二、插件架构设计

一个标准插件系统通常分为 4 层:

复制代码
Host(主程序)
│
├── Plugin.Abstractions(接口层)
│
├── Plugins(插件目录)
│     ├── PluginA.dll
│     └── PluginB.dll
│
└── PluginLoader(加载器)

关键设计原则

1️.接口共享(必须)

插件和主程序必须共享接口定义:

复制代码
public interface IPlugin
{
    string Name { get; }
    void Execute();
}

这个接口必须在 独立的 DLL(Abstractions)


2️.插件隔离加载

每个插件使用独立的 AssemblyLoadContext


3️. 支持卸载(重点)

使用:

复制代码
isCollectible: true

三、核心实现


1️.定义插件接口(共享库)

复制代码
// Plugin.Abstractions.dll
public interface IPlugin
{
    string Name { get; }
    void Execute();
}

2️.编写插件

复制代码
// PluginA.dll
public class PluginA : IPlugin
{
    public string Name => "Plugin A";

    public void Execute()
    {
        Console.WriteLine("Hello from Plugin A");
    }
}

3️.自定义 AssemblyLoadContext

这是整个系统的核心 👇

复制代码
using System.Reflection;
using System.Runtime.Loader;

public class PluginLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;

    public PluginLoadContext(string pluginPath)
        : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        string path = _resolver.ResolveAssemblyToPath(assemblyName);

        if (path != null)
        {
            return LoadFromAssemblyPath(path);
        }

        return null;
    }
}

关键点解释

组件 作用
AssemblyDependencyResolver 自动解析依赖
Load() 控制 DLL 如何加载
isCollectible 是否支持卸载

4️.插件加载器实现

复制代码
using System.Reflection;

public class PluginLoader
{
    public static IEnumerable<IPlugin> LoadPlugins(string folder)
    {
        foreach (var dll in Directory.GetFiles(folder, "*.dll"))
        {
            var context = new PluginLoadContext(dll);

            var assembly = context.LoadFromAssemblyPath(dll);

            foreach (var type in assembly.GetTypes())
            {
                if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract)
                {
                    var plugin = (IPlugin)Activator.CreateInstance(type);
                    yield return plugin;
                }
            }
        }
    }
}

5️.主程序调用

复制代码
class Program
{
    static void Main()
    {
        var plugins = PluginLoader.LoadPlugins("./Plugins");

        foreach (var plugin in plugins)
        {
            Console.WriteLine($"Loaded: {plugin.Name}");
            plugin.Execute();
        }
    }
}

四、支持插件卸载(高级)

注意:卸载不是立即发生,而是依赖 GC


改造 Loader

复制代码
public class PluginHandle
{
    public PluginLoadContext Context { get; set; }
    public WeakReference WeakRef { get; set; }
}

加载并可卸载

复制代码
public static PluginHandle Load(string path)
{
    var context = new PluginLoadContext(path);

    var assembly = context.LoadFromAssemblyPath(path);

    return new PluginHandle
    {
        Context = context,
        WeakRef = new WeakReference(context, trackResurrection: true)
    };
}

卸载

复制代码
handle.Context.Unload();

for (int i = 0; handle.WeakRef.IsAlive && i < 10; i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

五、解决依赖冲突问题(关键)

假设:

  • PluginA 使用 Newtonsoft.Json v12

  • PluginB 使用 Newtonsoft.Json v13

如果不隔离,会崩


AssemblyLoadContext 的优势

每个插件:

复制代码
new PluginLoadContext(pluginPath);

👉 每个插件独立依赖树 ✅


六、进阶优化设计


1. 插件元数据(推荐)

复制代码
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
    public string Version { get; set; }
}

2. 插件生命周期

复制代码
public interface IPlugin
{
    void OnLoad();
    void Execute();
    void OnUnload();
}

3. 插件隔离通信(重要)

不要直接传复杂对象,推荐:

  • DTO(共享模型)

  • 接口通信

  • 或 JSON / gRPC


4. 安全隔离(企业级)

AssemblyLoadContext ❌ 不等于安全沙箱

需要:

  • 进程隔离(推荐)

  • Docker

  • WASM(未来方向)


七、完整项目结构示例

复制代码
Solution
│
├── HostApp
├── Plugin.Abstractions
├── PluginLoader
│
└── Plugins
     ├── PluginA
     └── PluginB

八、常见坑总结(非常重要)

1. 类型不一致问题

复制代码
typeof(IPlugin) != pluginType.GetInterface(...)

原因:接口被加载两次

✔ 解决:接口必须来自同一个 DLL


2. 无法卸载

原因:

  • 静态变量引用

  • 线程未结束

  • 事件未解绑


3. 依赖丢失

✔ 必须使用:

复制代码
AssemblyDependencyResolver

九、适用场景

这个架构适用于:

  • IDE 插件系统(类似 VS / Rider)

  • 游戏 Mod 系统

  • SaaS 扩展模块

  • 工作流节点插件

  • 微内核架构(Microkernel)


十、总结

基于 AssemblyLoadContext 的插件架构具备:

动态加载

依赖隔离

可卸载

高扩展性

但同时需要注意:

不提供安全隔离

需要严格控制共享接口

卸载依赖 GC

相关推荐
关关长语2 小时前
HandyControl中Button图标展示多色路径
c#·.net·wpf·handycontrol
2501_930707782 小时前
使用C#代码获取PDF文件的页数
开发语言·pdf·c#
.select.2 小时前
虚函数和虚表
开发语言·c++·算法
王ASC2 小时前
Java不重启加载新的class文件
java·开发语言
乐观勇敢坚强的老彭2 小时前
c++信奥for循环强化03
开发语言·c++
咚为2 小时前
告别 lazy_static:深度解析 Rust OnceCell 的前世今生与实战
开发语言·后端·rust
全栈开发圈2 小时前
干货分享|R语言聚类分析1
开发语言·r语言
人工智能AI技术2 小时前
C# Runner + OpenClaw双实战:用.NET写原生AI Agent,告别Python依赖
人工智能·c#
Aawy1202 小时前
C++与Rust交互编程
开发语言·c++·算法