欢迎来到老周的水文演播中心。

咱们都知道,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 控制器的模型绑定(宏观篇)的更多相关文章

  1. ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门

    一.前言 1.本教程主要内容 ASP.NET Core MVC控制器简介 ASP.NET Core MVC控制器操作简介 ASP.NET Core MVC控制器操作简介返回类型简介 ASP.NET C ...

  2. ASP.NET Core 入门笔记5,ASP.NET Core MVC控制器入门

    摘抄自https://www.cnblogs.com/ken-io/p/aspnet-core-tutorial-mvc-controller-action.html 一.前言 1.本教程主要内容 A ...

  3. 你想要的都在这里,ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

  4. ASP.NET Core MVC四种枚举绑定方式

    前言 本节我们来讲讲在ASP.NET Core MVC又为我们提供了哪些方便,之前我们探讨过在ASP.NET MVC中下拉框绑定方式,这节我们来再来重点看看枚举绑定的方式,充分实现你所能想到的场景,满 ...

  5. ASP.NET Core MVC 控制器创建与依赖注入

    本文翻译自<Controller activation and dependency injection in ASP.NET Core MVC>,由于水平有限,故无法保证翻译完全准确,欢 ...

  6. 扒一扒asp.net core mvc控制器的寻找流程

    不太会排版,大家将就看吧. asp.net core mvc和asp.net mvc中都有一个比较有意思的而又被大家容易忽略的功能,控制器可以写在非Web程序集中,比如Web程序集:"MyW ...

  7. 使用Asp.Net Core MVC 开发项目实践[第五篇:缓存的使用]

    项目中我们常常会碰到一些数据,需要高频率用到但是又不会频繁变动的这类,我们就可以使用缓存把这些数据缓存起来(比如说本项目的导航数据,帖子频道数据). 我们项目中常用到有Asp.Net Core 本身提 ...

  8. asp.net core MVC 控制器,接收参数,数据绑定

    1.参数 HttpRequest HttpRequest 是用户请求对象 QueryString Form Cookie Session Header 实例: public IActionResult ...

  9. Asp.Net Core MVC控制器和视图之间传值

    一.Core MVC中控制器和视图之间传值方式和Asp.Net中非常类似 1.弱类型数据:ViewData,ViewBag 2.强类型数据:@model 二.代码 实例  1.ViewData pub ...

随机推荐

  1. HOOK API(四) —— 进程防终止

    0x00        前言 这算是一个实战吧,做的一个应用需要实现进程的防终止保护,查了相关资料后决定用HOOK API的方式实现.起初学习HOOK API的起因是因为要实现对剪切板的监控,后来面对 ...

  2. 简单理解Zookeeper的Leader选举

    Leader选举是保证分布式数据一致性的关键所在.Leader选举分为Zookeeper集群初始化启动时选举和Zookeeper集群运行期间Leader重新选举两种情况.在讲解Leader选举前先了解 ...

  3. #pragma mark指令

    1.#pragma mark指令的使用 功能:简单来说就是对代码的分组,方便代码查找和导航用的 它们告诉Xcode编译器,要在编辑器窗格顶部的方法和函数弹出菜单中将代码分隔开.一些类(尤其是一些控制器 ...

  4. MSTP多生成树协议

    MSTP多生成树协议 目录 MSTP多生成树协议 1.MSTP(Multiple Spanning Tree Protocol)概述 2.STP.RSTP.PVST的应用缺陷 3.MSTP的主要特点 ...

  5. 06 前端之Bootstrap框架

    目录 前端之Bootstrap框架 一.简介 二.引入方式 本地引入(最完整的) CDN引入 三.布局容器 四.栅格系统 五.列偏移 六.表格与表单 6.1表格 6.2表单form 七.按钮 预定义样 ...

  6. PHP面试常考内容之Memcache和Redis(2)

    你好,是我琉忆.继周一(2019.2-18)发布的"PHP面试常考内容之Memcache和Redis(1)"后,这是第二篇,感谢你的支持和阅读.本周(2019.2-18至2-22) ...

  7. Note -「Min_25 筛」“你就说这素因子你要不要吧?你要不要?”

      赛上想写,Track Lost 了属于是. \(\mathscr{Intro}\)   Min_25 筛是用于求积性函数前缀和,同时顺带求出一些"有意思"的信息的筛法.   一 ...

  8. Solution -「SDOI 2017」「洛谷 P3784」遗忘的集合

    \(\mathcal{Description}\)   Link.   给定 \(\{f_1,f_2,\cdots,f_n\}\),素数 \(p\).求字典序最小的 \(\{a_1,a_2,\cdot ...

  9. .NET 云原生架构师训练营(权限系统 代码重构)--学习笔记

    目录 模块拆分 代码重构 模块拆分 代码重构 AuthenticationController PermissionController IAuthorizationMiddlewareResultH ...

  10. java-poi 批量导入excel数据

    1,首先,前端发送MultipartFile类型文件,后端接收 2,分别创建多个ImportParams对象(easypoi),对应工作蒲 注意:pom中 要有相对应的配置 <!-- easyp ...