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

Jeffcky發表於2023-11-20

前言

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

問題產生

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

相關文章