在ASP.NET Core框架中,应用程序部件(ApplicationPart)承担着向运行时暴露控制器、视图组件以及标签助手等功能的职责。当系统需要在不重新编译主站点的前提下,从外部目录载入业务模块时,就必须借助ApplicationPartManager完成程序集的注册。动态加载的核心难点并不在于把DLL读入内存,而在于如何让新载入的类型被路由系统识别,同时避免与已有程序集产生版本冲突。

应用程序部件与AssemblyLoadContext的基础原理
ApplicationPart本质上是对一个Assembly的包装,框架通过它来扫描其中的Controller、ViewComponent等类型。在传统的静态引用中,项目文件里写明ProjectReference,编译后所有类型都在默认的加载上下文内。而动态加载要求我们把插件放在诸如Plugins文件夹中,宿主启动后再去读取这些文件。此时如果直接使用Assembly.LoadFile,会将其载入默认上下文,可能导致不同插件携带的相同依赖库互相覆盖。
为解决隔离问题,.NET提供了AssemblyLoadContext(简称ALC)。我们可以创建独立的ALC实例,并在其中加载插件程序集,使其依赖与主干隔离。但要注意,ALC本身不会自动把程序集变成ApplicationPart,必须手动构造AssemblyPart并添加到ApplicationPartManager。下面代码展示了一个简单的自定义加载器,它从磁盘路径读取DLL并返回对应的部件集合。
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
public class PluginLoader
{
public static List<ApplicationPart> LoadFromFolder(string folder)
{
var parts = new List<ApplicationPart>();
var alc = new AssemblyLoadContext("PluginContext", isCollectible: true);
foreach (var dll in Directory.GetFiles(folder, "*.dll"))
{
using var fs = new FileStream(dll, FileMode.Open, FileAccess.Read);
var asm = alc.LoadFromStream(fs);
parts.Add(new AssemblyPart(asm));
}
return parts;
}
}
上述代码使用了可回收的ALC(isCollectible设为true),这为后续卸载提供了可能。不过即便如此,若插件中注册了单例服务并且被根容器捕获,依然会造成实际上的内存滞留。因此动态加载不仅是技术问题,也涉及应用生命周期管理。理解部件与上下文的关系,是构建稳定插件体系的第一步。
在Startup或Program中注册动态部件
在ASP.NET Core 6之后的最小宿主模型里,配置MVC通常通过AddControllers或AddMvc方法。要在启动时加入动态部件,需要拿到ApplicationPartManager实例。一种常见做法是在配置服务时调用ConfigureApplicationPartManager,将前面加载的部件逐一添加进去。这样框架在构建控制器描述器时,就会把插件里的类型纳入路由表。
下面的示例演示了如何在Program.cs里完成注册。假设插件目录位于应用根下的Plugins,我们在添加MVC服务后立刻追加部件。需要注意的是,如果插件包含Razor视图,还要确保对应的Razor编译部件也被加入,否则运行时找不到cshtml预编译结果。对于纯API插件,仅AssemblyPart通常已足够。
var builder = WebApplication.CreateBuilder(args);
var pluginFolder = Path.Combine(AppContext.BaseDirectory, "Plugins");
var parts = PluginLoader.LoadFromFolder(pluginFolder);
builder.Services.AddControllers()
.ConfigureApplicationPartManager(apm =>
{
foreach (var part in parts)
{
apm.ApplicationParts.Add(part);
}
});
var app = builder.Build();
app.MapControllers();
app.Run();
这种注册方式在应用启动阶段一次性完成,适合插件固定、不频繁变动的场景。如果希望在运行中持续监听目录变化并热加载,就需要结合文件监视器与重新构建宿主,或者采用更高级的插件框架。但不论方案如何演变,向ApplicationPartManager添加部件这一动作始终是不可省略的环节。
另外,动态加入的部件中的控制器可能会与现有路由产生冲突,比如都使用相同的[Route]前缀。开发团队应当约定插件命名空间与路由规范,或在加载时做重复检查。通过apm.ApplicationParts可以遍历已注册部件,结合反射筛选类型,从而在加载期就抛出清晰的错误而不是等到请求时才爆炸。
依赖解析与卸载时机的实战考量
很多团队在初次尝试动态加载时,会遇到诸如FileLoadException或MissingMethodException,根源多半是依赖版本不一致。插件如果引用了Newtonsoft.Json的12.0.3,而宿主用的是13.0.1,在默认上下文中就会绑定失败。使用独立ALC后,插件可携带自己的依赖,但要求这些依赖DLL也放在插件目录并被同一ALC载入。否则运行时会去默认上下文找,依旧冲突。
卸载方面,可回收ALC必须没有任何被根引用的类型才能通过Unload成功。这意味着插件里不能把类型注册为静态字段、不能挂到主机的长期缓存。推荐做法是在插件中只提供控制器与轻量服务,并且服务生命周期控制在请求范围内。以下表格对比了两种加载策略的差异:
| 策略 | 隔离性 | 卸载难度 | 适用场景 |
|---|---|---|---|
| 默认上下文LoadFile | 差,易冲突 | 不可卸载 | 一次性工具 |
| 独立可回收ALC | 强,依赖隔离 | 需清理引用 | 插件化系统 |
从架构视角看,动态加载不仅是把DLL塞进框架,更是在设计一个边界清晰的模块协议。插件应当通过接口与主程序通信,而不是直接依赖宿主内部类型。这样即使将来宿主升级,只要接口契约不变,插件无需重新编译。结合ApplicationPart的灵活注册,团队可以逐步把单体应用拆成可独立部署的功能包,提升交付效率。
最后提醒,在Linux等区分大小写的文件系统上,插件DLL路径与依赖名必须严格匹配。曾经有案例因为开发机Windows不区分大小写,而生产环境加载失败,耗费大量排查时间。把动态加载逻辑写成单元测试,在CI中模拟临时目录载入,能提前暴露这类环境差异问题。
ASP.NET_CoreApplicationPart动态加载修改时间:2026-08-18 17:36:43