使用 AssemblyLoadContext 实现 .NET 真正的插件隔离 - 实践经验与架构决策分享
大家好!
我最近在一个模块化框架项目中使用了 AssemblyLoadContext (ALC) 来实现 .NET 中的运行时插件隔离。想和大家分享一些架构决策、遇到的挑战,并听听大家的想法。
我们要解决的问题
传统的 .NET 插件/模块系统通常将程序集加载到默认上下文中,这会导致:
- 当不同模块依赖同一库的不同版本时出现 DLL 版本冲突
- 没有真正的隔离 - 一个模块的崩溃可能导致整个应用宕机
- 难以在不重启应用的情况下热插拔插件
我们的方案:基于 AssemblyLoadContext 的隔离
我们实现了一个插件系统,每个插件运行在独立的 AssemblyLoadContext 中。以下是我们的一些经验:
关键设计决策
1. 共享契约程序集
// 所有插件只引用 Fastdotnet.Plugin.Contracts
// 这个程序集加载在默认上下文中,在所有插件间共享
public interface IPlugin
{
string PluginId { get; }
Task InitializeAsync(IServiceProvider serviceProvider);
Task StartAsync();
Task StopAsync();
}
这确保了宿主和插件之间的类型兼容性,同时保持依赖最小化。
2. 带依赖解析的自定义 ALC
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
// 首先尝试从插件自己的依赖中加载
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
// 回退到默认上下文加载共享程序集
return Default.LoadFromAssemblyName(assemblyName);
}
}
3. 每个插件独立的 DI 容器 每个插件拥有自己的 Autofac LifetimeScope,防止服务注册冲突:
var scope = containerBuilder.Build();
var plugin = scope.Resolve<IPlugin>();
我们遇到的挑战
挑战 1:跨上下文的类型兼容性 即使两个上下文加载了相同的契约程序集,默认上下文中的 typeof(IPlugin) 与插件上下文中的 typeof(IPlugin) 不相等。
解决方案:始终通过方法参数传递接口/类型,而不是直接比较它们。谨慎使用反射。
挑战 2:静态状态泄漏 共享库中的静态变量仍然在所有上下文间共享。
解决方案:避免在共享契约中使用静态状态。所有内容都使用依赖注入。
挑战 3:内存管理 可卸载的 ALC 需要仔细的资源清理。
解决方案:实现正确的 Dispose 模式,确保没有引用泄漏回默认上下文:
public async Task UnloadAsync()
{
await StopAsync();
// 清理事件订阅
// 处置所有作用域服务
_loadContext.Unload();
}
挑战 4:ASP.NET Core 中的控制器发现 隔离插件中的控制器不会被 MVC 自动发现。
解决方案:使用 ApplicationPartManager 手动注册插件控制器:
var partFactory = ApplicationPartFactory.GetApplicationPartFactory(pluginAssembly);
foreach (var part in partFactory.GetApplicationParts(pluginAssembly))
{
manager.ApplicationParts.Add(part);
}
性能考虑
- 初始加载:每个插件约 50-100ms(启动时可接受)
- 运行时开销:加载后几乎可以忽略
- 内存:每个插件增加约 5-10MB 开销(主要是 JIT 编译的代码)
- 隔离收益:对于多租户 SaaS 场景来说值得
与其他方案的对比
| 方案 | 隔离性 | 热插拔 | 复杂度 |
|---|---|---|---|
| 默认上下文 | ❌ 无 | ❌ 不支持 | 低 |
| AppDomain (.NET Framework) | ✅ 良好 | ⚠️ 有限 | 高 |
| AssemblyLoadContext | ✅ 优秀 | ✅ 支持 | 中等 |
| 微服务 | ✅ 完全 | ✅ 支持 | 非常高 |
ALC 找到了一个平衡点:比模块更好的隔离,比微服务更轻量。
向社区请教的问题
-
你们在生产环境中使用过 ALC 吗? 遇到了哪些陷阱?
-
替代方案:你们会推荐使用 MediatR 配合独立程序集吗?权衡是什么?
-
测试策略:如何有效地对隔离上下文中的插件进行单元测试?
-
安全考虑:动态程序集加载有哪些我们应该注意的安全隐患?
-
.NET 10 改进:最近的 .NET 版本中有哪些新特性让 ALC 更容易使用?
代码仓库
如果你有兴趣查看完整实现,这里是开源地址: https://github.com/CN-GodHei/fastdotnet
特别关注:
Fastdotnet.Core/Plugin/- 核心插件接口Fastdotnet.WebApi/Infrastructure/PluginLoader.cs- ALC 实现backend/Plugins/PluginA/- 示例插件
我特别希望能听到以下方面的反馈:
- 这种方法在大型应用中是否具有良好的可扩展性
- 插件间通信的更好模式
- 我们可能遗漏的潜在内存泄漏场景
感谢阅读!期待你们的见解。🙏
TL;DR: 使用 AssemblyLoadContext 实现了 .NET 中的插件系统以实现真正的隔离。每个插件有自己的依赖上下文,防止 DLL 冲突。主要挑战:跨上下文的类型兼容性、内存管理和 MVC 控制器发现。很想听听你们在类似架构方面的经验!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)