大家好!

我最近在一个模块化框架项目中使用了 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 找到了一个平衡点:比模块更好的隔离,比微服务更轻量。

向社区请教的问题

  1. 你们在生产环境中使用过 ALC 吗? 遇到了哪些陷阱?

  2. 替代方案:你们会推荐使用 MediatR 配合独立程序集吗?权衡是什么?

  3. 测试策略:如何有效地对隔离上下文中的插件进行单元测试?

  4. 安全考虑:动态程序集加载有哪些我们应该注意的安全隐患?

  5. .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 控制器发现。很想听听你们在类似架构方面的经验!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐