运行时替换MVC应用部件与Razor文件提供者的技术问题
看起来你已经走对了第一步——更新PartManager里的部件,但MVC框架内部还有不少缓存和依赖环节没处理,这就是为什么旧控制器还在生效。我来帮你拆解几个关键问题点和解决方案:
1. MVC控制器类型缓存没清除
默认情况下,MVC会把控制器类型缓存到ControllerTypeCache里,即便你更新了PartManager,这个缓存依然会返回旧的控制器类型。你需要手动清除这个缓存,因为它的Clear方法是非公开的,得用反射来调用:
// 从服务容器获取ControllerTypeCache实例 var controllerTypeCache = _serviceProvider.GetService<ControllerTypeCache>(); // 获取非公开的Clear方法并调用 var clearMethod = typeof(ControllerTypeCache).GetMethod("Clear", BindingFlags.Instance | BindingFlags.NonPublic); clearMethod?.Invoke(controllerTypeCache, null);
更彻底的方式是自定义ControllerFactory,每次请求都从PartManager重新获取最新的控制器类型,绕过默认缓存:
public class DynamicControllerFactory : DefaultControllerFactory { private readonly IServiceProvider _serviceProvider; public DynamicControllerFactory(IServiceProvider serviceProvider) : base(serviceProvider) { _serviceProvider = serviceProvider; } protected override Type GetControllerType(RequestContext requestContext, string controllerName) { var partManager = _serviceProvider.GetService<ApplicationPartManager>(); var controllerFeature = new ControllerFeature(); // 让所有ControllerFeatureProvider重新扫描部件生成控制器列表 foreach (var provider in partManager.FeatureProviders.OfType<ControllerFeatureProvider>()) { provider.PopulateFeature(partManager.ApplicationParts, controllerFeature); } // 匹配请求的控制器名称 return controllerFeature.Controllers .FirstOrDefault(t => t.Name.Equals($"{controllerName}Controller", StringComparison.OrdinalIgnoreCase)) ?? base.GetControllerType(requestContext, controllerName); } }
然后在Startup注册这个工厂:
services.AddControllersWithViews() .AddControllerFactory<DynamicControllerFactory>();
2. 视图引擎缓存未刷新
如果你的插件包含嵌入式视图,Razor视图引擎会缓存视图的查找结果。要清除这个缓存,同样需要反射操作内部的缓存字段,或者自定义视图引擎:
public class DynamicRazorViewEngine : RazorViewEngine { public DynamicRazorViewEngine(IRazorPageFactoryProvider pageFactoryProvider, IRazorPageActivator pageActivator, HtmlEncoder htmlEncoder, IOptions<RazorViewEngineOptions> optionsAccessor) : base(pageFactoryProvider, pageActivator, htmlEncoder, optionsAccessor) { } public void ClearViewCache() { // 清除视图位置缓存 var cacheField = typeof(RazorViewEngine).GetField("_viewLocationCache", BindingFlags.Instance | BindingFlags.NonPublic); if (cacheField?.GetValue(this) is ConcurrentDictionary<ViewLocationCacheKey, string> cache) { cache.Clear(); } } }
替换插件后,调用ClearViewCache()就能刷新视图缓存了。
3. 程序集加载隔离(关键)
如果你的插件程序集是直接加载到默认AssemblyLoadContext里的,那么即便你移除了AssemblyPart,旧程序集依然会留在内存中,控制器类型还是旧的。.NET Core之后可以用自定义AssemblyLoadContext来隔离插件,这样替换时可以卸载整个上下文,彻底清理旧程序集:
自定义插件加载上下文
public class PluginLoadContext : AssemblyLoadContext { private readonly AssemblyDependencyResolver _resolver; public PluginLoadContext(string pluginPath) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly Load(AssemblyName assemblyName) { var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero; } }
加载和替换插件的流程
// 字段保存旧的加载上下文和部件 private PluginLoadContext _oldPluginAlc; private AssemblyPart _oldPluginPart; public void ReplacePlugin(string newPluginPath) { // 1. 移除旧部件 if (_oldPluginPart != null) { _partManager.ApplicationParts.Remove(_oldPluginPart); } // 2. 卸载旧的加载上下文(释放旧程序集) _oldPluginAlc?.Unload(); // 3. 加载新插件 var newAlc = new PluginLoadContext(newPluginPath); var newAssembly = newAlc.LoadFromAssemblyPath(newPluginPath); var newPart = new AssemblyPart(newAssembly); // 4. 添加新部件到PartManager _partManager.ApplicationParts.Add(newPart); // 5. 清除各种缓存 ClearControllerCache(); _dynamicRazorViewEngine.ClearViewCache(); // 更新引用 _oldPluginAlc = newAlc; _oldPluginPart = newPart; }
4. 刷新路由(如果插件包含自定义路由)
如果你的插件注册了自定义路由,替换后需要重新注册并刷新路由表。可以先移除旧插件的路由(最好给插件路由加个唯一前缀或标记),再添加新的路由,然后调用:
RouteTable.Routes.Clear(); // 重新注册所有基础路由和新插件的路由 app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); // 添加新插件的路由 endpoints.MapControllers(); });
总结
运行时插件替换需要覆盖这几个环节:
- 用
AssemblyLoadContext隔离插件程序集,确保旧程序集能被卸载 - 更新
PartManager的部件集合 - 清除控制器类型缓存或自定义控制器工厂绕过缓存
- 刷新视图引擎缓存
- 重新注册并刷新路由(如果有自定义路由)
按这个流程操作后,新插件的控制器和视图应该就能正常响应了。
内容的提问来源于stack exchange,提问作者Andreas Zita

