ASP.NET Core 2.2 十九. Action参数的映射与模型绑定

前文说道了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://2i3i.com/aspnetcore2_19.html ,转载请注明来源地址。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇