如何扩展AspNetCore MVC视图发现逻辑适配控制器继承的自定义视图查找需求?
AspNetCore MVC 自定义视图发现逻辑实现方案
完全可以实现该需求,核心是利用ASP.NET Core MVC内置的IViewLocationExpander扩展点,自定义视图查找的候选路径和优先级,完全匹配你提出的规则:
- 第一优先级:当前控制器同名视图目录
- 第二优先级:控制器自定义特性指定的路径
- 第三优先级:控制器继承链中所有基控制器对应的视图目录
- 第四优先级:控制器所在目录、命名空间推导的视图路径
- 最后兜底:MVC默认的视图查找路径
第一步:定义控制器自定义路径标注特性
用于给控制器类手动指定要优先查找的视图路径,支持多个特性叠加:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class CustomViewPathAttribute : Attribute { public string ViewPathTemplate { get; } public CustomViewPathAttribute(string viewPathTemplate) { ViewPathTemplate = viewPathTemplate; } }
第二步:实现自定义视图位置扩展器
核心逻辑是在视图查找阶段,按照规则生成所有候选路径,按优先级返回给视图引擎:
public class CustomViewLocationExpander : IViewLocationExpander { public void PopulateValues(ViewLocationExpanderContext context) { // 将当前执行的控制器类型存入上下文,避免视图缓存串用 if (context.ActionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor) { context.Values["controller_type"] = actionDescriptor.ControllerTypeInfo.FullName; } } public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> defaultViewLocations) { // 从上下文取出控制器类型,解析生成候选路径 if (!context.Values.TryGetValue("controller_type", out var controllerTypeName) || string.IsNullOrEmpty(controllerTypeName)) { return defaultViewLocations; } var controllerType = Type.GetType(controllerTypeName); if (controllerType == null) return defaultViewLocations; var candidateLocations = new List<string>(); // 1. 优先匹配当前控制器同名视图目录 var currentControllerName = controllerType.Name; if (currentControllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) { currentControllerName = currentControllerName.Replace("Controller", "", StringComparison.OrdinalIgnoreCase); } candidateLocations.Add($"/Views/{currentControllerName}/{{0}}.cshtml"); // 2. 匹配控制器自定义特性标注的路径 var customPathAttributes = controllerType.GetCustomAttributes<CustomViewPathAttribute>(); foreach (var attr in customPathAttributes) { candidateLocations.Add(attr.ViewPathTemplate); } // 3. 遍历继承链,依次加入基控制器对应的视图目录 var baseType = controllerType.BaseType; while (baseType != null && baseType.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) { var baseControllerName = baseType.Name.Replace("Controller", "", StringComparison.OrdinalIgnoreCase); candidateLocations.Add($"/Views/{baseControllerName}/{{0}}.cshtml"); baseType = baseType.BaseType; } // 4. 基于控制器所在目录、命名空间推导视图路径 var controllerNamespace = controllerType.Namespace; if (controllerNamespace.StartsWith("App.Controllers.", StringComparison.OrdinalIgnoreCase)) { var relativeNamespace = controllerNamespace.Replace("App.Controllers.", "", StringComparison.OrdinalIgnoreCase); candidateLocations.Add($"/Views/{relativeNamespace}/{{0}}.cshtml"); } // 5. 加入默认路径作为兜底,去重避免重复查找 candidateLocations.AddRange(defaultViewLocations); return candidateLocations.Distinct(); } }
第三步:注册自定义扩展器到MVC管道
在项目启动配置中,将扩展器加入视图引擎的扩展列表:
// Program.cs 中配置MVC服务的位置 builder.Services.AddControllersWithViews(opt => { opt.ViewLocationExpanders.Add(new CustomViewLocationExpander()); });
针对示例控制器的查找效果
针对你给出的两个同命名空间下的控制器结构,当UserAccountExtController执行继承自基类的Action1、返回View()时,视图查找顺序为:
- 查找
/Views/UserAccountExt/Action1.cshtml - 若
UserAccountExtController类上标注了[CustomViewPath("/Views/xxx/{0}.cshtml")],则依次查找对应自定义路径 - 查找基类对应的
/Views/UserAccount/Action1.cshtml - 基于命名空间
App.Controllers.UserAccount推导,查找/Views/UserAccount/Action1.cshtml - 最后兜底查找默认的
/Views/Shared/Action1.cshtml等默认路径
实现注意事项
- 不要直接从路由值中取
controller参数生成路径,派生控制器复用基类Action时,路由携带的控制器名是派生类的短名,必须从ControllerActionDescriptor中获取真实的控制器类型,才能正确解析继承链。 - 必须在
PopulateValues方法中将控制器类型存入context.Values,否则ASP.NET Core的视图缓存机制会复用第一次查找的路径,导致不同控制器的查找逻辑错乱。 - 如需支持Area区域,只需在路径模板中加入Area参数(格式为
/Areas/{2}/Views/...)即可,其余逻辑无需调整。
内容的提问来源于stack exchange,提问作者Failwyn
相关产品推荐
相关产品推荐

