基于 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

相关推荐
lly2024064 小时前
C 标准库 - `<stdio.h>`
开发语言
沫璃染墨4 小时前
C++ string 从入门到精通:构造、迭代器、容量接口全解析
c语言·开发语言·c++
jwn9994 小时前
Laravel6.x核心特性全解析
开发语言·php·laravel
迷藏4944 小时前
**发散创新:基于Rust实现的开源合规权限管理框架设计与实践**在现代软件架构中,**权限控制(RBAC)** 已成为保障
java·开发语言·python·rust·开源
功德+n5 小时前
Linux下安装与配置Docker完整详细步骤
linux·运维·服务器·开发语言·docker·centos
明日清晨5 小时前
python扫码登录dy
开发语言·python
我是唐青枫5 小时前
C#.NET gRPC 深入解析:Proto 定义、流式调用与服务间通信取舍
开发语言·c#·.net
JJay.5 小时前
Kotlin 高阶函数学习指南
android·开发语言·kotlin
bazhange5 小时前
python如何像matlab一样使用向量化替代for循环
开发语言·python·matlab
jinanwuhuaguo5 小时前
截止到4月8日,OpenClaw 2026年4月更新深度解读剖析:从“能力回归”到“信任内建”的范式跃迁
android·开发语言·人工智能·深度学习·kotlin