【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;
}
嗯,真相大白了。
今天就水到这里,下一篇咱们再聊聊模型绑定的微观层面,尤其是怎么去自定义。
【ASP.NET Core】MVC 控制器的模型绑定(宏观篇)的更多相关文章
- ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门
一.前言 1.本教程主要内容 ASP.NET Core MVC控制器简介 ASP.NET Core MVC控制器操作简介 ASP.NET Core MVC控制器操作简介返回类型简介 ASP.NET C ...
- ASP.NET Core 入门笔记5,ASP.NET Core MVC控制器入门
摘抄自https://www.cnblogs.com/ken-io/p/aspnet-core-tutorial-mvc-controller-action.html 一.前言 1.本教程主要内容 A ...
- 你想要的都在这里,ASP.NET Core MVC四种枚举绑定方式
前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...
- ASP.NET Core MVC四种枚举绑定方式
前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...
- ASP.NET Core MVC 控制器创建与依赖注入
本文翻译自<Controller activation and dependency injection in ASP.NET Core MVC>,由于水平有限,故无法保证翻译完全准确,欢 ...
- 扒一扒asp.net core mvc控制器的寻找流程
不太会排版,大家将就看吧. asp.net core mvc和asp.net mvc中都有一个比较有意思的而又被大家容易忽略的功能,控制器可以写在非Web程序集中,比如Web程序集:"MyW ...
- 使用Asp.Net Core MVC 开发项目实践[第五篇:缓存的使用]
项目中我们常常会碰到一些数据,需要高频率用到但是又不会频繁变动的这类,我们就可以使用缓存把这些数据缓存起来(比如说本项目的导航数据,帖子频道数据). 我们项目中常用到有Asp.Net Core 本身提 ...
- asp.net core MVC 控制器,接收参数,数据绑定
1.参数 HttpRequest HttpRequest 是用户请求对象 QueryString Form Cookie Session Header 实例: public IActionResult ...
- Asp.Net Core MVC控制器和视图之间传值
一.Core MVC中控制器和视图之间传值方式和Asp.Net中非常类似 1.弱类型数据:ViewData,ViewBag 2.强类型数据:@model 二.代码 实例 1.ViewData pub ...
随机推荐
- HOOK API(四) —— 进程防终止
0x00 前言 这算是一个实战吧,做的一个应用需要实现进程的防终止保护,查了相关资料后决定用HOOK API的方式实现.起初学习HOOK API的起因是因为要实现对剪切板的监控,后来面对 ...
- 简单理解Zookeeper的Leader选举
Leader选举是保证分布式数据一致性的关键所在.Leader选举分为Zookeeper集群初始化启动时选举和Zookeeper集群运行期间Leader重新选举两种情况.在讲解Leader选举前先了解 ...
- #pragma mark指令
1.#pragma mark指令的使用 功能:简单来说就是对代码的分组,方便代码查找和导航用的 它们告诉Xcode编译器,要在编辑器窗格顶部的方法和函数弹出菜单中将代码分隔开.一些类(尤其是一些控制器 ...
- MSTP多生成树协议
MSTP多生成树协议 目录 MSTP多生成树协议 1.MSTP(Multiple Spanning Tree Protocol)概述 2.STP.RSTP.PVST的应用缺陷 3.MSTP的主要特点 ...
- 06 前端之Bootstrap框架
目录 前端之Bootstrap框架 一.简介 二.引入方式 本地引入(最完整的) CDN引入 三.布局容器 四.栅格系统 五.列偏移 六.表格与表单 6.1表格 6.2表单form 七.按钮 预定义样 ...
- PHP面试常考内容之Memcache和Redis(2)
你好,是我琉忆.继周一(2019.2-18)发布的"PHP面试常考内容之Memcache和Redis(1)"后,这是第二篇,感谢你的支持和阅读.本周(2019.2-18至2-22) ...
- Note -「Min_25 筛」“你就说这素因子你要不要吧?你要不要?”
赛上想写,Track Lost 了属于是. \(\mathscr{Intro}\) Min_25 筛是用于求积性函数前缀和,同时顺带求出一些"有意思"的信息的筛法. 一 ...
- Solution -「SDOI 2017」「洛谷 P3784」遗忘的集合
\(\mathcal{Description}\) Link. 给定 \(\{f_1,f_2,\cdots,f_n\}\),素数 \(p\).求字典序最小的 \(\{a_1,a_2,\cdot ...
- .NET 云原生架构师训练营(权限系统 代码重构)--学习笔记
目录 模块拆分 代码重构 模块拆分 代码重构 AuthenticationController PermissionController IAuthorizationMiddlewareResultH ...
- java-poi 批量导入excel数据
1,首先,前端发送MultipartFile类型文件,后端接收 2,分别创建多个ImportParams对象(easypoi),对应工作蒲 注意:pom中 要有相对应的配置 <!-- easyp ...