你所不知道的ASP.NET Core MVC/WebApi基础系列(二)

前言

很久没冒泡了,算起来估计有快半年没更新博客了,估计是我第一次停更如此之久,人总有懒惰的时候,时间越长越懒惰,可是呢,不学又不行,持续的惰性是不行dei,要否则会被时光所抛弃,技术所淘汰,好吧,进入今天的主题,本节内容,咱们来说讲.NET Core当中的模型绑定系统、模型绑定原理、自定义模型绑定、混合绑定、ApiController特性本质,可能有些园友已经看过,可是效果不太好哈,这篇是解释最为详细的一篇,建议已经学过我发布课程的童鞋也看下,本篇内容略长,请保持耐心,我只讲大家会用到的或者说可以学到东西的内容。api

 

模型绑定系统

对于模型绑定,.NET Core给咱们提供了[BindRequired]、[BindNever]、[FromHeader]、[FromQuery]、[FromRoute]、[FromForm]、[FromServices]、[FromBody]等特性,[BindRequired]和[BindNever]翻译成必须绑定,从不绑定咱们称之为行为绑定,而紧跟后面的五个From,翻译成从哪里来,咱们称之为来源绑定,下面咱们详细介绍这两种绑定类型,本节内容使用版本.NET Core 2.2版本。安全

行为绑定

 [BindRequired]表示参数的键必需要提供,可是并不关心参数的值是否为空,[BindNever]表示忽略对属性的绑定,行为绑定看似很简单,其实否则,待我娓娓道来,首先咱们来看以下代码片断。app

    public class Customer
    {
        [BindNever]
        public int Id { get; set; }
    }

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post(Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

上述咱们定义了一个Customer类,而后类中的id字段经过[BindNever]特性进行标识,接下来咱们一切都经过Postman来发出请求框架

当咱们如上发送请求时,响应将返回状态码200成功且id没有绑定上,符合咱们的预期,其意思就是从不绑定属性id,好接下来咱们将控制器上的Post方法参数添加[FromBody]标识看看,代码片断变成以下:ide

        [HttpPost]
        public IActionResult Post([FromBody]Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }

这是为什么,咱们经过[FromBody]特性标识后,此时也将属性id加上了[BindNever]特性(代码和如上同样,不重复贴了),结果id绑定上了,说明[BindNever]特性对经过[FromBody]特性标识的参数无效,状况真的是这样吗?接下来咱们尝试将[BindNever]绑定到对象看看,以下:函数

    public class Customer
    {
        public int Id { get; set; }
    }

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post([BindNever][FromBody]Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

上述咱们将[BindNever]绑定到对象Customer上,同时对于[BindNever]和[FromBody]特性没有前后顺序,也就是说咱们也能够将[FromBody]放在[BindNever]后面,接下来咱们利用Postman再次发送以下请求。ui

此时咱们能够明确看到,咱们发送的请求包含id字段,且此时咱们将[BindNever]绑定到对象上时,最终id则没绑定到对象上,达到咱们的预期且验证经过,可是话说回来,将[BindNever]绑定到对象上毫无心义,由于此时对象上全部属性都将会被忽略。因此到这里咱们能够得出[BindNever]对于[FromBody]特性请求的结论:url

对于使用【FromBody】特性标识的请求,【BindNever】特性应用到模型上的属性时,此时绑定无效,应用到模型对象上时,此时将彻底忽略对模型对象上的全部属性spa

对于来自URL或者表单上的请求,【BindNever】特性应用到模型上的属性时,此时绑定无效,应用到模型对象时,此时将彻底忽略对模型对象上的全部属性翻译

 好了,接下来咱们再来看看[BindRequired],咱们继续给出以下代码:

   public class Customer
    {
        [BindRequired]
        public int Id { get; set; }
    }

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post(Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

 

经过[BindRequired]特性标识属性,咱们基于表单的请求且未给出属性id的值,此时属性未绑定上且验证未经过,符合咱们预期。接下来咱们再来看看【FromBody】特性标识的请求,代码就不给出了,咱们只是在对象上加上了[FromBody]而已,咱们看看最终结果。

此时从表面上看好像达到了咱们的预期,在这里即便咱们对属性id不指定【BindRequired】特性,结果也是同样验证未经过,这是为什么,由于默认状况下,在.NET Core中对于【FromBody】特性标识的对象不可为空,内置进行了处理,咱们进行以下设置容许为空。

    services.AddMvc(options=> 
            {
                options.AllowEmptyInputInBodyModelBinding = true;
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

咱们进行上述设置后,咱们不给定属性id的值,确定会验证经过对不对,那咱们接下来再给定一个属性Age呢,而后发出请求不包含Age属性,以下

    public class Customer
    {
        [BindRequired]
        public int Id { get; set; }

        [BindRequired]
        public int Age { get; set; }
    }

到这里咱们发现咱们对属性Age添加了【BindRequired】特性,此时验证倒是经过的,咱们再加思考一番,或许是咱们给定的属性Age是int有默认值为0,因此验证经过,好想法,你能够继续添加一个字符串类型的属性,而后添加【BindRequired】特性,同时最后请求中不包含该属性,此时结果依然是验证经过的(不信本身试试)。

此时咱们发现经过[FromBody]特性标识的请求,咱们将默认对象不可空的状况排除在外,说明[BindRequired]特性标识的属性对[FromBody]特性标识的请求无效,同时呢,咱们转到[BindRequired]特性的定义有以下解释:

// 摘要:
// Indicates that a property is required for model binding. When applied to a property, the model binding system requires a value for that property. When applied to
// a type, the model binding system requires values for all properties that type defines.

翻译过来不难理解,当咱们经过[BindRequired]特性标识时,说明在模型绑定时属性是必须给出的,当应用到属性时,要求模型绑定系统必须验证此属性的值必需要给出,当应用到类型时,要求模型绑定系统必须验证类型中定义的全部属性必须有值。这个解释让咱们没法信服,对于基于URL或者基于表单的请求和【FromBody】特性的请求明显有区别,可是定义倒是一律而论。到这里咱们遗漏到了一个【Required】特性,咱们添加一个Address属性,而后请求中不包含Address属性,

    public class Customer
    {
        [BindRequired]
        public int Id { get; set; }
        [BindRequired]
        public int Age { get; set; }
        [Required]
        public string Address { get; set; }
    }

从上图看出使用【FromBody】标识的请求,经过Required特性标识属性也符合预期,固然对于URL和表单请求也符合预期,在此再也不演示。我并未看过源码,我大胆猜想下是不是以下缘由才有其区别呢(我的猜想)

解释都在强调模型绑定系统,因此在.NET Core中出现的【BindNever】和【BindRequired】特性专为.NET Core MVC模型绑定系统而设计,而对于【FromBody】特性标识后,由于其进行属性的序列化和反序列化与Input Formatter有关,好比经过JSON.NET,因此至于属性的忽略和映射与否和咱们使用序列化和反序列化的框架有关,由咱们本身来定义,好比使用JSON.NET则属性忽略使用【JsonIgnore】。

因此说基于【FromBody】特性标识的请求,是否映射,是否必须由咱们使用的序列化和反序列化框架决定,在.NET Core中默认是JSON.NET,因此对于如上属性是否必须提供,咱们须要使用JSON.NET中的Api,好比以下。

    public class Customer
    {
        [JsonProperty(Required = Required.Always)]
        public int Id { get; set; }

        [JsonProperty(Required = Required.Always)]
        public int Age { get; set; }
    }

请求参数安全也是须要咱们考虑的因素,好比以下咱们对象包含IsAdmin属性,咱们后台会根据该属性值判断是否为对应角色进行UI的渲染,咱们能够经过[Bind]特性应用于对象指定映射哪些属性,此时请求中参数即便显式指定了该参数值也不会进行映射(这里仅仅只是举例说明,例子可能并不是合理),代码以下:

    public class Customer
    {
        public int Id { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }
        public bool IsAdmin { get; set; }
    }

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post(
            [Bind(nameof(Customer.Id),nameof(Customer.Age),nameof(Customer.Address) )] Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

来源绑定

在.NET Core中出现了不一样的特性,好比上述咱们所讲解的行为绑定,而后是接下来咱们要讲解的来源绑定,它们出现的意义和做用在哪里呢?它比.NET中的模型绑定更加灵活,而不是同样,为什么灵活不是我嘴上说说而已,经过实际例子证实给你看,每个新功能或特性的出现是为了解决对应的问题或改善对应的问题,首先咱们来看以下代码:

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost("{id:int}")]
        public IActionResult Post(int id, Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

 咱们经过路由指定id为4,而后url上指定为3,你猜映射到后台id上的参数结果是4仍是3呢,在customer上的参数id是4仍是3呢?

从上图咱们看到id是4,而customer对象中的id值为2,咱们从中能够得出一个什么结论呢,来,咱们进行以下总结。

 在.NET Core中,默认状况下参数绑定存在优先级,路由的优先级大于表单的优先级,表单的优先级大于URL的优先级即(路由>表单>URL)

这是默认状况下的优先级,为何说在.NET Core中很是灵活呢,由于咱们能够经过来源进行显式绑定,好比强制指定id来源于查询字符串,而customer中的id源于查询路由,以下:

        [HttpPost("{id:int}")]
        public IActionResult Post([FromQuery]int id, [FromRoute] Customer customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }

还有什么[FromForm]、[FromServices]、[FromHeader]等来源绑定都是强制指定参数究竟是来源于表单、请求头、查询字符串、路由仍是Body,到这里无需我再过多讲解了,一个例子足以说明其灵活性。

模型绑定(强大支持举例)

上述讲解来源绑定咱们认识到其灵活性,可能有部分童鞋压根都不知道.NET Core中对模型绑定的强大支持,哪里强大了,在讲解模型绑定原理以前,来给你们举几个实际的例子来讲明,首先咱们来看以下请求代码:

对于如上请求,咱们大部分的作法则是经过以下建立一个类来接受上述URL参数。

    public class Example
    {
        public int A { get; set; }
        public int B { get; set; }
        public int C { get; set; }
    }

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpGet]
        public IActionResult Post(Example employee)
        {
            return Ok();
        }
    }

这种常见作法在ASP.NET MVC/Web Api中也是支持的,好了,接下来咱们将上述控制器代码进行以下修改后在.NET Core中是支持的,而在.NET MVC/Web Api中是不支持的,不信,您能够试试。

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpGet]
        public IActionResult Get(Dictionary<string, int> pairs)
        {
            return Ok();
        }
    }

至于在.NE Core中为什么可以绑定上,主要是在.NET Core实现了字典的DictionaryModelBinder,因此能够将URL上的参数当作字典的键,而参数值做为键对应的值,看的不过瘾,对不对,好,接下来咱们看看以下请求,您以为控制器应该如何接收URL上的参数呢?

 

大胆发挥您的想象,在咱们的控制器Action方法上,咱们如何去接收上述URL上的参数呢?好了,不卖关子了,

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpGet]
        public IActionResult Post(List<Dictionary<string, int>> pairs)
        {
            return Ok();
        }
    }

是否是说明.NET Core就不支持了呢?显然不是,咱们将参数名称须要修改一致才行,咱们将URL上的参数名称修改成和控制器方法上的参数一致(固然类型也要一致,不然也会映射不上),以下:

 好了,见识到.NET Core中模型绑定系统的强大,接下来咱们马不停蹄去看看模型绑定原理是怎样的吧,GO。

模型绑定原理

了解模型绑定原理有什么做用呢?当.NET Core提供给咱们的模型绑定系统不知足咱们的需求时,咱们能够自定义模型绑定来实现咱们的需求,这里我简单说下整个过程是这样的,而后呢,给出我画的一张详细图关于模型绑定的整个过程是这样。当咱们在startup中使用services.AddMvc()方法时,说明咱们会使用MVC框架,此时在背后对于模型绑定作了什么呢?

【1】初始化ModelBinderProviders集合,并向此集合中添加16个已经实现的ModelBinderProvider

【2】初始化ValuesProviderFactories集合,并向此集合中添加4个ValueFactory

【3】以单例形式注入<IModelBinderFactory,ModelBinderFactory>

【4】添加其余模型元数据信息

接下来究竟是怎样将参数进行绑定的呢?首先咱们来定义一个IModelBinder接口,以下:

    public interface IModelBinder
    {
        Task BindModelAsync(ModelBindingContext bindingContext);
    }

那这个接口用来干吗呢,经过该接口中定义的方法名称咱们就知道,这就是最终咱们获得的ModelBinder,继而经过绑定上下文来绑定参数, 那么具体ModelBinder又怎么来呢?接下来定义IModelBinderProvder接口,以下:

    public interface IModelBinderProvider
    {
        IModelBinder GetBinder(ModelBinderProviderContext context);
    }

经过IModelBinderProvider接口中的ModelBinderProvderContext获取具体的ModelBinder,那么经过该接口中的方法GetBinder,咱们如何获取具体的ModelBinder呢,换而言之,咱们怎么去建立具体的ModelBinder呢,在添加MVC框架时咱们注入了ModelBinderFactory,此时ModelBinderFactory上场了,代码以下:

    public class ModelBinderFactory : IModelBinderFactory
    {
        public IModelBinder CreateBinder(ModelBinderFactoryContext context)
        {
            .....
        }
    }

那这个方法内部是如何实现的呢?其实很简单,也是在咱们添加MVC框架时,初始了16个具体ModelBinderProvider即List<IModelBinderProvider>,此时在这个方法里面去遍历这个集合,此时上述方法内部实现变成以下伪代码:

    public class ModelBinderFactory : IModelBinderFactory
    {
        public IModelBinder CreateBinder(ModelBinderFactoryContext context)
        {
            IModelBinderProvider[] _providers;
            IModelBinder result = null;

            for (var i = 0; i < _providers.Length; i++)
            {
                var provider = _providers[i];
                result = provider.GetBinder(providerContext);
                if (result != null)
                {
                    break;
                }
            }
        }
    }

至于它如何获得是哪个具体的ModelBinderProvider的,这就涉及到具体细节实现了,简单来讲根据绑定来源(Bindingsource)以及对应的元数据信息而获得,有想看源码细节的童鞋,可将以下图下载放大后去看。

 

自定义模型绑定

简单讲了下模型绑定原理,更多细节参看上述图查看,接下来咱们动手实践下,经过上述从总体上的讲解,咱们知道要想实现自定义模型绑定,咱们必须实现两个接口,实现IModelBinderProvider接口来实例化ModelBinder,实现IModelBinder接口来将参数进行绑定,最后呢,将咱们自定义实现的ModelBinderProvider添加到MVC框架选项中的ModelBinderProvider集合中去。首先咱们定义以下类:

    public class Employee
    {
        [Required]
        public decimal Salary { get; set; }
    }

咱们定义一个员工类,员工有薪水,若是公司遍及于全世界各地,因此对于各国的币种不同,假设是中国员工,则币种为人民币,假设一名中国员工薪水为10000人民币,咱们想要将【¥10000】绑定到Salary属性上,此时咱们经过Postman模拟请求看看。

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post(Employee customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
   

从如上图响应结果看出,此时默认的模型绑定系统将再也不适用,由于咱们加上了币种符号,因此此时咱们必须实现自定义的模型绑定,接下来咱们经过两种不一样的方式来实现自定义模型绑定。

货币符号自定义模型绑定方式(一)

咱们知道对于货币符号能够经过NumberStyles.Currency来指定,有了解过模型绑定原理的童鞋应该知道对于在.NET Core默认的ModelBinderProviders集合中并有DecimalModelBinderProvider,而是FloatingPointTypeModelBinderProvider来支持货币符号,而对应背后的具体实现是DecimalModelBinder,因此咱们大可借助于内置已经实现的DecimalModelBinder来实现自定义模型绑定,因此此时咱们仅仅只须要实现IModelBinderProvider接口,而IModelBinder接口对应的就是DecimalModelBinder内置已经实现,代码以下:

    public class RMBModelBinderProvider : IModelBinderProvider
    {
        private readonly ILoggerFactory _loggerFactory;
        public RMBModelBinderProvider(ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;

        }
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            //元数据为复杂类型直接跳过
            if (context.Metadata.IsComplexType)
            {
                return null;
            }

            //上下文中获取元数据类型非decimal类型直接跳过
            if (context.Metadata.ModelType != typeof(decimal))
            {
                return null;
            }
            
            return new DecimalModelBinder(NumberStyles.Currency, _loggerFactory);
        }
    }

接下来则是将咱们上述实现的RMBModelBinderProvider添加到ModelBinderProviders集合中去,这里须要注意,咱们知道最终获得具体的ModelBinder,内置是采用遍历集合而实现,一旦找到直接跳出,因此咱们将自定义实现的ModelBinderProvider强烈建议添加到集合中首位即便用Insert方法,而不是Add方法,以下:

            services.AddMvc(options =>
            {
                var loggerFactory = _serviceProvider.GetService<ILoggerFactory>();
                options.ModelBinderProviders.Insert(0, new RMBModelBinderProvider(loggerFactory));
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

货币符号自定义模型绑定方式(二)

 上述咱们是采用内置提供给咱们的DecimalModelBinder解决了货币符号问题,接下来咱们将经过特性来实现指定属性为货币符号,首先咱们定义以下接口解析属性值是否成功与否

    public interface IRMB
    {
        decimal RMB(string modelValue, out bool success);
    }

而后写一个以下RMB属性特性实现上述接口。

    [AttributeUsage(AttributeTargets.Property)]
    public class RMBAttribute : Attribute, IRMB
    {
        private static NumberStyles styles = NumberStyles.Currency;
        private CultureInfo CultureInfo = new CultureInfo("zh-cn");
        public decimal RMB(string modelValue, out bool success)
        {
            success = decimal.TryParse(modelValue, styles, CultureInfo, out var valueDecimal);
            return valueDecimal;
        }
    }

接下来咱们则是实现IModelBinderProvider接口,而后在此接口实现中去获取模型元数据类型中的属性是否实现了上述RMB特性,若是是,咱们则实例化ModelBinder并将RMB特性传递过去并获得其值,完整代码以下:

    public class RMBAttributeModelBinderProvider : IModelBinderProvider
    {
        private readonly ILoggerFactory _loggerFactory;
        public RMBAttributeModelBinderProvider(ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;

        }
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (!context.Metadata.IsComplexType)
            {
                var propertyName = context.Metadata.PropertyName;
                var propertyInfo = context.Metadata.ContainerMetadata.ModelType.GetProperty(propertyName);
                var attribute = propertyInfo.GetCustomAttributes(typeof(RMBAttribute), false).FirstOrDefault();
                if (attribute != null)
                {
                    return new RMBAttributeModelBinder(context.Metadata.ModelType, attribute as RMBAttribute, _loggerFactory);
                }
            }
            return null;
        }
    }
    public class RMBAttributeModelBinder : IModelBinder
    {
        IRMB rMB;
        private SimpleTypeModelBinder modelBinder;
        public RMBAttributeModelBinder(Type type, RMBAttribute attribute, ILoggerFactory loggerFactory)
        {
            rMB = attribute as IRMB;
            modelBinder = new SimpleTypeModelBinder(type, loggerFactory);
        }
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var modelName = bindingContext.ModelName;
            var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
                var valueString = valueProviderResult.FirstValue;
                var result = rMB.RMB(valueString, out bool success);
                if (success)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }
            return modelBinder.BindModelAsync(bindingContext);
        }
    }

最后则是添加到集合中去并在属性Salary上使用RMB特性,好比ModelBinderContext和ModelBinderProviderContext上下文是什么,无非就是模型元数据和一些参数罢了,这里就不一一解释了,本身调试还会了解的更多。以下:

     services.AddMvc(options =>
     {
         var loggerFactory = _serviceProvider.GetService<ILoggerFactory>();
         options.ModelBinderProviders.Insert(0, new RMBAttributeModelBinderProvider(loggerFactory));
     }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    public class Employee
    {
        [Required]
        [RMB]
        public decimal Salary { get; set; }
    }

混合绑定 

什么是混合绑定呢?就是将不一样的绑定模式混合在一块儿使用,有的人可说了,你这和没讲有什么区别,好了,我来举一个例子,好比咱们想将URL上的参数绑定到【FromBody】特性的参数上,前提是在URL上的参数在【FromBody】参数没有,好像仍是有点模糊,来,上代码。

    [Route("[controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost("{id:int}")]
        public IActionResult Post([FromBody]Employee customer)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }
    public class Employee
    {
        public int Id { get; set; }
        [Required]
        public decimal Salary { get; set; }
    }

如上示意图想必已经很明确了,在Body中咱们并未指定属性Id,可是咱们想要将路由中的id也就是4绑定到【FromBody】标识的参数Employee的属性Id,例子跟实际不是合理的,只是为了演示混合绑定,这点请忽略。问题已经阐述的很是明确了,不知您是否有了解决思路,既然是【FromBody】,内置已经实现的BodyModelBinder咱们依然要绑定,咱们只须要将路由中的值绑定到Employee对象中的id便可,来,咱们首先实现IModelBinderProvider接口,以下:

    public class MixModelBinderProvider : IModelBinderProvider
    {
        private readonly IList<IInputFormatter> _formatters;
        private readonly IHttpRequestStreamReaderFactory _readerFactory;

        public MixModelBinderProvider(IList<IInputFormatter> formatters,
            IHttpRequestStreamReaderFactory readerFactory)
        {
            _formatters = formatters;
            _readerFactory = readerFactory;
        }
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            //若是上下文为空,返回空
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            //若是元数据模型类型为Employee实例化MixModelBinder
            if (context.Metadata.ModelType == typeof(Employee))
            {
                return new MixModelBinder(_formatters, _readerFactory);
            }

            return null;
        }
    }

接下来则是实现IModelBinder接口诺,绑定【FromBody】特性请求参数,绑定属性Id。

    public class MixModelBinder : IModelBinder
    {
        private readonly BodyModelBinder bodyModelBinder;
        public MixModelBinder(IList<IInputFormatter> formatters,
            IHttpRequestStreamReaderFactory readerFactory)
        {
            //原来【FromBody】绑定参数依然要绑定,因此须要实例化BodyModelBinder
            bodyModelBinder = new BodyModelBinder(formatters, readerFactory);
        }
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            //绑定【FromBody】特性请求参数
            bodyModelBinder.BindModelAsync(bindingContext);

            if (!bindingContext.Result.IsModelSet)
            {
                return null;
            }

            //获取绑定对象
            var model = bindingContext.Result.Model;

            //绑定属性Id
            if (model is Employee employee)
            {
                var idString = bindingContext.ValueProvider.GetValue("id").FirstValue;
                if (int.TryParse(idString, out var id))
                {
                    employee.Id = id;
                }

                bindingContext.Result = ModelBindingResult.Success(model);
            }
            return Task.CompletedTask;
        }
    }

其实到这里咱们应该更加明白,【BindRequired】和【BindNever】特性只针对MVC模型绑定系统起做用,而对于【FromBody】特性的请求参数与Input Formatter有关,也就是与所用的序列化和反序列化框架有关。接下来咱们添加自定义实现的混合绑定类,以下:

            services.AddMvc(options =>
            {
                var readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
                options.ModelBinderProviders.Insert(0, new MixModelBinderProvider(options.InputFormatters, readerFactory));
            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

ApiController特性本质

.NET Core每一个版本的迭代更新都带给咱们最佳体验,直到.NET Core 2.0版本咱们知道MVC和Web Api将控制器合并也就是共同继承自Controller,可是呢,毕竟若是仅仅只是作Api开发因此彻底用不到MVC中Razor视图引擎,在.NET Core 2.1版本出现了ApiController特性, 同时出现了新的约定,也就是咱们控制器基类能够再也不是Controller而是ControllerBase,这是一个更加轻量的控制器基类,它不支持Razor视图引擎,ControllerBase控制器和ApiController特性结合使用,彻底演变成干净的Api控制器,因此到这里至少咱们了解到了.NET Core中的Controller和ControllerBase区别所在,Controller包含Razor视图引擎,而要是若是咱们仅仅只是作接口开发,则只需使用ControllerBase控制器结合ApiController特性便可。那么问题来了,ApiController特性的出现到底为咱们带来了什么呢?说的更加具体一点则是,它为咱们解决了什么问题呢?有的人说.NET Core中模型绑定系统或者ApiController特性的出现显得很复杂,其实否则,只是咱们不了解背后它所解决的应用场景,一旦用了以后,发现各类问题呈现出来了,仍是基础没有夯实,接下来咱们一块儿来看看。在讲解模型绑定系统时,咱们了解到对于参数的验证咱们须要经过代码 ModelState.IsValid 来判断,好比以下代码:

    public class Employee
    {
        public int Id { get; set; }

        [Required]
        public string Address { get; set; }
    }

    [Route("[Controller]")]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post([FromBody]Employee employee)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            return Ok();
        }
    }

当咱们请求参数中未包含Address属性时,此时经过上述模型验证未经过响应400。当控制器经过ApiController修饰时,此时内置会自动进行验证,也就是咱们没必要要在控制器方法中一遍遍写ModelState.IsValid方法,那么问题来了,内置究竟是如何进行自动验证的呢?首先会在.NET Core应用程序初始化时,注入以下接口以及具体实现。

services.TryAddEnumerable(
                ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());

那么针对ApiBehaviorApplicationModelProvider这个类到底作了什么呢?在此类构造函数中添加了6个约定,其余四个不是咱们研究的重点,有兴趣的童鞋能够私下去研究,咱们看看最重要的两个类: InvalidModelStateFilterConvention 和 InferParameterBindingInfoConvention ,而后在此类中有以下方法:

        public void OnProvidersExecuting(ApplicationModelProviderContext context)
        {
            foreach (var controller in context.Result.Controllers)
            {
                if (!IsApiController(controller))
                {
                    continue;
                }

                foreach (var action in controller.Actions)
                {
                    // Ensure ApiController is set up correctly
                    EnsureActionIsAttributeRouted(action);

                    foreach (var convention in ActionModelConventions)
                    {
                        convention.Apply(action);
                    }
                }
            }
        }

至于方法OnProviderExecuting方法在什么时候被调用咱们无需太多关心,这不是咱们研究的重点,咱们看到此方法中的具体就是作了判断咱们是否在控制器上经过ApiController进行了修饰,若是是,则遍历咱们默认添加的6个约定,好了接下来咱们首先来看InvalidModelStateFilterConvention约定,最终咱们会看到此类中添加了ModelStateInvalidFilterFactory,而后针对此类的实例化ModelStateInvalidFilter类,而后在此类中咱们看到实现了IAactionFilter接口,以下:

        public void OnActionExecuting(ActionExecutingContext context)
        {
            if (context.Result == null && !context.ModelState.IsValid)
            {
                _logger.ModelStateInvalidFilterExecuting();
                context.Result = _apiBehaviorOptions.InvalidModelStateResponseFactory(context);
            }
        }

到这里想必咱们明白了在控制器上经过ApiController修饰解决了第一个问题:在添加MVC框架时,会为咱们注入一个ModelStateInvalidFilter,并在OnActionExecuting方法期间运行,也就是执行控制器方法时运行,固然也是在进行模型绑定以后自动进行ModelState验证是否有效,未经过则当即响应400。到这里是否是就这样完事了呢,显然不是,为什么,咱们在控制器上经过ApiController来进行修饰,以下代码:

    [Route("[Controller]")]
    [ApiController]
    public class ModelBindController : Controller
    {
        [HttpPost]
        public IActionResult Post(Employee employee)
        {
            //if (!ModelState.IsValid)
            //{
            //    return BadRequest(ModelState);
            //}
            return Ok();
        }
    }

对比上述代码,咱们只是添加ApiController修饰控制器,同时咱们已了然内部会自动进行模型验证,因此咱们注释了模型验证代码,而后咱们也将【FromBody】特性去除,这时咱们进行请求,响应以下,符合咱们预期:

咱们仅仅只是将添加了ApiController修饰控制器,为什么咱们将【FromBody】特性去除则请求依然好使,并且结果也如咱们预期同样呢?答案则是:参数来源绑定推断,经过ApiController修饰控制器,会用到咱们上述提出的第二个约定类(参数绑定信息推断),到了这里是否是发现.NET Core为咱们作了好多,别着急,事情还未彻底水落石出,接下来咱们来看看,咱们以前所给出的URL参数绑定到字典上的例子。

    [Route("[Controller]")]
    [ApiController]
    public class ModelBindController : Controller
    {
        [HttpGet]
        public IActionResult Get(List<Dictionary<string, int>> pairs)
        {
            return Ok();
        }
    }

到这里咱们瞬间懵逼了,以前的请求如今却出现了415,也就是媒介类型不支持,咱们什么都没干,只是添加了ApiController修饰控制器而已,如此而已,问题出现了一百八十度的大转折,这个问题谁来解释解释下。咱们仍是看看参数绑定信息约定类的具体实现,一探究竟,以下:

            if (!options.SuppressInferBindingSourcesForParameters)
            {
                var convention = new InferParameterBindingInfoConvention(modelMetadataProvider)
                {
                    AllowInferringBindingSourceForCollectionTypesAsFromQuery = options.AllowInferringBindingSourceForCollectionTypesAsFromQuery,
                };

                ActionModelConventions.Add(convention);
            }

第一个判断则是是否启动参数来源绑定推断,告诉咱们这是可配置的,好了,咱们将其还原不启用,此时再请求回归如初,以下:

  services.Configure<ApiBehaviorOptions>(options=>
  {
     options.SuppressInferBindingSourcesForParameters = true;
  }).AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

 那么内置到底作了什么,其实上述答案已经给出了,咱们看看上述这行代码: options.AllowInferringBindingSourceForCollectionTypesAsFromQuery ,由于针对集合类型,.NET Core无从推断究竟是来自于Body仍是Query,因此呢,.NET Core再次给定了咱们一个可配置选项,咱们显式配置经过以下配置集合类型是来自于Query,此时请求则好使,不然将默认是Body,因此出现415。

services.Configure<ApiBehaviorOptions>(options=>
{
    options.AllowInferringBindingSourceForCollectionTypesAsFromQuery = true;
}).AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

好了,上述是针对集合类型进行可配置强制指定其来源,那么问题又来了,对于对象又该如何呢?首先咱们将上述显式配置集合类型来源于Query给禁用(禁不由用皆可),咱们看看下以下代码:

    [Route("[Controller]")]
    [ApiController]
    public class ModelBindController : Controller
    {
        [HttpGet("GetEmployee")]
        public IActionResult GetEmployee(Employee employee)
        {
            return Ok();
        }
    }

再次让咱们大跌眼镜,好像自从添加上了ApiController修饰控制器,各类问题呈现,咱们仍是看看.NET Core最终其推断,究竟是如何推断的呢?

        internal void InferParameterBindingSources(ActionModel action)
        {
            for (var i = 0; i < action.Parameters.Count; i++)
            {
                var parameter = action.Parameters[i];
                var bindingSource = parameter.BindingInfo?.BindingSource;
                if (bindingSource == null)
                {
                    bindingSource = InferBindingSourceForParameter(parameter);

                    parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
                    parameter.BindingInfo.BindingSource = bindingSource;
                }
            }
            ......
        }

        // Internal for unit testing.
        internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
        {
            if (IsComplexTypeParameter(parameter))
            {
                return BindingSource.Body;
            }

            if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
            {
                return BindingSource.Path;
            }

            return BindingSource.Query;
        }

        private bool ParameterExistsInAnyRoute(ActionModel action, string parameterName)
        {
            foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(action))
            {
                if (route == null)
                {
                    continue;
                }

                var parsedTemplate = TemplateParser.Parse(route.Template);
                if (parsedTemplate.GetParameter(parameterName) != null)
                {
                    return true;
                }
            }

            return false;
        }

        private bool IsComplexTypeParameter(ParameterModel parameter)
        {
            // No need for information from attributes on the parameter. Just use its type.
            var metadata = _modelMetadataProvider
                .GetMetadataForType(parameter.ParameterInfo.ParameterType);

            if (AllowInferringBindingSourceForCollectionTypesAsFromQuery && metadata.IsCollectionType)
            {
                return false;
            }

            return metadata.IsComplexType;
        }

经过上述代码咱们可知推断来源结果只有三种:Body、Path、Query。由于咱们未显式配置绑定来源,因此走参数推断来源,而后首先判断是否为复杂类型,判断条件是若是AllowInferringBindingSourceForCollectionTypesAsFromQuery配置为true,同时为集合类型说明来源为Body。此时咱们不管是否显式配置绑定集合类型是否来源于FromQuery,确定不知足这两个条件,接着执行metadate.IsComplexType,很显然Employee为复杂类型,咱们再次经过源码也可证实,在获取模型元数据时,经过 !TypeDescriptor.GetConverter(typeof(ModelType)).CanConvertFrom(typeof(string)) 判断是否为复杂类型,因此此时返回绑定来源于Body,因此出现415,问题已经分析的很清楚了,来,最终,咱们给ApiController特性本质下一个结论:

经过ApiController修饰控制器,内置实现了6个默认约定,其中最重要的两个约定则是,其一解决模型自动验证,其二则是当未配置绑定来源,执行参数推断来源,可是,可是,这个仅仅只是针对Body、Path、Query而言。

当控制器方法上参数为字典或集合时,若是请求参数来源于URL也就是查询字符串请显式配置AllowInferringBindingSourceForCollectionTypesAsFromQuery为true,不然会推断绑定来源为Body,从而响应415。

当控制器方法上参数为复杂类型时,若是请求参数来源于Body,能够无需显式配置绑定来源,若是参数来源为URL也就是查询字符串,请显式配置参数绑定来源【FromQuery】,若是参数来源于表单,请显式配置参数绑定来源【FromForm】,不然会推断绑定为Body,从而响应415。

总结

本文比较详细的阐述了.NET Core中的模型绑定系统、模型绑定原理、自定义模型绑定原理、混合绑定等等,其实还有一些基础内容我还未写出,后续有可能我接着研究并补上,.NET Core中强大的模型绑定支持以及灵活性控制都是.NET MVC/Web Api不可比拟的,虽然很基础可是又有多少人知道而且了解过这些呢,同时针对ApiController特性确实给咱们省去了没必要要的代码,可是带来的参数来源推断让咱们有点懵逼,若是不看源码,断不可知这些,我我的认为针对添加ApiController特性后的参数来源推断,没什么鸟用,强烈建议显式配置绑定来源,也就没必要记住上述结论了,本篇文章耗费我三天时间所写,修修补补,其中所带来的价值,一个字:值。

求职

本人离职中,如有合适机会但愿园友给引荐,引荐,推荐,推荐,私信我,深圳上班,谢谢。

相关文章
相关标签/搜索