前言 一年多沒更新博客,上一次寫此系列還是四年前,雖遲但到,沒有承諾,主打隨性,所以不存在斷更,催更,哈哈,上一篇我們細究從請求到綁定詳細原理,本篇則是探討模型綁定細節,當一個問題產生到最終解決時,回過頭我們整體分析其產生背景以及設計思路才能有所獲。好了,廢話不多說,我們開始模型綁定細節之旅。 問題 ...
前言
一年多沒更新博客,上一次寫此系列還是四年前,雖遲但到,沒有承諾,主打隨性,所以不存在斷更,催更,哈哈,上一篇我們細究從請求到綁定詳細原理,本篇則是探討模型綁定細節,當一個問題產生到最終解決時,回過頭我們整體分析其產生背景以及設計思路才能有所獲。好了,廢話不多說,我們開始模型綁定細節之旅。
問題產生
我們定義一個模型,然後進行查詢請求,當然,此時我們在後臺控制器Action方法上推薦明確使用查詢特性即FromQuery接收,代碼如下
public class UserAddress { public string Code { get; set; } }
[ApiController] [Route("api/[controller]/[action]")] public class UserAddressController : ControllerBase { private readonly ILogger<UserAddressController> _logger; public UserAddressController(ILogger<UserAddressController> logger) { _logger = logger; } [HttpGet] public IActionResult Get([FromQuery] UserAddress address) { return Ok(address); } }
沒任何毛病,接下來我們在定義用戶地址類上增加一個屬性,如下所示
public class UserAddress { public string Code { get; set; } public string Address { get; set; } }
值綁定不上,這是神馬情況,這難道是官方的Bug嗎,我們用6.0和7.0都是如此,毫無疑問,利用.NET 8.0依然是此等結果,問題來了,請稍加思考大概是什麼原因,讓我們繼續往下分析
根因源碼分析
通過前後對比我們可以初步分析到原因可能是二方面之一或者二者結合,其一,對象屬性address和接收對象參數變數address不能相同(不區分大小寫),其二,接受對象參數變數address和URL上的鍵名稱address不能相同(不區分大小寫)。我們暫且只能分析到這個地方,當然,我們一試便知,至於根因是什麼,接下來我們只能去分析模型綁定源碼,說到分析源碼,可能有些童鞋不知從何開始,這裡給出我們從0開始分析其根因的整個過程,以供需要的童鞋做參考哈。僅我個人看法,除非精通,否則必會經歷一個過程,這是必然,所以不用懷疑任誰都不能立馬找到大概源碼在哪裡,我們註意關註點分析,別看著看著跑偏了,既然是模型綁定而且是查詢綁定,這是在瞭解基本原理或學習官網文檔有所印象的前提下,先看這裡
然後我們怎麼開始呢,我們直接自定義實現一個查詢字元串值綁定即將上述代碼拷貝一份出來,比如有些是有依賴的等等,將其修改去掉等等處理,還是我們強調的關註點,最後我們還要添加自定義實現
builder.Services.AddControllers(options => { options.ValueProviderFactories.Insert(0, new QueryStringValueProviderFactory()); });
我們看到實際上值都已正確獲取到,但實際上傳過來的鍵應該是屬性Code或者Address才對,同時我們發現在此實現中包含了一個是否包含首碼的方法,這好像貌似就是針對我們綁定的屬性加上方法上的參數變數即address,所以我們斷點一步步調試進入該方法具體實現
源碼調試現在還是方便了很多,我們來到綁定源頭即將ActionContext轉換為ModelBindingContext,也就是調用具體綁定實現之前即相關參數綁定准備前夕,我們看到賦值給了模型綁定上下文中的模型名稱即ModelName,我們猜測這就是增加的首碼,繼續往下調試實際調用的綁定者是哪一個,我們看到實際使用的複雜對象綁定,框架內置實現了十幾個綁定,ValueProvider只是其中後臺接收最簡單的參數類型或者直接接收請求上下文相關的預處理,大多都由ModelBinder來接收處理綁定到控制器方法上,調試源碼並不是那麼明朗,我們直接再自定義實現一個ComplexObjectModelBinderProvider,其具體ComplexObjectModelBinder有個方法BindPropertiesAsync,這是實際做相關處理的地方
/// <summary> /// Create a property model name with a prefix. /// </summary> /// <param name="prefix">The prefix to use.</param> /// <param name="propertyName">The property name.</param> /// <returns>The property model name.</returns> public static string CreatePropertyModelName(string? prefix, string? propertyName) { if (string.IsNullOrEmpty(prefix)) { return propertyName ?? string.Empty; } if (string.IsNullOrEmpty(propertyName)) { return prefix ?? string.Empty; } if (propertyName.StartsWith('[')) { // The propertyName might represent an indexer access, in which case combining // with a 'dot' would be invalid. This case occurs only when called from ValidationVisitor. return prefix + propertyName; } return prefix + "." + propertyName; }
好了,到了這裡我們只是知道了框架就是這麼做的處理導致值綁定不上,問題又來了,請思考框架這麼設計的初衷和思想是什麼呢。框架為我們考慮了諸多場景,我們刪除上述所有自定義實現, 框架以為我們想要達到如下綁定目的,但沒曾想劍走偏鋒,實際被我們鑽了個空子,正所謂你以為的是你以為的並不是我以為的,然後一臉懵波
舉一反三
還沒完,繼續開課,我們分析完整個前因後果後,我們終於明白了IValueProvider介面中所說的首碼具體指的是什麼意思,然後對於首碼匹配使用二分法演算法,同理,我們也不難看出,上述是對象綁定處理,在相同條件下,對於集合亦是如此。
總結
當進行查詢操作時請求URL上的鍵名稱若和後臺接收參數變數名稱相同且不區分大小寫,框架以為我們想要使用接收參數變數作為首碼來綁定值,在相同等等條件下,對於集合亦是如此,除非我們自定義實現一套,否則我們萬不可將其定義為相同名稱,如此會導致值綁定不上。
翻譯
搜索
複製
你所看到的並非事物本身,而是經過詮釋後所賦予的意義