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

.NET Core MVC跨程序集视图加载异常及解决方案咨询

ASP.NET Core MVC多模块视图定位问题解决

问题背景

我有一个ASP.NET Core MVC Web应用,包含多个独立程序集(模块),每个模块对应专属URL前缀。我通过module路由参数为每个模块添加URL前缀,现有AccessControl和TimeAttendance两个模块,它们都包含同名的AreaController:

AccessControl模块控制器

namespace Modules.AccessControl.UI.Controllers
{
    [Route("[module]/[controller]")]
    public class AreaController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return View();
        }
    }
}

TimeAttendance模块控制器

namespace Modules.TimeAttendance.UI.Controllers
{
    [Route("[module]/[controller]")]
    public class AreaController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return View();
        }
    }
}

两个模块各自拥有Views/Area/Index.cshtml视图文件,但访问https://localhost:44375/TimeAttendance/Area时,虽然能正确执行TimeAttendance模块的Index动作,却错误加载AccessControl模块的视图。

已知信息:

  • 手动指定视图完整路径可以解决问题,但希望通过配置实现自动匹配
  • 因为AccessControl是第一个加载的程序集,视图引擎会优先匹配它的视图文件

需求:让return View()默认根据当前控制器所在程序集定位视图,类似Swagger中通过程序集名称区分资源的方式。


尝试过程中的问题

我尝试通过自定义IViewLocationExpander修改视图搜索路径,但出现视图未找到错误,直接指定/Modules.TimeAttendance.UI/Views/Area/Index.cshtml路径也无效。

已实现的路由代码

public class Module(string routePrefix, IModuleStartup startup)
{
    /// <summary>
    /// 获取模块中所有控制器和端点的路由前缀
    /// </summary>
    public string RoutePrefix { get; } = routePrefix;

    /// <summary>
    /// 获取模块的启动类
    /// </summary>
    public IModuleStartup Startup { get; } = startup;

    /// <summary>
    /// 获取模块的程序集
    /// </summary>
    public Assembly Assembly => Startup.GetType().Assembly;
}

public class ModuleRoutingConvention(IEnumerable<Module> modules) : IActionModelConvention
{
    private readonly IEnumerable<Module> _modules = modules;

    public void Apply(ActionModel action)
    {
        var module = _modules.FirstOrDefault(m => action.Controller.ControllerType.Assembly.FullName?.StartsWith($"Modules.{m.RoutePrefix}") ?? false);
        if (module == null)
        {
            return;
        }

        action.RouteValues.Add("module", module.RoutePrefix);
    }
}

public class ModuleRoutingMvcOptionsPostConfigure : IPostConfigureOptions<MvcOptions>
{
    private readonly IEnumerable<Module> _modules;

    public ModuleRoutingMvcOptionsPostConfigure(IEnumerable<Module> modules)
    {
        _modules = modules;
    }

    public void PostConfigure(string? name, MvcOptions options)
    {
        options.Conventions.Add(new ModuleRoutingConvention(_modules));
    }
}

/// <summary>
/// 添加模块的扩展方法
/// </summary>
public static class ModuleServiceCollection
{
    /// <summary>
    /// 添加模块
    /// </summary>
    /// <param name="services">服务集合</param>
    /// <param name="routePrefix">模块的路由前缀</param>
    /// <typeparam name="TStartup">模块启动类的类型</typeparam>
    /// <returns>服务集合</returns>
    public static IServiceCollection AddModule<TStartup>(this IServiceCollection services, string routePrefix, IConfiguration configuration)
        where TStartup : IModuleStartup, new()
    {
        // 注册程序集到MVC,以便找到模块中的控制器
        services.AddControllers().ConfigureApplicationPartManager(manager =>
            manager.ApplicationParts.Add(new AssemblyPart(typeof(TStartup).Assembly)));

        var startup = new TStartup();
        startup.ConfigureServices(services, configuration);

        services.AddSingleton(new Module(routePrefix, startup));

        return services;
    }
}

自定义IViewLocationExpander代码(初始版本)

public class ModuleViewLocationMapper : IViewLocationExpander
{
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        List<string> locations = new List<string>(viewLocations);
        var controllerActionDescriptor = (ControllerActionDescriptor)context.ActionContext.ActionDescriptor;
        if (controllerActionDescriptor != null)
            locations.Insert(0, string.Concat("~/", controllerActionDescriptor.ControllerTypeInfo.Assembly.GetName().Name, "/Views/{1}/{0}", RazorViewEngine.ViewExtension));
        locations.RemoveAt(1);
        return locations;
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        
    }
}

解决方案

要解决这个问题,需要让视图引擎优先搜索当前控制器所在程序集的嵌入式视图,具体步骤如下:

1. 将模块视图设置为嵌入式资源

对于每个模块类库中的视图文件(如Index.cshtml),右键选择属性,将生成操作改为嵌入式资源。

2. 修正自定义IViewLocationExpander

初始版本的问题在于未正确处理嵌入式资源路径,且缺少缓存键导致视图路径被错误缓存。修正后的代码:

public class ModuleViewLocationExpander : IViewLocationExpander
{
    private const string AssemblyKey = "module-assembly";

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        if (context.Values.TryGetValue(AssemblyKey, out var assemblyName) && !string.IsNullOrEmpty(assemblyName))
        {
            // 优先搜索当前模块的视图路径
            var moduleViewPaths = new[]
            {
                $"/{assemblyName}/Views/{{1}}/{{0}}{RazorViewEngine.ViewExtension}",
                $"/{assemblyName}/Views/Shared/{{0}}{RazorViewEngine.ViewExtension}"
            };
            // 先返回模块专属路径,再返回默认路径
            return moduleViewPaths.Concat(viewLocations);
        }
        return viewLocations;
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        var descriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
        if (descriptor != null)
        {
            // 存储控制器所在程序集名称,用于缓存和路径生成
            context.Values[AssemblyKey] = descriptor.ControllerTypeInfo.Assembly.GetName().Name;
        }
    }
}

3. 注册嵌入式视图文件提供程序

在每个模块的IModuleStartup实现中,注册嵌入式视图的文件提供程序,让视图引擎能找到程序集内的视图资源:

public class TimeAttendanceStartup : IModuleStartup
{
    public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<RazorViewEngineOptions>(options =>
        {
            options.FileProviders.Add(new EmbeddedFileProvider(
                typeof(TimeAttendanceStartup).Assembly,
                $"{typeof(TimeAttendanceStartup).Namespace}.Views"));
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 按需实现中间件配置
    }
}

4. 注册自定义视图定位扩展器

在主项目的Program.cs中注册自定义的IViewLocationExpander:

builder.Services.Configure<RazorViewEngineOptions>(options =>
{
    options.ViewLocationExpanders.Add(new ModuleViewLocationExpander());
});

完成以上配置后,return View()会自动优先搜索当前控制器所在模块程序集的视图,彻底解决跨模块视图加载错误的问题。


内容的提问来源于stack exchange,提问作者jstuardo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:34:55