【ASP.NET|【ASP.NET Core】MVC 控制器的模型绑定(宏观篇)
欢迎来到老周的水文演播中心。
咱们都知道,MVC的控制器也可以用来实现 Web API 的(它们原本就是一个玩意儿),区别嘛也就是一个有 View 而另一个没有 View。于是,在依赖注入的服务容器中,我们可以这样添加功能:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); //无 View builder.Services.AddControllersWithViews(); //有 View
如果你的控制器有用到视图的时候,就调用第二个的方法。它们的核心服务一样。
-----------------------------------------------------------------------------
当客户端历尽千辛万苦,跨越数不清的跃点,把请求提交到服务器后,MVC 运行时会分析请求的内容,从中还原出我们代码所需要的对象,通常是 Action 方法的参数。
要把客户端提交的数据填充到咱们所需要的对象中,得用到模型绑定。
我们先别管这概念抽象不抽象,举个例子,假设某控制器是有视图的,返回了一个页面,页面上有 form 元素(表单),可以让用户填写个人信息,然后提交(POST)给服务器,完成报名。
假设表示”会员“信息的是个叫 Member 的类。
public class Member { public int ID { get; set; }public string? Name { get; set; }public int Age { get; set; }public string? Phone { get; set; }public string? Description { get; set; } }
你如果注意看的话,你会发现:上面类中属性名与 HTML 页上
上面所举例的 form-data 数据是来源于 HTTP 请求的正文(body),其实,模型绑定的值还有其他来源:
1、正文(body),就是上文所列的;
2、URL 查询字符串,比如 http://dong_gua.com/action?name=小冬瓜&age=27&phone=13762634599&description=呵呵呵;
3、Header,即HTTP标头,比如在发送 HTTP 请求时,你可以在 Header 集合中加入 name: 小王, age: 25……;
4、路由参数,比如这样:
[Route("[controller]/[action]/{kid}")] public IActionResult GetLoaders(int kid) { …… }
要传点什么给 kid 参数,就访问
https://dabaojian.cn/home/getloaders/3561
数值 3561 就传递给 kid 参数了。那如果路由参数和参数的名字不同,但我还想传值给它怎么办?欲知答案,且听下回分解。
-------------------------------------------------------------------------------------------------------------
咱们现在讨论控制器,是不考虑它有没有 View 的,毕竟都是一个东西。于是,问题就来了——如果控制器类上应用了 ApiControllerAttribute 后会怎么样?用上这个特性和不用这个特性又有啥不一样?
多说无益,用例子来说明吧。假设我定义了这么个不长脸的控制器。
[Route("api/zzz")] public class HomeController : ControllerBase { [Route("send")] public IActionResult PostData(Person p) { if (p.ID == 0 || p.Name is null) return Content("WHF !"); // 未成功 string msg = $"姓名:{p.Name},编号:{p.ID}。\n提交成功"; return Content(msg); } }
Person 类定义:
public class Person { public int ID { get; set; }public string? Name { get; set; }public int Age { get; set; }public string? Phone { get; set; } }
虽然这个控制器类上设有用到 ApiControllerAttribute,但它是可以作为 Web API 来调用的,试试看。

文章图片
发送消息:
POST /api/zzz/send HTTP/1.1 Accept: */* Host: localhost:2022 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Type: multipart/form-data; boundary=--------------------------556592807377348094609386 Content-Length: 489 ----------------------------556592807377348094609386 Content-Disposition: form-data; name="id" 1001 ----------------------------556592807377348094609386 Content-Disposition: form-data; name="name" 小张 ----------------------------556592807377348094609386 Content-Disposition: form-data; name="age" 29 ----------------------------556592807377348094609386 Content-Disposition: form-data; name="phone" 18044332515 ----------------------------556592807377348094609386--
响应的消息:
HTTP/1.1 200 OK Content-Length: 47 Content-Type: text/plain; charset=utf-8 Date: Fri, 18 Mar 2022 03:17:28 GMT Server: Kestrel 姓名:小张,编号:1001。 提交成功
嗯,以 form-data 的格式提交是没问题的,试试 JSON 格式(Content-Type: application/json)。
/* 发送消息 */ POST /api/zzz/send HTTP/1.1 Content-Type: application/json Accept: */* Host: localhost:2022 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Length: 86 { "id": 45, "name": "小于", "age": 72, "phone": "19952558123" } /* 响应消息 */ HTTP/1.1 200 OK Content-Length: 5 Content-Type: text/plain; charset=utf-8 Date: Fri, 18 Mar 2022 03:22:19 GMT Server: Kestrel WHF !
咦?没提取到数据?
MVC 默认的模型绑定能找到 form 格式提交的,但 JSON 格式提交的,它没找到在哪。那咱们就告诉它数据从哪里来。
[Route("send")] public IActionResult PostData([FromBody] Person p) { …… }
然后,它就找到了。
POST /api/zzz/send HTTP/1.1 Content-Type: application/json User-Agent: PostmanRuntime/7.29.0 Accept: */* Host: localhost:2022 Accept-Encoding: gzip, deflate, br Connection: keep-alive Content-Length: 86 { "id": 45, "name": "小于", "age": 72, "phone": "19952558123" } HTTP/1.1 200 OK Content-Length: 45 Content-Type: text/plain; charset=utf-8 Date: Fri, 18 Mar 2022 03:28:54 GMT Server: Kestrel 姓名:小于,编号:45。 提交成功
要是你的控制器是专门作为 API 调用的,那么,你应该在控制器类的定义上应用特性 ApiControllerAttribute。
[Route("api/zzz"), ApiController] public class HomeController : ControllerBase { [Route("send")] public IActionResult PostData(Person p) { …… } }
这时候,参数 p 不用加 FromBody 特性,你用 JSON 格式提交,它会完美处理。一旦控制器成为 API 专用控制器后,客户端提交的数据它就交给 InputFormatter 去处理转化了。
前面老周写过自定义 OutputFormatter 的水文(就是有关返回数据格式的那两篇)。你想啊,有输出格式,肯定也有输入格式。同理地,默认是支持 JSON 格式,XML 得你手动开启,方法有老周以前写的水文中的方法一样,毕竟输入输出格式化是成对出现的。
A、针对 Web API ,一般使用 InputFormatter 来读取数据,完成模型绑定。前提是控制器类上要有 ApiControllerAttribute;
B、对于没有 ApiControllerAttribute 的控制器,就当作一般化处理,默认接收 form-data,也可以通过各种特性配置让它支持其他数据内容。
在控制器类上应用 ApiControllerAttribute 就是让运行时加入一些专门针对 API 调用的服务组件,让你的代码写起来更方便。比如直接就能接收 JSON 数据,返回 JSON 结果。
不过,控制器类若是应用了 ApiControllerAttribute 后,就会有限制条件(特殊要求):
在 Program.cs 文件中,你既可以用 app.MapControllers() 方法来添加终结点处理的中间件,也可以用 app.MapControllerRoute() 方法来注册全局路由规则;但是,API 专用的控制器上必须加 Route 特性来指定路由规则,不能共用全局路由规则。不然运行后被报错。

文章图片
.NET Core 运行时是怎么知道的?先看看 ApiControllerAttribute 类的定义。
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata, IFilterMetadata { public ApiControllerAttribute() { // } }
别的不用管,关键点是它实现了一个诡异的接口:IApiBehaviorMetadata,这个接口派生自 IFilterMetadata 接口。对这个接口不要抱什么好奇心,里面啥也没有。它只不过是用来做”标记“的,标记你这个控制器是不是 Web API 特供。在 ApiBehaviorApplicationModelProvider 类中会进行验证。
private static bool IsApiController(ControllerModel controller) { if (controller.Attributes.OfType().Any()) { return true; }var controllerAssembly = controller.ControllerType.Assembly; var assemblyAttributes = controllerAssembly.GetCustomAttributes(); return assemblyAttributes.OfType ().Any(); }
正好,ApiControllerAttribute 类就是实现这个接口的。如果找到,表明这个控制器类是 API 特供,于是,下一步就要找控制器类和方法上有没有应用 Route 特性。
if (!IsAttributeRouted(actionModel.Controller.Selectors) && !IsAttributeRouted(actionModel.Selectors)) { // Require attribute routing with controllers annotated with ApiControllerAttribute var message = Resources.FormatApiController_AttributeRouteRequired( actionModel.DisplayName, nameof(ApiControllerAttribute)); throw new InvalidOperationException(message); }static bool IsAttributeRouted(IList selectorModel) { for (var i = 0; i < selectorModel.Count; i++) { if (selectorModel[i].AttributeRouteModel != null) { return true; } }return false; }
嗯,真相大白了。
今天就水到这里,下一篇咱们再聊聊模型绑定的微观层面,尤其是怎么去自定义。
推荐阅读
- 历史上的今天|【历史上的今天】3 月 18 日(香农发表划时代论文;微软发布 IE5;早期计算设备先驱出生)
- 【N32G457】基于RT-Thread和【N32G457 】的智能字符识别系统
- 蓝桥刷题冲冲!|【真题】第十一届蓝桥真题
- 蓝桥真题|【蓝桥真题五】带三百人训练了十天精选蓝桥真题,看看他们都练些什么(三门语言题解)
- Activiti工作流入门筑基圆满
- python|【小白向】蓝桥杯练习系统——基础练习部分python语句解析
- 蓝桥杯 3 奇妙的数字
- leetcode|算法入门之字符串(Python)【初级算法——字符串】【蓝桥杯练习】【力扣练习】
- 数据结构与算法|【蓝桥杯】 BASIC-16 分解质因数
- 2065(【例2.2】整数的和 1006:A+B问题)