前文说道了Action的激活,这里有个关键的操作就是Action参数的映射与模型绑定,这里即涉及到简单的string、int等类型,也包含Json等复杂类型,本文详细分享一下这一过程。(ASP.NET Core 系列目录

一、概述

  当客户端发出一个请求的时候,参数可能存在于URL中也可能是在请求的Body中,而参数类型也大不相同,可能是简单类型的参数,如字符串、整数或浮点数,也可能是复杂类型的参数,比如常见的Json、XML等,这些事怎么与目标Action的参数关联在一起并赋值的呢?

  故事依然是发生在通过路由确定了被请求的Action之后,invoker的创建与执行阶段(详见Action的执行)。

invoker的创建阶段,创建处理方法,并根据目标Action的actionDescriptor获取到它的所有参数,分析各个参数的类型确定对应参数的绑定方法,

invoker的执行阶段,调用处理方法,遍历参数逐一进行赋值。

为了方便描述,创建一个测试Action如下,它有两个参数,下文以此为例进行描述。:

  1. public JsonResult Test([FromBody]User user,string note = "FlyLolo")
  2. {
  3. return new JsonResult(user.Code + "|" + user.Name );
  4. }

二、准备阶段

1. 创建绑定方法

 当收到请求后,由路由系统确定了被访问的目标Action是我们定义的Test方法, 这时进入invoker的创建阶段,前文说过它有一个关键属性cacheEntry是由多个对象组装而成(发生在ControllerActionInvokerCache的GetCachedResult方法中),其中一个是propertyBinderFactory:

  1. var propertyBinderFactory = ControllerBinderDelegateProvider.CreateBinderDelegate(_parameterBinder,_modelBinderFactory,_modelMetadataProvider,actionDescriptor,_mvcOptions);

看一下CreateBinderDelegate这个方法:

  1. public static ControllerBinderDelegate CreateBinderDelegate(ParameterBinder parameterBinder,IModelBinderFactory modelBinderFactory,
    IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
  2. {
  3. //各种验证 略
  4.  
  5. var parameterBindingInfo = GetParameterBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor, mvcOptions);
  6. var propertyBindingInfo = GetPropertyBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor);
  7. if (parameterBindingInfo == null && propertyBindingInfo == null)
  8. {
  9. return null;
  10. }
  11. return Bind;
  12.  
  13. async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
  14. {
  15. //后文详细描述
  16. }
  17. }

前文说过,invoker的创建阶段就是创建一些关键对象和一些用于执行的方法,而propertyBinderFactory 就是众多方法之中的一个,前文介绍它是一个用于参数绑定的Task,而没有详细说明,现在可以知道它被定义为一个名为Bind的Task,最终作为invoker的一部分等待被执行进行参数绑定。

2. 为每个参数匹配Binder

上面的CreateBinderDelegate方法创建了两个对象parameterBindingInfo 和propertyBindingInfo ,顾名思义,一个用于参数一个用于属性。看一下parameterBindingInfo 的创建:

  1. private static BinderItem[] GetParameterBindingInfo(IModelBinderFactory modelBinderFactory,IModelMetadataProvider modelMetadataProvider,ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
  2. {
  3. var parameters = actionDescriptor.Parameters;
  4. if (parameters.Count == )
  5. {
  6. return null;
  7. }
  8. var parameterBindingInfo = new BinderItem[parameters.Count];
  9. for (var i = ; i < parameters.Count; i++)
  10. {
  11. var parameter = parameters[i];
  12.           //略。。。
  13. var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
  14. {
  15. BindingInfo = parameter.BindingInfo,
  16. Metadata = metadata,
  17. CacheToken = parameter,
  18. });
  19.  
  20. parameterBindingInfo[i] = new BinderItem(binder, metadata);
  21. }
  22.  
  23. return parameterBindingInfo;
  24. }

可以看到parameterBindingInfo 本质是一个BinderItem[]

  1. private readonly struct BinderItem
  2. {
  3. public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata)
  4. {
  5. ModelBinder = modelBinder;
  6. ModelMetadata = modelMetadata;
  7. }
  8.  
  9. public IModelBinder ModelBinder { get; }
  10.  
  11. public ModelMetadata ModelMetadata { get; }
  12. }

通过遍历目标Action的所有参数actionDescriptor.Parameters,根据参数逐一匹配一个对应定的处理对象BinderItem。

如本例,会匹配到两个Binder:

参数 user   ===>  {Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder}

参数 note  ===>   {Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinder}

这是如何匹配的呢,系统定义了一系列provider,如下图

                图一

会遍历他们分别与当前参数做匹配:

  1. for (var i = ; i < _providers.Length; i++)
  2. {
  3. var provider = _providers[i];
  4. result = provider.GetBinder(providerContext);
  5. if (result != null)
  6. {
  7. break;
  8. }
  9. }

同样以这两个Binder为例看一下,BodyModelBinderProvider

  1. public IModelBinder GetBinder(ModelBinderProviderContext context)
  2. {
  3. if (context == null)
  4. {
  5. throw new ArgumentNullException(nameof(context));
  6. }
  7.  
  8. if (context.BindingInfo.BindingSource != null &&
  9. context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
  10. {
  11. if (_formatters.Count == )
  12. {
  13. throw new InvalidOperationException(Resources.FormatInputFormattersAreRequired(
  14. typeof(MvcOptions).FullName,
  15. nameof(MvcOptions.InputFormatters),
  16. typeof(IInputFormatter).FullName));
  17. }
  18.  
  19. return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options);
  20. }
  21.  
  22. return null;
  23. }

BodyModelBinder的主要判断依据是BindingSource.Body  也就是user参数我们设置了[FromBody]。

同理SimpleTypeModelBinder的判断依据是 if (!context.Metadata.IsComplexType) 。

找到对应的provider后,则会由该provider来new 一个 ModelBinder返回,也就有了上文的BodyModelBinder和SimpleTypeModelBinder。

小结:至此前期准备工作已经完成,这里创建了三个重要的对象:

1. Task Bind() ,用于绑定的方法,并被封装到了invoker内的CacheEntry中。

2. parameterBindingInfo :本质是一个BinderItem[],其中的BinderItem数量与Action的参数数量相同。

3. propertyBindingInfo:类似parameterBindingInfo, 用于属性绑定,下面详细介绍。

                  图二

三、执行阶段

  从上一节的小结可以猜到,执行阶段就是调用Bind方法,利用创建的parameterBindingInfo和propertyBindingInfo将请求发送来的参数处理后赋值给Action对应的参数。

  同样,这个阶段发生在invoker(即ControllerActionInvoker)的InvokeAsync()阶段,当调用到它的Next方法的时候,首先第一步State为ActionBegin的时候就会调用BindArgumentsAsync()方法,如下

  1. private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
  2. {
  3. switch (next)
  4. {
  5. case State.ActionBegin:
  6. {
  7.               //略。。。
  8. _arguments = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
  9.  
  10. var task = BindArgumentsAsync();
  11. }

而BindArgumentsAsync()方法会调用上一节创建的_cacheEntry.ControllerBinderDelegate,也就是Task Bind() 方法

  1. private Task BindArgumentsAsync()
  2. {
  3. // 略。。。
  4.  
  5. return _cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments);
  6. }

上一节略了,现在详细看一下这个方法,

  1. async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
  2. {
  3. var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
  4. var parameters = actionDescriptor.Parameters;
  5.  
  6. for (var i = ; i < parameters.Count; i++) //遍历参数集和,逐一处理
  7. {
  8. var parameter = parameters[i];
  9. var bindingInfo = parameterBindingInfo[i];
  10. var modelMetadata = bindingInfo.ModelMetadata;
  11.  
  12. if (!modelMetadata.IsBindingAllowed)
  13. {
  14. continue;
  15. }
  16.  
  17. var result = await parameterBinder.BindModelAsync(
  18. controllerContext,
  19. bindingInfo.ModelBinder,
  20. valueProvider,
  21. parameter,
  22. modelMetadata,
  23. value: null);
  24.  
  25. if (result.IsModelSet)
  26. {
  27. arguments[parameter.Name] = result.Model;
  28. }
  29. }
  30.  
  31. var properties = actionDescriptor.BoundProperties;
  32. for (var i = ; i < properties.Count; i++)
  33. //略
  34. }

  主体就是两个for循环,分别用于处理参数和属性,依然是以参数处理为例说明。

  依然是先获取到Action所有的参数,然后进入for循环进行遍历,通过parameterBindingInfo[i]获取到参数对应的BinderItem,这些都准备好后调用parameterBinder.BindModelAsync()方法进行参数处理和赋值。注意这里传入了 bindingInfo.ModelBinder ,在parameterBinder中会调用传入的modelBinder的BindModelAsync方法

  1. modelBinder.BindModelAsync(modelBindingContext);

而这个modelBinder是根据参数匹配的,也就是到现在已经将被处理对象交给了上文的BodyModelBinder、SimpleTypeModelBinder等具体的ModelBinder了。

以BodyModelBinder为例:

  1. public async Task BindModelAsync(ModelBindingContext bindingContext)
  2. {
  3. //略。。。
  4.  
  5. var formatterContext = new InputFormatterContext(httpContext,modelBindingKey,bindingContext.ModelState, bindingContext.ModelMetadata, _readerFactory, allowEmptyInputInModelBinding);
  6. var formatter = (IInputFormatter)null;
  7. for (var i = ; i < _formatters.Count; i++)
  8. {
  9. if (_formatters[i].CanRead(formatterContext))
  10. {
  11. formatter = _formatters[i];
  12. _logger?.InputFormatterSelected(formatter, formatterContext);
  13. break;
  14. }
  15. else
  16. {
  17. _logger?.InputFormatterRejected(_formatters[i], formatterContext);
  18. }
  19. }
  20.  
  21. var result = await formatter.ReadAsync(formatterContext);
  1. //略。。。
  1. }

部分代码已省略,剩余部分可以看到,这里像上文匹配provider一样,会遍历一个名为_formatters的集和,通过子项的CanRead方法来确定是否可以处理这样的formatterContext。若可以,则调用该formatter的ReadAsync()方法进行处理。这个_formatters集和默认有两个Formatter, Microsoft.AspNetCore.Mvc.Formatters.JsonPatchInputFormatter} 和  Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatter , JsonPatchInputFormatter的判断逻辑是这样的

  1. if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) ||
  2. !modelTypeInfo.IsGenericType)
  3. {
  4. return false;
  5. }

  它会判断请求的类型是否为IJsonPatchDocument,JsonPatch见本文后面的备注,回到本例,我们经常情况遇到的还是用JsonInputFormatter,此处它会被匹配到。它继承自TextInputFormatter , TextInputFormatter 又继承自 InputFormatter,JsonInputFormatter未重写CanRead方法,采用InputFormatter的CanRead方法。

  1. public virtual bool CanRead(InputFormatterContext context)
  2. {
  3. if (SupportedMediaTypes.Count == )
  4. {
  5. var message = Resources.FormatFormatter_NoMediaTypes(GetType().FullName, nameof(SupportedMediaTypes));
  6. throw new InvalidOperationException(message);
  7. }
  8.  
  9. if (!CanReadType(context.ModelType))
  10. {
  11. return false;
  12. }
  13.  
  14. var contentType = context.HttpContext.Request.ContentType;
  15. if (string.IsNullOrEmpty(contentType))
  16. {
  17. return false;
  18. }
  19. return IsSubsetOfAnySupportedContentType(contentType);
  20. }

  例如要求ContentType不能为空。本例参数为 [FromBody]User user ,并标识了 content-type: application/json ,通过CanRead验证后,

  1. public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context,Encoding encoding)
  2. {
  3. //略。。。。using (var streamReader = context.ReaderFactory(request.Body, encoding))
  4. {
  5. using (var jsonReader = new JsonTextReader(streamReader))
  6. {
  7. jsonReader.ArrayPool = _charPool;
  8. jsonReader.CloseInput = false;
  9.             //略。。var type = context.ModelType;
  10. var jsonSerializer = CreateJsonSerializer();
  11. jsonSerializer.Error += ErrorHandler;
  12. object model;
  13. try
  14. {
  15. model = jsonSerializer.Deserialize(jsonReader, type);
  16. }
  17.  
  18.             //略。。。
  19. }
  20. }
  21. }

  可以看到此处就是将收到的请求的内容Deserialize,获取到一个model返回。此处的jsonSerializer是 Newtonsoft.Json.JsonSerializer ,系统默认采用的json处理组件是Newtonsoft。model返回后,被赋值给对应的参数,至此赋值完毕。

小结:本阶段的工作是获取请求参数的值并赋值给Action的对应参数的过程。由于参数不同,会分配到一些不同的处理方法中处理。例如本例涉及到的provider(图一)、不同的ModelBinder(BodyModelBinder和SimpleTypeModelBinder)、不同的Formatter等等,实际项目中还会遇到其他的类型,这里不再赘述。

而文中有两个需要单独说明的,在后面的小节里说一下。

四、propertyBindingInfo

上文提到了但没有介绍,它主要用于处理Controller的属性的赋值,例如:

  1. public class FlyLoloController : Controller
  2. {
  3. [ModelBinder]
  4. public string Key { get; set; }

有一个属性Key被标记为[ModelBinder],它会在Action被请求的时候,像给参数赋值一样赋值,处理方式也类似,不再描述。

五、JsonPatch

上文中提到了JsonPatchInputFormatter,简要说一下JsonPatch,可以理解为操作json的文档,比如上文的User类是这样的:

  1. public class User
  2. {
  3. public string Code { get; set; }
  4. public string Name { get; set; }
    //other ...
  5. }

  现在我只想修改它的Name属性,默认情况下我仍然会需要提交这样的json

  1. {"Code":"001","Name":"张三", .........}

  这不科学,从省流量的角度来说也觉得太多了,用JsonPatch可以这样写

  1. [
  2. { "op" : "replace", "path" : "/Name", "value" : "张三" }
  3. ]

  

ASP.NET Core 2.2 十九. Action参数的映射与模型绑定的更多相关文章

  1. ASP.NET Core 2.2 十九. 你扔过来个json,我怎么接

    原文:ASP.NET Core 2.2 十九. 你扔过来个json,我怎么接 前文说道了Action的激活,这里有个关键的操作就是Action参数的映射与模型绑定,这里即涉及到简单的string.in ...

  2. ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案 try.dot.net 的正确使用姿势 .Net NPOI 根据excel模板导出excel、直接生成excel .Net NPOI 上传excel文件、提交后台获取excel里的数据

    ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案   ASP.NET Core 从2.2版本开始,采用了一个新的名为Endpoint的路由方案,与原来的方案在使用上差别不 ...

  3. ExpandoObject与DynamicObject的使用 RabbitMQ与.net core(一)安装 RabbitMQ与.net core(二)Producer与Exchange ASP.NET Core 2.1 : 十五.图解路由(2.1 or earler) .NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

    ExpandoObject与DynamicObject的使用   using ImpromptuInterface; using System; using System.Dynamic; names ...

  4. ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案

    原文:ASP.NET Core 2.2 : 十六.扒一扒2.2版更新的新路由方案 ASP.NET Core 从2.2版本开始,采用了一个新的名为Endpoint的路由方案,与原来的方案在使用上差别不大 ...

  5. ASP.NET Core MVC中Controller的Action,默认既支持HttpGet,又支持HttpPost

    我们知道ASP.NET Core MVC中Controller的Action上可以声明HttpGet和HttpPost特性标签,来限制可以访问Action的Http请求类型(GET.POST等). 那 ...

  6. ASP.NET Core MVC中Controller的Action如何直接使用Response.Body的Stream流输出数据

    在ASP.NET Core MVC中,我们有时候需要在Controller的Action中直接输出数据到Response.Body这个Stream流中,例如如果我们要输出一个很大的文件到客户端浏览器让 ...

  7. ASP.NET MVC 5使用Filter过滤Action参数防止sql注入,让你代码安全简洁

    在开发程序的过程中,稍微不注意就会隐含有sql注入的危险.今天我就来说下,ASP.NET mvc 5使用Filter过滤Action参数防止sql注入,让你代码安全简洁.不用每下地方对参数的值都进行检 ...

  8. 创建基于ASP.NET core 3.1 的RazorPagesMovie项目(二)-应用模型类配合基架生成工具生成Razor页面

    本节中,将学习添加用于管理跨平台的SQLLite数据库中的电影的类Movie.从ASP.NET core 模板创建的应用使用SQLLite数据库. 应用模型类(Movie)配合Entity Frame ...

  9. ASP.NET Core 2.2 十八.各种Filter的内部处理机制及执行顺序

    ASP.NET core 的Filter是系统中经常用到的,本文详细分享一下各种Filter定义.执行的内部机制以及执行顺序.(ASP.NET Core 系列目录) 一. 概述 ASP.NET Cor ...

随机推荐

  1. 在asp.net core2.1中添加中间件以扩展Swashbuckle.AspNetCore3.0支持简单的文档访问权限控制

    Swashbuckle.AspNetCore3.0 介绍 一个使用 ASP.NET Core 构建的 API 的 Swagger 工具.直接从您的路由,控制器和模型生成漂亮的 API 文档,包括用于探 ...

  2. DuelJS 介绍

    DuelJS 是什么? DuelJS是一个快速和小型的JavaScript库,可以帮助实现浏览器tab页主从关系的切换.使用它可以优化你浏览器和服务器之间的通信,以及你浏览器内部tab页之间的通信. ...

  3. pandas列合并为一行

    将dataframe利用pandas列合并为一行,类似于sql的GROUP_CONCAT函数.例如如下dataframe id_part pred pred_class v_id 0 d 0 0.12 ...

  4. 从零开始搭建运维体系 - ansible

    从零开始搭建运维体系 - ansible 基本配置好了局域网内的机器后,第一个遇到的问题就是如何批量操作这么多台机器,ansible就是这么一个自动化运维工具. ansible是一个基于ssh的批量远 ...

  5. springboot~Mongodb的集成与使用

    说说springboot与大叔lind.ddd的渊源 Mongodb在Lind.DDD中被二次封装过(大叔的.net和.net core),将它当成是一种仓储来使用,对于开发人员来说只公开curd几个 ...

  6. SQL优化 MySQL版 -分析explain SQL执行计划与Type级别详解

    type索引类型.类型 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 注:看此文章前,需要有一定的Mysql基础或观看上一篇文章,该文章传送门: https://www.cnblo ...

  7. SpringBoot技术栈搭建个人博客【后台开发】

    前言:在之前,我们已经完成了项目的基本准备,那么就可以开始后台开发了,突然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,但是后台管理我们同样也是需要API的,那么就在这一篇里 ...

  8. Virtual Box虚拟机Ubuntu18.X系统安装及Mysql基本开发配置

    Linux简介 什么是 Linux? Linux:世界上不仅只有一个 Windows 操作系统,还有 Linux.mac.Unix 等操作系统.桌面操作系统下 Windows 是霸主,而 Linux ...

  9. Chrome - JavaScript调试技巧总结(浏览器调试JS)

    Chrome 是 Google 出品的一款非常优秀的浏览器,其内置了开发者工具(Windows 系统中按下 F12 即可开启),可以让我们方便地对 JavaScript 代码进行调试. 为方便大家学习 ...

  10. Flask的session使用

    由于http是无状态保存的协议,session可以看作不同请求之间保存数据的一种机制.flask的session是基于cookie的会话保持. 流程 当客户端进行第一次请求的时候,客户端的HTTP r ...