1. 项目概述:这不是造轮子,是重建对Web开发底层的理解
“写自己的ASP.NET MVC框架(下)”——看到这个标题,很多人的第一反应是:又一个重复造轮子的教程?但如果你真动手做过上半部分,或者哪怕只是认真读完过MVC源码的Startup、Controller激活、Action执行链路,你就会明白:这根本不是为了替代官方框架,而是为了把那些被封装在 Microsoft.AspNetCore.Mvc.Core 里、被 AddControllersWithViews() 一键注入、被 [Route] 和 [HttpGet] 自动解析的魔法,一层层剥开,亲手缝合成一条可触摸、可调试、可打断点的完整请求响应流水线。我带过三届.NET后端实习生,几乎所有人第一次看到 IActionResult.ExecuteResultAsync() 被调用时都愣住——“原来return View()不是直接吐HTML,而是先走ResultExecutor,再进ViewEngine,最后才到Razor编译器?”这种顿悟,只有亲手实现过ViewResult、JsonResult、RedirectResult的执行逻辑才能获得。这个“下”字,意味着上半部分已完成了路由注册、控制器发现与激活;而本篇聚焦的是MVC最核心的契约层: 模型绑定、动作过滤、结果执行、视图渲染四大支柱 。它适合两类人:一是想跳出CRUD、真正吃透ASP.NET运行时机制的中高级开发者;二是正在设计内部低代码平台、需要深度定制Action执行生命周期的架构师。你不需要精通IL或CLR,但得熟悉委托、泛型约束、依赖注入容器的基本玩法——因为接下来每一行代码,都在和 IServiceProvider 、 MethodInfo 、 Expression 打交道。
2. 核心设计思路拆解:为什么必须从Filter Pipeline开始重构?
2.1 拒绝“先写Controller再补Filter”的倒置逻辑
市面上绝大多数手写MVC教程,包括微软官方文档的简化示例,都是先搭好Controller基类,再零散加几个 [Authorize] 或 [ValidateAntiForgeryToken] 装饰器,最后说一句“Filter支持AOP”。这完全违背了真实MVC的执行本质。我在为某金融客户做性能审计时发现,他们自研的轻量MVC框架因Filter执行顺序混乱,导致日志Filter在异常Filter之后才触发,关键错误堆栈全丢了。根源就在于设计时没把Filter Pipeline当作第一公民。真正的MVC请求流是: Route → Controller Activator → Filter Pipeline(Authorization→Resource→Action→Exception→Result)→ Action Execution → Result Execution → View Rendering 。其中Filter Pipeline不是可选插件,而是贯穿全程的骨架。所以本篇第一步,就是定义 IFilterMetadata 接口族,并强制所有Filter实现 IAsyncActionFilter 或 IAsyncResultFilter ——不是为了炫技,而是让 FilterDescriptor 能统一描述同步/异步行为,避免 Task.Run(() => { ... }) 这种反模式。
2.2 模型绑定不等于反序列化:从 IModelBinder 到 ModelBindingContext
很多人以为 [FromBody] 就是Newtonsoft.Json.DeserializeObject, [FromQuery] 就是 Request.Query.ToDictionary() 。错。真正的模型绑定是 上下文感知的、可中断的、支持验证的三阶段过程 :1)定位值提供者(Query/Route/Form/Body);2)类型转换(TypeConverter + 自定义Converter);3)验证(DataAnnotations + IValidatableObject)。我曾为医疗系统重写绑定器,要求 DateTime 字段必须带时区信息,否则拒绝绑定——这无法靠 JsonSerializerOptions.Converters.Add(new DateTimeOffsetConverter()) 解决,必须在 BindModelAsync 中手动校验 bindingContext.ValueProvider.GetValue("time").FirstValue 。因此本框架的 DefaultModelBinder 不继承 IModelBinder ,而是实现 IModelBinderProvider ,根据参数特性( [FromBody] / [FromForm] )动态返回不同 IModelBinder 实例。比如 FromBodyModelBinder 会检查Content-Type是否为 application/json ,否则抛出 UnsupportedMediaTypeResult ,而不是静默失败。
2.3 视图引擎的“双模态”设计:Razor不是唯一选项
官方MVC默认只认 .cshtml ,但企业级应用常需多模板引擎共存:报表导出用 Handlebars ,邮件通知用 Scriban ,甚至遗留系统要兼容 WebForms 的 .aspx 。硬编码Razor会导致 IViewEngine 扩展性归零。我们的方案是: ViewEngineCollection 继承 IViewEngine ,内部维护 List<IViewEngine> ,按优先级顺序调用 FindView 。当 ViewResult 执行时,先问Razor引擎:“有 /Views/Home/Index.cshtml 吗?”没有则问Handlebars:“有 /Views/Home/Index.hbs 吗?”。更关键的是 IView 接口设计——它不返回 string ,而是接受 ViewContext 并写入 HttpResponse.Body 。这样 RazorView 可调用 RazorPage.RenderAsync() , HandlebarsView 则用 template.RenderAsync(model) ,彻底解耦渲染逻辑。实测下来,切换引擎只需替换 services.AddSingleton<IViewEngine, HandlebarsViewEngine>() ,无需改任何Controller代码。
3. 核心模块实现详解:从Filter执行到View渲染的逐帧拆解
3.1 Filter Pipeline的异步状态机实现
Filter执行最易踩坑的是 同步Filter混入异步Pipeline 。比如 IActionFilter.OnActionExecuting 是同步方法,但若内部调用 await _cache.GetAsync() ,就会造成死锁。正确解法是定义统一的 FilterStage 枚举:
public enum FilterStage
{
Authorization,
Resource,
Action,
Exception,
Result
}
每个Filter实现 IFilterFactory ,由 FilterProvider 根据 FilterStage 分组。执行时, FilterInvoker 按Stage顺序遍历,对 IAsyncActionFilter 调用 OnActionExecutionAsync ,对 IActionFilter 则包装为 Task.FromResult(OnActionExecuting(context)) 。关键代码如下:
public async Task InvokeAsync(Ac


485

被折叠的 条评论
为什么被折叠?



