你所不知道的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特性後的參數來源推斷,沒什麼鳥用,強烈建議顯式配置綁定來源,也就沒必要記住上述結論了,本篇文章耗費我三天時間所寫,修修補補,其中所帶來的價值,一個字:值。

求職

本人離職中,如有合適機會但願園友給引薦,引薦,推薦,推薦,私信我,深圳上班,謝謝。

相關文章
相關標籤/搜索