前文说道了 Action 的激活, 这里有个关键的操作就是 Action 参数的映射与模型绑定, 这里即涉及到简单的 string,int 等类型, 也包含 JSON 等复杂类型, 本文详细分享一下这一过程.(ASP.NET Core 系列目录)
一, 概述
当客户端发出一个请求的时候, 参数可能存在于 URL 中也可能是在请求的 Body 中, 而参数类型也大不相同, 可能是简单类型的参数, 如字符串, 整数或浮点数, 也可能是复杂类型的参数, 比如常见的 JSON,xml 等, 这些事怎么与目标 Action 的参数关联在一起并赋值的呢?
故事依然是发生在通过路由确定了被请求的 Action 之后, invoker 的创建与执行阶段 (详见 Action 的执行).
invoker 的创建阶段, 创建处理方法, 并根据目标 Action 的 actionDescriptor 获取到它的所有参数, 分析各个参数的类型确定对应参数的绑定方法,
invoker 的执行阶段, 调用处理方法, 遍历参数逐一进行赋值.
为了方便描述, 创建一个测试 Action 如下, 它有两个参数, 下文以此为例进行描述.:
- public JsonResult Test([FromBody]User user,string note = "FlyLolo")
- {
- return new JsonResult(user.Code + "|" + user.Name );
- }
二, 准备阶段
1. 创建绑定方法
当收到请求后, 由路由系统确定了被访问的目标 Action 是我们定义的 Test 方法, 这时进入 invoker 的创建阶段, 前文说过它有一个关键属性 cacheEntry 是由多个对象组装而成 (发生在 ControllerActionInvokerCache 的 GetCachedResult 方法中), 其中一个是 propertyBinderFactory:
var propertyBinderFactory = ControllerBinderDelegateProvider.CreateBinderDelegate(_parameterBinder,_modelBinderFactory,_modelMetadataProvider,actionDescriptor,_mvcOptions);
看一下 CreateBinderDelegate 这个方法:
- public static ControllerBinderDelegate CreateBinderDelegate(ParameterBinder parameterBinder,IModelBinderFactory modelBinderFactory,
- IModelMetadataProvider modelMetadataProvider, ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
- {
- // 各种验证 略
- var parameterBindingInfo = GetParameterBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor, mvcOptions);
- var propertyBindingInfo = GetPropertyBindingInfo(modelBinderFactory, modelMetadataProvider, actionDescriptor);
- if (parameterBindingInfo == null && propertyBindingInfo == null)
- {
- return null;
- }
- return Bind;
- async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
- {
- // 后文详细描述
- }
- }
前文说过, invoker 的创建阶段就是创建一些关键对象和一些用于执行的方法, 而 propertyBinderFactory 就是众多方法之中的一个, 前文介绍它是一个用于参数绑定的 Task, 而没有详细说明, 现在可以知道它被定义为一个名为 Bind 的 Task, 最终作为 invoker 的一部分等待被执行进行参数绑定.
2. 为每个参数匹配 Binder
上面的 CreateBinderDelegate 方法创建了两个对象 parameterBindingInfo 和 propertyBindingInfo , 顾名思义, 一个用于参数一个用于属性. 看一下 parameterBindingInfo 的创建:
- private static BinderItem[] GetParameterBindingInfo(IModelBinderFactory modelBinderFactory,IModelMetadataProvider modelMetadataProvider,ControllerActionDescriptor actionDescriptor, MvcOptions mvcOptions)
- {
- var parameters = actionDescriptor.Parameters;
- if (parameters.Count == 0)
- {
- return null;
- }
- var parameterBindingInfo = new BinderItem[parameters.Count];
- for (var i = 0; i <parameters.Count; i++)
- {
- var parameter = parameters[i];
- // 略...
- var binder = modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
- {
- BindingInfo = parameter.BindingInfo,
- Metadata = metadata,
- CacheToken = parameter,
- });
- parameterBindingInfo[i] = new BinderItem(binder, metadata);
- }
- return parameterBindingInfo;
- }
可以看到 parameterBindingInfo 本质是一个 BinderItem[]
- private readonly struct BinderItem
- {
- public BinderItem(IModelBinder modelBinder, ModelMetadata modelMetadata)
- {
- ModelBinder = modelBinder;
- ModelMetadata = modelMetadata;
- }
- public IModelBinder ModelBinder { get; }
- public ModelMetadata ModelMetadata { get; }
- }
通过遍历目标 Action 的所有参数 actionDescriptor.Parameters, 根据参数逐一匹配一个对应定的处理对象 BinderItem.
如本例, 会匹配到两个 Binder:
参数 user ===> {Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder}
参数 note ===> {Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinder}
这是如何匹配的呢, 系统定义了一系列 provider, 如下图
图一
会遍历他们分别与当前参数做匹配:
- for (var i = 0; i <_providers.Length; i++)
- {
- var provider = _providers[i];
- result = provider.GetBinder(providerContext);
- if (result != null)
- {
- break;
- }
- }
同样以这两个 Binder 为例看一下, BodyModelBinderProvider:
- public IModelBinder GetBinder(ModelBinderProviderContext context)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
- if (context.BindingInfo.BindingSource != null &&
- context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
- {
- if (_formatters.Count == 0)
- {
- throw new InvalidOperationException(Resources.FormatInputFormattersAreRequired(
- typeof(MvcOptions).FullName,
- nameof(MvcOptions.InputFormatters),
- typeof(IInputFormatter).FullName));
- }
- return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options);
- }
- return null;
- }
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() 方法, 如下
- private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
- {
- switch (next)
- {
- case State.ActionBegin:
- {
- // 略...
- _arguments = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
- var task = BindArgumentsAsync();
- }
而 BindArgumentsAsync() 方法会调用上一节创建的_cacheEntry.ControllerBinderDelegate, 也就是 Task Bind() 方法
- private Task BindArgumentsAsync()
- {
- // 略...
- return _cacheEntry.ControllerBinderDelegate(_controllerContext, _instance, _arguments);
- }
上一节略了, 现在详细看一下这个方法,
- async Task Bind(ControllerContext controllerContext, object controller, Dictionary<string, object> arguments)
- {
- var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
- var parameters = actionDescriptor.Parameters;
- for (var i = 0; i <parameters.Count; i++) // 遍历参数集和, 逐一处理
- {
- var parameter = parameters[i];
- var bindingInfo = parameterBindingInfo[i];
- var modelMetadata = bindingInfo.ModelMetadata;
- if (!modelMetadata.IsBindingAllowed)
- {
- continue;
- }
- var result = await parameterBinder.BindModelAsync(
- controllerContext,
- bindingInfo.ModelBinder,
- valueProvider,
- parameter,
- modelMetadata,
- value: null);
- if (result.IsModelSet)
- {
- arguments[parameter.Name] = result.Model;
- }
- }
- var properties = actionDescriptor.BoundProperties;
- for (var i = 0; i < properties.Count; i++)
- // 略
- }
主体就是两个 for 循环, 分别用于处理参数和属性, 依然是以参数处理为例说明.
依然是先获取到 Action 所有的参数, 然后进入 for 循环进行遍历, 通过 parameterBindingInfo[i] 获取到参数对应的 BinderItem, 这些都准备好后调用 parameterBinder.BindModelAsync() 方法进行参数处理和赋值. 注意这里传入了 bindingInfo.ModelBinder , 在 parameterBinder 中会调用传入的 modelBinder 的 BindModelAsync 方法
modelBinder.BindModelAsync(modelBindingContext);
而这个 modelBinder 是根据参数匹配的, 也就是到现在已经将被处理对象交给了上文的 BodyModelBinder,SimpleTypeModelBinder 等具体的 ModelBinder 了.
以 BodyModelBinder 为例:
- public async Task BindModelAsync(ModelBindingContext bindingContext)
- {
- // 略...
- var formatterContext = new InputFormatterContext(httpContext,modelBindingKey,bindingContext.ModelState, bindingContext.ModelMetadata, _readerFactory, allowEmptyInputInModelBinding);
- var formatter = (IInputFormatter)null;
- for (var i = 0; i < _formatters.Count; i++)
- {
- if (_formatters[i].CanRead(formatterContext))
- {
- formatter = _formatters[i];
- _logger?.InputFormatterSelected(formatter, formatterContext);
- break;
- }
- else
- {
- _logger?.InputFormatterRejected(_formatters[i], formatterContext);
- }
- }
- var result = await formatter.ReadAsync(formatterContext);
- // 略...
- }
部分代码已省略, 剩余部分可以看到, 这里像上文匹配 provider 一样, 会遍历一个名为_formatters 的集和, 通过子项的 CanRead 方法来确定是否可以处理这样的 formatterContext. 若可以, 则调用该 formatter 的 ReadAsync() 方法进行处理. 这个_formatters 集和默认有两个 Formatter, Microsoft.AspNetCore.Mvc.Formatters.JsonPatchInputFormatter} 和 Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatter , JsonPatchInputFormatter 的判断逻辑是这样的
- if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) ||
- !modelTypeInfo.IsGenericType)
- {
- return false;
- }
它会判断请求的类型是否为 IJsonPatchDocument,JsonPatch 见本文后面的备注, 回到本例, 我们经常情况遇到的还是用 JsonInputFormatter, 此处它会被匹配到. 它继承自 TextInputFormatter , TextInputFormatter 又继承自 InputFormatter,JsonInputFormatter 未重写 CanRead 方法, 采用 InputFormatter 的 CanRead 方法.
- public virtual bool CanRead(InputFormatterContext context)
- {
- if (SupportedMediaTypes.Count == 0)
- {
- var message = Resources.FormatFormatter_NoMediaTypes(GetType().FullName, nameof(SupportedMediaTypes));
- throw new InvalidOperationException(message);
- }
- if (!CanReadType(context.ModelType))
- {
- return false;
- }
- var contentType = context.HttpContext.Request.ContentType;
- if (string.IsNullOrEmpty(contentType))
- {
- return false;
- }
- return IsSubsetOfAnySupportedContentType(contentType);
- }
例如要求 ContentType 不能为空. 本例参数为 [FromBody]User user , 并标识了 content-type: application/JSON , 通过 CanRead 验证后,
- public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context,Encoding encoding)
- {
- // 略....using (var streamReader = context.ReaderFactory(request.Body, encoding))
- {
- using (var jsonReader = new JsonTextReader(streamReader))
- {
- jsonReader.ArrayPool = _charPool;
- jsonReader.CloseInput = false;
- // 略..var type = context.ModelType;
- var jsonSerializer = CreateJsonSerializer();
- jsonSerializer.Error += ErrorHandler;
- object model;
- try
- {
- model = jsonSerializer.Deserialize(jsonReader, type);
- }
- // 略...
- }
- }
- }
可以看到此处就是将收到的请求的内容 Deserialize, 获取到一个 model 返回. 此处的 jsonSerializer 是 Newtonsoft.JSON.JsonSerializer , 系统默认采用的 JSON 处理组件是 Newtonsoft.model 返回后, 被赋值给对应的参数, 至此赋值完毕.
小结: 本阶段的工作是获取请求参数的值并赋值给 Action 的对应参数的过程. 由于参数不同, 会分配到一些不同的处理方法中处理. 例如本例涉及到的 provider(图一), 不同的 ModelBinder(BodyModelBinder 和 SimpleTypeModelBinder), 不同的 Formatter 等等, 实际项目中还会遇到其他的类型, 这里不再赘述.
而文中有两个需要单独说明的, 在后面的小节里说一下.
四, propertyBindingInfo
上文提到了但没有介绍, 它主要用于处理 Controller 的属性的赋值, 例如:
- public class FlyLoloController : Controller
- {
- [ModelBinder]
- public string Key { get; set; }
有一个属性 Key 被标记为 [ModelBinder], 它会在 Action 被请求的时候, 像给参数赋值一样赋值, 处理方式也类似, 不再描述.
五, JsonPatch
上文中提到了 JsonPatchInputFormatter, 简要说一下 JsonPatch, 可以理解为操作 JSON 的文档, 比如上文的 User 类是这样的:
- public class User
- {
- public string Code { get; set; }
- public string Name { get; set; }
- //other ...
- }
现在我只想修改它的 Name 属性, 默认情况下我仍然会需要提交这样的 JSON
{"Code":"001","Name":"张三", .........}
这不科学, 从省流量的角度来说也觉得太多了, 用 JsonPatch 可以这样写
- [
- { "op" : "replace", "path" : "/Name", "value" : "张三" }
- ]
来源: https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_19.html