你所不知道的ASP.NET Core進階系列(三)

前言

一年多沒更新博客,上一次寫此係列還是四年前,雖遲但到,沒有承諾,主打隨性,所以不存在斷更,催更,哈哈,上一篇我們細究從請求到綁定詳細原理,本篇則是探討模型綁定細節,當一個問題產生到最終解決時,回過頭我們整體分析其產生背景以及設計思路纔能有所獲。好了,廢話不多說,我們開始模型綁定細節之旅。

問題產生

我們定義一個模型,然後進行查詢請求,當然,此時我們在後臺控制器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上的鍵名稱若和後臺接收參數變量名稱相同且不區分大小寫,框架以爲我們想要使用接收參數變量作爲前綴來綁定值,在相同等等條件下,對於集合亦是如此,除非我們自定義實現一套,否則我們萬不可將其定義爲相同名稱,如此會導致值綁定不上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章