在现代 .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