

【ASP.NET Core】MVC 控制器的模型绑定(宏观篇) - 东邪独孤
source link: https://www.cnblogs.com/tcjiaan/p/16018580.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【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)给服务器,完成报名。
<form asp-action="PostData" asp-controller="Main"> <div class="line"> <div class="lhd"> <label for="name">姓名:</label> </div> <div class="rctl"> <input type="text" name="name" id="name" /> </div> </div> <div class="line"> <div class="lhd"> <label for="age">年龄:</label> </div> <div class="rctl"> <input type="number" name="age" id="age" max="120" min="10" /> </div> </div> <div class="line"> <div class="lhd"> <label for="phone">手机号码:</label> </div> <div class="rctl"> <input type="tel" name="phone" id="phone" /> </div> </div> <div class="line"> <div class="lhd"> <label for="desc">简介:</label> </div> <div class="rctl"> <input name="description" id ="desc" /> </div> </div> <div class="line"> <button type="submit">提交</button> </div> </form>
假设表示”会员“信息的是个叫 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>元素里面的字段名是对应的(不要在意大小写,ID 属性是自动生成的,所以不需要用户填)。
控制器中的某个 action 可能是这样的:
public IActionResult PostData(Member mb) { // 干点别的事…… // ID 随机 mb.ID = rand.Next(); if(!ModelState.IsValid) { return Content("伙计,你提交的数据不对劲啊"); } return Ok(mb); }
当你在浏览器中打开页面时,你看到的是这样的。
你填入个人信息后,HTTP 请求是用 form-data 的内容类型提交的。
name=%E5%B0%8F%E6%9D%8E&age=34&phone=19958240311&description=%E5%A5%BD%E5%81%9A%E6%87%92%E5%90%83&__RequestVerificationToken=CfDJ8LWOO6CmpapIpbgTQWfkRfjsIo5GgqvJaC2rqhwFEpA_gf8yWZ31sgsqzZg2BDpCdcKcrZ9zXpCcRqYdfMWwXsNuWFi6b1Yq69YP2SOmtOYlTBDNPRyTwYLzidJNCF_tGrOO0mNyNU59ovmUA4UYnBk
原文如下:
age: 34 description: "好做懒吃" id: 1500452404 name: "小李" phone: "19958240311"
运行时通过对提交的 form-data 进行分析,读出与 Member 类各属性名称对应的字段值,然后进行绑定,最终程序代码能得到一个带属性值 Member 实例。嗯,这就好像反序列化一样。
在 MVC 里面有一堆叫 ModelBinder 的东东,能够针对 HTTP 提交的请求,将值转化为 .NET 类型。ASP.NET Core 已为咱们内置了许多常用的 ModelBinder,包罗万象,应有尽有。所以,99.9625% 的情况下我们不需要自己编写 Binder。
这些 Binder 位于 Microsoft.AspNetCore.Mvc.ModelBinding.Binders 命名空间下。
如果没有特别指定,在模型绑定时会在 HTTP 请求中查找与类型属性名称相同的字段,比如上面举例中的<input>元素,它们的 name 分别为”id“、"name"、”age“等。
当然,form 字段名可以带前缀,例如上面那个 action 方法的定义。
public IActionResult PostData(Member mb);
也就是说,参数的名字叫”mb“,所以,在 <form> 里面,可以这样命名:
<form asp-action="PostData" asp-controller="Main"> <div class="line"> …… <div class="rctl"> <input type="text" name="mb.name" id="name" /> </div> </div> <div class="line"> …… <div class="rctl"> <input type="number" name="mb.age" id="age" max="120" min="10" /> </div> </div> <div class="line"> …… <div class="rctl"> <input type="tel" name="mb.phone" id="phone" /> </div> </div> <div class="line"> …… <div class="rctl"> <input name="mb.description" id ="desc" /> </div> </div> …… </form>
上面所举例的 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<IApiBehaviorMetadata>().Any()) { return true; } var controllerAssembly = controller.ControllerType.Assembly; var assemblyAttributes = controllerAssembly.GetCustomAttributes(); return assemblyAttributes.OfType<IApiBehaviorMetadata>().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> selectorModel) { for (var i = 0; i < selectorModel.Count; i++) { if (selectorModel[i].AttributeRouteModel != null) { return true; } } return false; }
嗯,真相大白了。
今天就水到这里,下一篇咱们再聊聊模型绑定的微观层面,尤其是怎么去自定义。
Recommend
-
86
“导航属性”是实体框架用得算是比较频繁的概念。 首先,它是类型成员,其次,他是属性,这不是 F 话,而是明确它的本质。那么,什么场景下会用到导航属性呢?重点就落在“导航”一词上了,当实体 A 需要引用
-
68
有朋友说老周近来博客更新较慢,确实有些慢,因为有些 bug 要研究,另外就是老周把部分内容转到直播上面,所以写博客的内容减少了一点。 老周觉得,视频直播可能会好一些,虽然我的水平一般,不过直播时,老周
-
8
【ASP.NET Core】绑定到 CancellationToken 对象 负责管理 HTTP 请求上下文的 HttpConte...
-
8
【ASP.NET Core】URL重写 今天老周和大伙伴们聊聊有关...
-
9
【ASP.NET Core】模型绑定:重命名绑定字段 前面在写模...
-
11
【ASP.NET Core】自定义的配置源 本文的主题是简单说说...
-
9
【ASP.NET Core】选项模式的相关接口 在 .NET 中,配置...
-
8
【ASP.NET Core】选项类的依赖注入 咱们继续上一个话题...
-
6
【ASP.NET Core】在Blazor中获取 HTTP 上下文信息 今天...
-
5
【ASP.NET Core】MVC控制器的各种自定义:IActionHttpMethodProvider 接口 ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK