前文说道了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" : "张三" }
]