You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

运行时替换MVC应用部件与Razor文件提供者的技术问题

解决ASP.NET MVC/WebAPI运行时插件替换后旧控制器仍响应的问题

看起来你已经走对了第一步——更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:17:17