ASP.NET Core 中的資料繫結

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

本文會說明何謂模型繫結、其運作方式,以及如何自訂其行為。

何謂模型繫結

控制器和 Razor Pages 使用來自 HTTP 要求的資料。 例如,路由資料可能會提供記錄索引鍵,而已張貼的表單欄位可能會提供模型屬性的值。 撰寫程式碼來擷取這些值的每一個並將它們從字串轉換成 .NET 類型,不但繁瑣又容易發生錯誤。 模型繫結會自動化此程序。 模型繫結系統:

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 在方法參數和公開的屬性中,將資料提供給控制器和 Razor Pages。
  • 將字串資料轉換成 .NET 類型。
  • 更新複雜類型的屬性。

範例

假設您有下列的動作方法:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

而應用程式收到具有此 URL 的要求:

https://contoso.com/api/pets/2?DogsOnly=true

在路由傳送系統選取該動作方法之後,模型繫結會逐步執行下列的步驟:

  • 尋找第一個參數 GetById,它是名為 id 的整數。
  • 查看 HTTP 要求中所有可用的來源,在路由資料中找到 id = "2"。
  • 將字串 "2" 轉換成整數 2。
  • 尋找 GetById 的下一個參數 (一個名為 dogsOnly 的布林值)。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 "true" 轉換成布林值 true

架構接著會呼叫 GetById 方法,針對 id 參數傳送 2、dogsOnly 參數傳送 true

在上例中,模型繫結目標都是簡單類型的方法參數。 目標也可能是複雜類型的屬性。 成功繫結每個屬性之後,就會針對該屬性進行模型驗證。 哪些資料繫結至模型,以及任何繫結或驗證錯誤的記錄,都會儲存在 ControllerBase.ModelStatePageModel.ModelState。 為了解此程序是否成功,應用程式會檢查 ModelState.IsValid 旗標。

目標

模型繫結會嘗試尋找下列幾種目標的值:

  • 要求路由目標的控制器動作方法參數。
  • 將要求路由傳送到其中的目標 Razor Pages 處理常式方法的參數。
  • 控制站的公用屬性或 PageModel 類別,如由屬性指定。

[BindProperty] 屬性

可以套用至控制器或 PageModel 類別的公開屬性,以使模型繫結以該屬性為目標:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

[BindProperties] 屬性

可以套用至控制器或 PageModel 類別,以告訴模型繫結以類別的所有公開屬性為目標:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

適用於 HTTP GET 要求的模型繫結

根據預設,屬性不會針對 HTTP GET 要求繫結。 一般而言,GET 要求只需要記錄識別碼參數。 此記錄識別碼用來查詢資料庫中的項目。 因此,不需要繫結保存模型實例的屬性。 在您要從 GET 要求將屬性繫結至資料的案例中,請將 SupportsGet 屬性設為 true

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

模型繫結簡單和複雜類型

模型繫結使用特定定義來描述其作業類型。 簡單的類型 是使用 TypeConverterTryParse 方法從單一字串轉換而來。 「複雜類型」是指從多個輸入值進行轉換。 架構會根據是否有 TypeConverterTryParse 來判斷差異。 建議您建立一個類型轉換器,或使用 TryParse 來進行 stringSomeType 的轉換 (這不需要外部資源或多個輸入)。

來源

根據預設,模型繫結會從下列 HTTP 要求的來源中,取得索引鍵/值組形式的資料:

  1. 表單欄位
  2. 要求本文 (針對具有 [ApiController] 屬性的控制器。)
  3. 路由資料
  4. 查詢字串參數
  5. 已上傳的檔案

對於每個目標參數或屬性,會依照前面清單中所指示的順序掃描來源。 但也有一些例外:

  • 路由資料和查詢字串值只用於簡單類型。
  • 上傳的檔案只繫結到實作 IFormFileIEnumerable<IFormFile> 的目標類型。

如果預設來源不正確,請使用下列其中一個屬性來指定來源:

這些屬性:

  • 會個別新增至模型屬性 (而不是模型類別),如下列範例所示:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • 會選擇性地接受建構函式中的模型名稱值。 如果屬性名稱與要求中的值不相符,則會提供此選項。 例如,要求中的值可能是名稱中有連字號的標頭,如下列範例所示:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 屬性

[FromBody] 屬性套用至參數,以從 HTTP 要求的本文中填入其屬性。 ASP.NET Core 執行階段會將讀取本文的責任委派給輸入格式器。 本文稍後會說明輸入格式器。

[FromBody] 套用至複雜的類型參數時,會忽略套用至其屬性的任何繫結來源屬性。 例如,下列的 Create 動作會指定其 pet 參數從本文填入:

public ActionResult<Pet> Create([FromBody] Pet pet)

類別 Pet 會指定其 Breed 屬性從查詢字串參數填入:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

在前述範例中:

  • 會忽略 [FromQuery] 屬性。
  • Breed 屬性不會從查詢字串參數填入。

輸入格式器只會讀取本文,而不了解繫結來源屬性。 如果在本文中找到適當的值,則會使用該值來填入 Breed 屬性。

請勿將 [FromBody] 套用至每個動作方法的多個參數。 一旦輸入格式器讀取要求串流後,即無法再次讀取以用來繫結其他 [FromBody] 參數。

其他來源

來源資料會由值提供者 提供給模型繫結系統。 您可以撰寫並註冊自訂值提供者,其從其他來源取得模型繫結資料。 例如,您可能需要來自 cookie 或工作階段狀態的資料。 若要從新來源取得資料:

  • 建立會實作 IValueProvider 的類別。
  • 建立會實作 IValueProviderFactory 的類別。
  • Program.cs 中註冊 Factory 類別。

以下這個範例包含了一個值提供者factory 的範例,用於從 cookie 中取得值。 請在 Program.cs 中註冊自訂值提供者 factory:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

上面的程式碼將自訂值提供者放在所有內建值提供者之後。 若要使其成為清單中的第一個,請呼叫 Insert(0, new CookieValueProviderFactory()) 而不是 Add

無模型屬性的來源

預設情況下,如果未找到模型屬性的值,則不會建立模型狀態錯誤。 屬性設定為 null 或預設值:

  • 可為 Null 的簡單類型會設為 null
  • 不可為 Null 的實值型別會設為 default(T)。 例如,參數 int id 設為 0。
  • 對於複雜類型,模型繫結會使用預設的建構函式來建立實例,而不需要設定屬性。
  • 陣列設為 Array.Empty<T>(),但 byte[] 陣列設為 null

如果在模型屬性的表單欄位中找不到任何內容時模型狀態應無效,則請使用 [BindRequired] 屬性。

請注意,此 [BindRequired] 行為適用於來自已張貼表單資料的模型繫結,不適合要求本文中的 JSON 或 XML 資料。 要求本文資料由輸入格式器處理。

類型轉換錯誤

如果找到來源但無法轉換為目標類型,則模型狀態將被標記為無效。 目標參數或屬性會設為 null 或預設值,如上一節中所述。

在具有 [ApiController] 屬性的 API 控制器中,無效的模型狀態會導致自動 HTTP 400 回應。

在 Razor 頁面中,重新顯示有錯誤訊息的頁面:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

當上面的程式碼重新顯示該頁面時,無效的輸入不會顯示在表單欄位中。 這是因為模型屬性已設為 null 或預設值。 無效的輸入確實會出現在錯誤訊息中。 如果您想要在表單欄位中重新顯示不正確的資料,請考慮讓模型屬性成為字串,以手動方式執行資料轉換。

如果您不希望類型轉換錯誤導致模型狀態錯誤,建議使用相同的策略。 在這種情況下,請將模型屬性設為字串。

簡單型別

如需簡單和複雜類型的說明,請參閱模型繫結簡單和複雜類型

模型繫結器可將來源字串轉換成的簡單型別包括:

IParsable<T>.TryParse 繫結

IParsable<TSelf>.TryParse API 支援繫結控制器動作參數值:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

以下的 DateRange 類別實作了 IParsable<TSelf> 以支援繫結日期範圍:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

上述 程式碼:

  • 將代表兩個日期的字串轉換成 DateRange 物件
  • 模型繫結器使用 IParsable<TSelf>.TryParse 方法來繫結 DateRange

下列控制器動作使用 DateRange 類別來繫結日期範圍:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

以下的 Locale 類別實作了 IParsable<TSelf> 以支援繫結至 CultureInfo

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

下列控制器動作使用 Locale 類別來繫結至 CultureInfo 字串:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

下列控制器動作使用 DateRangeLocale 類別來將日期範圍與 CultureInfo 繫結:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

GitHub 上的 API 範例應用程式顯示了上述的 API 控制器範例。

TryParse 繫結

TryParse API 支援繫結控制器動作參數值:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse 是建議的參數繫結方法,因為與 TryParse 不同,它不依賴反映。

下列的 DateRangeTP 類別實作了 TryParse

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

下列控制器動作使用 DateRangeTP 類別來繫結日期範圍:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

複雜類型

複雜類型必須具有公開的預設建構函式和公開的可寫入屬性才能進行繫結。 發生模型繫結時,類別會使用公用預設建構函式具現化。

對於複雜類型的每個屬性,模型繫結會在來源中尋找下列的名稱模式prefix.property_name。 如果找不到,它會只尋找沒有前置詞的 property_name。 是否使用前置詞的決定不是針對每個屬性來做出的。 例如,對於包含 ?Instructor.Id=100&Name=foo 的查詢,繫結到方法 OnGet(Instructor instructor),產生的類型 Instructor 的物件會包含:

  • 請將 Id 設定為 100
  • 請將 Name 設定為 null。 模型繫結需要 Instructor.Name,因為上面的查詢參數中使用了 Instructor.Id

若要繫結至參數,則前置詞是參數名稱。 若要繫結至 PageModel 公用屬性,則前置詞為公用屬性名稱。 某些屬性 (Attribute) 具有 Prefix 屬性 (Property),其可讓您覆寫參數或屬性名稱的預設使用方法。

例如,假設複雜類型是下列 Instructor 類別:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

前置詞 = 參數名稱

如果要繫結的模型是名為 instructorToUpdate 的參數:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

模型繫結從查看索引鍵 instructorToUpdate.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

前置詞 = 屬性名稱

如果要繫結的模型是控制器或 PageModel 類別名為 Instructor 的屬性:

[BindProperty]
public Instructor Instructor { get; set; }

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

自訂前置詞

如果要繫結的模型是名為 instructorToUpdate 的參數,且 Bind 屬性指定 Instructor 為前置詞:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

複雜類型目標的屬性

有數個內建屬性可用於控制複雜類型的模型繫結:

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們不會 影響處理已張貼 JSON 和 XML 要求本文的輸入格式器。 本文稍後會說明輸入格式器。

[Bind] 屬性

可以套用至類別或方法參數。 指定模型繫結應包含哪些模型屬性。 [Bind]不會 影響輸入格式器。

在下列範例中,當呼叫任何處理常式或動作方法時,只會繫結 Instructor 模型的指定屬性:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

在下列範例中,當呼叫 OnPost 方法時,只會繫結 Instructor 模型的指定屬性:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 屬性可用來防止「建立」案例中的大量指派。 它在編輯場景中效果不佳,因為排除的屬性會設為 null 或預設值,而不是保持不變。 建議使用檢視模型而非 [Bind] 屬性來防禦大量指派。 如需詳細資訊,請參閱關於過度發佈的安全性注意事項

[ModelBinder] 屬性

ModelBinderAttribute 可以套用至類型、屬性或參數。 它允許指定用來繫結特定實例或類型的模型繫結器類型。 例如:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder<MyInstructorModelBinder>] Instructor instructor)

[ModelBinder] 屬性也可以用來在它進行模型繫結時變更屬性或參數的名稱:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

[BindRequired] 屬性

如果模型的屬性不能發生繫結,則會造成模型繫結新增模型狀態錯誤。 以下為範例:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

另請參閱模型驗證中對 [Required] 屬性的討論。

[BindNever] 屬性

可以套用至屬性或類型。 避免模型繫結設定模型的屬性。 套用至類型時,模型繫結系統會排除類型所定義的所有屬性。 以下為範例:

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

    // ...
}

集合

針對簡單型別集合的目標,模型繫結會尋找符合 parameter_nameproperty_name 的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設要繫結的參數是名為 selectedCourses 的陣列:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • 表單或查詢字串資料可以是下列其中一種格式:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    如果名為 indexIndex 的參數或屬性與集合值相鄰,請避免繫結該參數或屬性。 模型系結會嘗試使用 index 作為集合的索引,這可能會導致不正確的繫結。 例如,請考慮下列動作:

    public IActionResult Post(string index, List<Product> products)
    

    在上面的程式碼中,index 查詢字串參數繫結到 index 方法參數,並且也用來繫結產品集合。 重新命名 index 參數或使用模型繫結屬性來設定繫結,可避免此問題:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • 下列格式只在表單資料中受到支援:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的陣列傳遞至 selectedCourses 參數:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    使用下標數字 (... [0] ... [1] ...) 的資料格式必須確定它們從零開始按順序編號。 下標編號中如有任何間距,則會忽略間隔後的所有項目。 例如,如果下標是 0 和 2,而不是 0 和 1,則忽略第二個項目。

字典

對於 Dictionary 目標,模型繫結會尋找與 parameter_nameproperty_name 相符的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是一個名為 selectedCoursesDictionary<int, string>

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • 已張貼的表單或查詢字串資料看起來會像下列其中一個範例:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的字典傳遞至 selectedCourses 參數:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

建構函式繫結和記錄類型

模型繫結需要複雜類型具有無參數的建構函式。 System.Text.JsonNewtonsoft.Json 型的輸入格式器都支援還原序列化沒有無參數建構函式的類別。

記錄類型是一個透過網路簡潔表示資料的好方法。 ASP.NET Core 支援使用單一建構函式來進行模型繫結和驗證記錄類型:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

驗證記錄類型時,執行階段會特別針對參數 (而不是屬性) 搜尋繫結和驗證中繼資料。

以下這個架構允許對記錄類型進行繫結和驗證:

public record Person([Required] string Name, [Range(0, 100)] int Age);

若要讓上述內容能夠運作,類型必須:

  • 為記錄類型。
  • 確切只有一個公開的建構函式。
  • 包含具有相同名稱和類型之屬性的參數。 名稱的大小寫不得不同。

不含無參數之建構函式的 POCO

沒有無參數建構函式的 POCO 無法繫結。

下列程式碼會產生一個例外狀況訊息,指出類型必須具有無參數的建構函式:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

「具有手動編寫建構函式的記錄類型」

看起來像主要構造函數的「具有手動編寫建構函式的記錄類型」可以運作

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

記錄類型、驗證和繫結中繼資料

對於記錄類型,會使用參數上的驗證和繫結中繼資料。 而屬性上的任何中繼資料會被忽略

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

驗證和中繼資料

驗證會使用參數上的中繼資料,但會使用屬性來讀取值。 在具有主要建構函式的一般案例中,兩者會相同。 不過,有一些方法可以克服它:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel 不會更新記錄類型上的參數

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

在本例中,MVC 不會嘗試再次繫結 Name。 不過,會允許更新 Age

模型繫結路由資料和查詢字串的全球化行為

ASP.NET Core 路由值提供者和查詢字串值提供者:

  • 將值視為不因文化特性而異。
  • 預期 URL 不因文化特性而異。

相反地,來自表單資料的值會進行區分文化特性的轉換。 這是有意為之的,以便 URL 可以跨不同地區設定來共用。

若要讓 ASP.NET Core 路由值提供者和查詢字串值提供者能夠進行區分文化特性的轉換:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

特殊資料類型

模型繫結可以處理某些特殊資料類型。

IFormFile 和 IFormFileCollection

HTTP 要求包含上傳的檔案。 也支援多個檔案的 IEnumerable<IFormFile>

CancellationToken

動作可以選擇性地將 CancellationToken 繫結為參數。 這會繫結 RequestAborted,當 HTTP 要求底層的連線被中止時,它會發出訊號。 動作可以使用此參數來取消作為控制器動作的一部分執行的長時間執行的非同步作業。

FormCollection

用來擷取已張貼表單資料中的所有值。

輸入格式器

要求本文中的資料可以是 JSON、XML 或一些其他格式。 模型繫結會使用設定處理特定內容類型的「輸入格式器」,來剖析此資料。 預設情況下,ASP.NET Core 包含 JSON 型的輸入格式器,用於處理 JSON 資料。 您可以為其他內容類型新增其他格式器。

ASP.NET Core 選取以 Consumes 屬性為基礎的輸入格式器。 若無任何屬性,則它會使用 Content-Type 標頭

使用內建的 XML 輸入格式器:

使用輸入格式器自訂模型繫結

輸入格式器會完全負責從要求本文讀取資料。 若要自訂此程序,請設定輸入格式器所使用的 API。 本節描述如何自訂 System.Text.Json 型的輸入格式器,以了解一個名為 ObjectId 的自訂類型。

請考慮下列模型,其中包含自訂 ObjectId 屬性:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

若要在使用 System.Text.Json 時自訂模型繫結程序,請建立一個衍生自 JsonConverter<T> 的類別:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

若要使用自訂轉換器,請將 JsonConverterAttribute 屬性套用至該類型。 在下列範例中,ObjectId 類型會設定為使用 ObjectIdConverter 作為其自訂轉換器:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

如需詳細資訊,請參閱如何撰寫自訂轉換器

排除模型繫結中的指定類型

模型繫結和驗證系統的行為是由 ModelMetadata 所驅動。 您可以將詳細資料提供者新增至 MvcOptions.ModelMetadataDetailsProviders,藉以自訂 ModelMetadata。 內建的詳細資料提供者可用於停用模型繫結或驗證所指定類型。

若要停用指定類型之所有模型的模型繫結,請在 Program.cs 中新增 ExcludeBindingMetadataProvider。 例如,若要對類型為 System.Version 的所有模型停用模型繫結:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

若要停用指定類型屬性的驗證,請在 Program.cs 中新增 SuppressChildValidationMetadataProvider。 例如,若要針對類型為 System.Guid 的屬性停用驗證:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

自訂模型繫結器

您可以撰寫自訂的模型繫結器來擴充模型繫結,並使用 [ModelBinder] 屬性以針對特定目標來選取它。 深入了解自訂模型繫結

手動模型繫結

使用 TryUpdateModelAsync 方法即可手動叫用模型繫結。 此方法已於 ControllerBasePageModel 類別中定義。 方法多載可讓您指定要使用的前置詞和值提供者。 如果模型繫結失敗,此方法會傳回 false。 以下為範例:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync 會使用值提供者從表單本文、查詢字串和路由資料取得資料。 TryUpdateModelAsync 通常:

  • 會與使用控制器和檢視表的 Razor Pages 和 MVC 應用程式一起使用,以防止過度發佈。
  • 不會與 Web API 一起使用 (除非從表單資料、查詢字串和路由資料中取用)。 取用 JSON 的 Web API 端點會使用輸入格式器來將要求本文還原序列化為物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

此屬性的名稱會遵循指定資料來源的模型繫結屬性的模式。 但它與繫結來自值提供者的資料無關。 它從相依性插入容器取得類型的執行個體。 其目的是只有在呼叫特定方法時,才在您需要服務時提供建構函式插入的替代項目。

如果該類型的實例未在相依性插入容器中註冊,則應用程式會在嘗試繫結參數時擲回例外狀況。 若要讓參數成為選擇性參數,請使用下列其中一種方法:

  • 將參數設為可為 Null。
  • 設定參數的預設值。

對於可為 Null 的參數,請確定在存取該參數之前它不是 null

其他資源

本文會說明何謂模型繫結、其運作方式,以及如何自訂其行為。

何謂模型繫結

控制器和 Razor Pages 使用來自 HTTP 要求的資料。 例如,路由資料可能會提供記錄索引鍵,而已張貼的表單欄位可能會提供模型屬性的值。 撰寫程式碼來擷取這些值的每一個並將它們從字串轉換成 .NET 類型,不但繁瑣又容易發生錯誤。 模型繫結會自動化此程序。 模型繫結系統:

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 在方法參數和公開的屬性中,將資料提供給控制器和 Razor Pages。
  • 將字串資料轉換成 .NET 類型。
  • 更新複雜類型的屬性。

範例

假設您有下列的動作方法:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

而應用程式收到具有此 URL 的要求:

https://contoso.com/api/pets/2?DogsOnly=true

在路由傳送系統選取該動作方法之後,模型繫結會逐步執行下列的步驟:

  • 尋找第一個參數 GetById,它是名為 id 的整數。
  • 查看 HTTP 要求中所有可用的來源,在路由資料中找到 id = "2"。
  • 將字串 "2" 轉換成整數 2。
  • 尋找 GetById 的下一個參數 (一個名為 dogsOnly 的布林值)。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 "true" 轉換成布林值 true

架構接著會呼叫 GetById 方法,針對 id 參數傳送 2、dogsOnly 參數傳送 true

在上例中,模型繫結目標都是簡單類型的方法參數。 目標也可能是複雜類型的屬性。 成功繫結每個屬性之後,就會針對該屬性進行模型驗證。 哪些資料繫結至模型,以及任何繫結或驗證錯誤的記錄,都會儲存在 ControllerBase.ModelStatePageModel.ModelState。 為了解此程序是否成功,應用程式會檢查 ModelState.IsValid 旗標。

目標

模型繫結會嘗試尋找下列幾種目標的值:

  • 要求路由目標的控制器動作方法參數。
  • 將要求路由傳送到其中的目標 Razor Pages 處理常式方法的參數。
  • 控制站的公用屬性或 PageModel 類別,如由屬性指定。

[BindProperty] 屬性

可以套用至控制器或 PageModel 類別的公開屬性,以使模型繫結以該屬性為目標:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

[BindProperties] 屬性

可以套用至控制器或 PageModel 類別,以告訴模型繫結以類別的所有公開屬性為目標:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

適用於 HTTP GET 要求的模型繫結

根據預設,屬性不會針對 HTTP GET 要求繫結。 一般而言,GET 要求只需要記錄識別碼參數。 此記錄識別碼用來查詢資料庫中的項目。 因此,不需要繫結保存模型實例的屬性。 在您要從 GET 要求將屬性繫結至資料的案例中,請將 SupportsGet 屬性設為 true

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

模型繫結簡單和複雜類型

模型繫結使用特定定義來描述其作業類型。 簡單的類型 是使用 TypeConverterTryParse 方法從單一字串轉換而來。 「複雜類型」是指從多個輸入值進行轉換。 架構會根據是否有 TypeConverterTryParse 來判斷差異。 建議您建立一個類型轉換器,或使用 TryParse 來進行 stringSomeType 的轉換 (這不需要外部資源或多個輸入)。

來源

根據預設,模型繫結會從下列 HTTP 要求的來源中,取得索引鍵/值組形式的資料:

  1. 表單欄位
  2. 要求本文 (針對具有 [ApiController] 屬性的控制器。)
  3. 路由資料
  4. 查詢字串參數
  5. 已上傳的檔案

對於每個目標參數或屬性,會依照前面清單中所指示的順序掃描來源。 但也有一些例外:

  • 路由資料和查詢字串值只用於簡單類型。
  • 上傳的檔案只繫結到實作 IFormFileIEnumerable<IFormFile> 的目標類型。

如果預設來源不正確,請使用下列其中一個屬性來指定來源:

這些屬性:

  • 會個別新增至模型屬性 (而不是模型類別),如下列範例所示:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • 會選擇性地接受建構函式中的模型名稱值。 如果屬性名稱與要求中的值不相符,則會提供此選項。 例如,要求中的值可能是名稱中有連字號的標頭,如下列範例所示:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 屬性

[FromBody] 屬性套用至參數,以從 HTTP 要求的本文中填入其屬性。 ASP.NET Core 執行階段會將讀取本文的責任委派給輸入格式器。 本文稍後會說明輸入格式器。

[FromBody] 套用至複雜的類型參數時,會忽略套用至其屬性的任何繫結來源屬性。 例如,下列的 Create 動作會指定其 pet 參數從本文填入:

public ActionResult<Pet> Create([FromBody] Pet pet)

類別 Pet 會指定其 Breed 屬性從查詢字串參數填入:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

在前述範例中:

  • 會忽略 [FromQuery] 屬性。
  • Breed 屬性不會從查詢字串參數填入。

輸入格式器只會讀取本文,而不了解繫結來源屬性。 如果在本文中找到適當的值,則會使用該值來填入 Breed 屬性。

請勿將 [FromBody] 套用至每個動作方法的多個參數。 一旦輸入格式器讀取要求串流後,即無法再次讀取以用來繫結其他 [FromBody] 參數。

其他來源

來源資料會由值提供者 提供給模型繫結系統。 您可以撰寫並註冊自訂值提供者,其從其他來源取得模型繫結資料。 例如,您可能需要來自 cookie 或工作階段狀態的資料。 若要從新來源取得資料:

  • 建立會實作 IValueProvider 的類別。
  • 建立會實作 IValueProviderFactory 的類別。
  • Program.cs 中註冊 Factory 類別。

以下這個範例包含了一個值提供者factory 的範例,用於從 cookie 中取得值。 請在 Program.cs 中註冊自訂值提供者 factory:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

上面的程式碼將自訂值提供者放在所有內建值提供者之後。 若要使其成為清單中的第一個,請呼叫 Insert(0, new CookieValueProviderFactory()) 而不是 Add

無模型屬性的來源

預設情況下,如果未找到模型屬性的值,則不會建立模型狀態錯誤。 屬性設定為 null 或預設值:

  • 可為 Null 的簡單類型會設為 null
  • 不可為 Null 的實值型別會設為 default(T)。 例如,參數 int id 設為 0。
  • 對於複雜類型,模型繫結會使用預設的建構函式來建立實例,而不需要設定屬性。
  • 陣列設為 Array.Empty<T>(),但 byte[] 陣列設為 null

如果在模型屬性的表單欄位中找不到任何內容時模型狀態應無效,則請使用 [BindRequired] 屬性。

請注意,此 [BindRequired] 行為適用於來自已張貼表單資料的模型繫結,不適合要求本文中的 JSON 或 XML 資料。 要求本文資料由輸入格式器處理。

類型轉換錯誤

如果找到來源但無法轉換為目標類型,則模型狀態將被標記為無效。 目標參數或屬性會設為 null 或預設值,如上一節中所述。

在具有 [ApiController] 屬性的 API 控制器中,無效的模型狀態會導致自動 HTTP 400 回應。

在 Razor 頁面中,重新顯示有錯誤訊息的頁面:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

當上面的程式碼重新顯示該頁面時,無效的輸入不會顯示在表單欄位中。 這是因為模型屬性已設為 null 或預設值。 無效的輸入確實會出現在錯誤訊息中。 如果您想要在表單欄位中重新顯示不正確的資料,請考慮讓模型屬性成為字串,以手動方式執行資料轉換。

如果您不希望類型轉換錯誤導致模型狀態錯誤,建議使用相同的策略。 在這種情況下,請將模型屬性設為字串。

簡單型別

如需簡單和複雜類型的說明,請參閱模型繫結簡單和複雜類型

模型繫結器可將來源字串轉換成的簡單型別包括:

IParsable<T>.TryParse 繫結

IParsable<TSelf>.TryParse API 支援繫結控制器動作參數值:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

以下的 DateRange 類別實作了 IParsable<TSelf> 以支援繫結日期範圍:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

上述 程式碼:

  • 將代表兩個日期的字串轉換成 DateRange 物件
  • 模型繫結器使用 IParsable<TSelf>.TryParse 方法來繫結 DateRange

下列控制器動作使用 DateRange 類別來繫結日期範圍:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

以下的 Locale 類別實作了 IParsable<TSelf> 以支援繫結至 CultureInfo

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

下列控制器動作使用 Locale 類別來繫結至 CultureInfo 字串:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

下列控制器動作使用 DateRangeLocale 類別來將日期範圍與 CultureInfo 繫結:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

GitHub 上的 API 範例應用程式顯示了上述的 API 控制器範例。

TryParse 繫結

TryParse API 支援繫結控制器動作參數值:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse 是建議的參數繫結方法,因為與 TryParse 不同,它不依賴反映。

下列的 DateRangeTP 類別實作了 TryParse

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

下列控制器動作使用 DateRangeTP 類別來繫結日期範圍:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

複雜類型

複雜類型必須具有公開的預設建構函式和公開的可寫入屬性才能進行繫結。 發生模型繫結時,類別會使用公用預設建構函式具現化。

對於複雜類型的每個屬性,模型繫結會在來源中尋找下列的名稱模式prefix.property_name。 如果找不到,它會只尋找沒有前置詞的 property_name。 是否使用前置詞的決定不是針對每個屬性來做出的。 例如,對於包含 ?Instructor.Id=100&Name=foo 的查詢,繫結到方法 OnGet(Instructor instructor),產生的類型 Instructor 的物件會包含:

  • 請將 Id 設定為 100
  • 請將 Name 設定為 null。 模型繫結需要 Instructor.Name,因為上面的查詢參數中使用了 Instructor.Id

若要繫結至參數,則前置詞是參數名稱。 若要繫結至 PageModel 公用屬性,則前置詞為公用屬性名稱。 某些屬性 (Attribute) 具有 Prefix 屬性 (Property),其可讓您覆寫參數或屬性名稱的預設使用方法。

例如,假設複雜類型是下列 Instructor 類別:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

前置詞 = 參數名稱

如果要繫結的模型是名為 instructorToUpdate 的參數:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

模型繫結從查看索引鍵 instructorToUpdate.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

前置詞 = 屬性名稱

如果要繫結的模型是控制器或 PageModel 類別名為 Instructor 的屬性:

[BindProperty]
public Instructor Instructor { get; set; }

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

自訂前置詞

如果要繫結的模型是名為 instructorToUpdate 的參數,且 Bind 屬性指定 Instructor 為前置詞:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

複雜類型目標的屬性

有數個內建屬性可用於控制複雜類型的模型繫結:

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們不會 影響處理已張貼 JSON 和 XML 要求本文的輸入格式器。 本文稍後會說明輸入格式器。

[Bind] 屬性

可以套用至類別或方法參數。 指定模型繫結應包含哪些模型屬性。 [Bind]不會 影響輸入格式器。

在下列範例中,當呼叫任何處理常式或動作方法時,只會繫結 Instructor 模型的指定屬性:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

在下列範例中,當呼叫 OnPost 方法時,只會繫結 Instructor 模型的指定屬性:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 屬性可用來防止「建立」案例中的大量指派。 它在編輯場景中效果不佳,因為排除的屬性會設為 null 或預設值,而不是保持不變。 建議使用檢視模型而非 [Bind] 屬性來防禦大量指派。 如需詳細資訊,請參閱關於過度發佈的安全性注意事項

[ModelBinder] 屬性

ModelBinderAttribute 可以套用至類型、屬性或參數。 它允許指定用來繫結特定實例或類型的模型繫結器類型。 例如:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

[ModelBinder] 屬性也可以用來在它進行模型繫結時變更屬性或參數的名稱:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

[BindRequired] 屬性

如果模型的屬性不能發生繫結,則會造成模型繫結新增模型狀態錯誤。 以下為範例:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

另請參閱模型驗證中對 [Required] 屬性的討論。

[BindNever] 屬性

可以套用至屬性或類型。 避免模型繫結設定模型的屬性。 套用至類型時,模型繫結系統會排除類型所定義的所有屬性。 以下為範例:

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

    // ...
}

集合

針對簡單型別集合的目標,模型繫結會尋找符合 parameter_nameproperty_name 的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設要繫結的參數是名為 selectedCourses 的陣列:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • 表單或查詢字串資料可以是下列其中一種格式:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    如果名為 indexIndex 的參數或屬性與集合值相鄰,請避免繫結該參數或屬性。 模型系結會嘗試使用 index 作為集合的索引,這可能會導致不正確的繫結。 例如,請考慮下列動作:

    public IActionResult Post(string index, List<Product> products)
    

    在上面的程式碼中,index 查詢字串參數繫結到 index 方法參數,並且也用來繫結產品集合。 重新命名 index 參數或使用模型繫結屬性來設定繫結,可避免此問題:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • 下列格式只在表單資料中受到支援:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的陣列傳遞至 selectedCourses 參數:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    使用下標數字 (... [0] ... [1] ...) 的資料格式必須確定它們從零開始按順序編號。 下標編號中如有任何間距,則會忽略間隔後的所有項目。 例如,如果下標是 0 和 2,而不是 0 和 1,則忽略第二個項目。

字典

對於 Dictionary 目標,模型繫結會尋找與 parameter_nameproperty_name 相符的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是一個名為 selectedCoursesDictionary<int, string>

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • 已張貼的表單或查詢字串資料看起來會像下列其中一個範例:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的字典傳遞至 selectedCourses 參數:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

建構函式繫結和記錄類型

模型繫結需要複雜類型具有無參數的建構函式。 System.Text.JsonNewtonsoft.Json 型的輸入格式器都支援還原序列化沒有無參數建構函式的類別。

記錄類型是一個透過網路簡潔表示資料的好方法。 ASP.NET Core 支援使用單一建構函式來進行模型繫結和驗證記錄類型:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

驗證記錄類型時,執行階段會特別針對參數 (而不是屬性) 搜尋繫結和驗證中繼資料。

以下這個架構允許對記錄類型進行繫結和驗證:

public record Person([Required] string Name, [Range(0, 100)] int Age);

若要讓上述內容能夠運作,類型必須:

  • 為記錄類型。
  • 確切只有一個公開的建構函式。
  • 包含具有相同名稱和類型之屬性的參數。 名稱的大小寫不得不同。

不含無參數之建構函式的 POCO

沒有無參數建構函式的 POCO 無法繫結。

下列程式碼會產生一個例外狀況訊息,指出類型必須具有無參數的建構函式:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

「具有手動編寫建構函式的記錄類型」

看起來像主要構造函數的「具有手動編寫建構函式的記錄類型」可以運作

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

記錄類型、驗證和繫結中繼資料

對於記錄類型,會使用參數上的驗證和繫結中繼資料。 而屬性上的任何中繼資料會被忽略

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

驗證和中繼資料

驗證會使用參數上的中繼資料,但會使用屬性來讀取值。 在具有主要建構函式的一般案例中,兩者會相同。 不過,有一些方法可以克服它:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel 不會更新記錄類型上的參數

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

在本例中,MVC 不會嘗試再次繫結 Name。 不過,會允許更新 Age

模型繫結路由資料和查詢字串的全球化行為

ASP.NET Core 路由值提供者和查詢字串值提供者:

  • 將值視為不因文化特性而異。
  • 預期 URL 不因文化特性而異。

相反地,來自表單資料的值會進行區分文化特性的轉換。 這是有意為之的,以便 URL 可以跨不同地區設定來共用。

若要讓 ASP.NET Core 路由值提供者和查詢字串值提供者能夠進行區分文化特性的轉換:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

特殊資料類型

模型繫結可以處理某些特殊資料類型。

IFormFile 和 IFormFileCollection

HTTP 要求包含上傳的檔案。 也支援多個檔案的 IEnumerable<IFormFile>

CancellationToken

動作可以選擇性地將 CancellationToken 繫結為參數。 這會繫結 RequestAborted,當 HTTP 要求底層的連線被中止時,它會發出訊號。 動作可以使用此參數來取消作為控制器動作的一部分執行的長時間執行的非同步作業。

FormCollection

用來擷取已張貼表單資料中的所有值。

輸入格式器

要求本文中的資料可以是 JSON、XML 或一些其他格式。 模型繫結會使用設定處理特定內容類型的「輸入格式器」,來剖析此資料。 預設情況下,ASP.NET Core 包含 JSON 型的輸入格式器,用於處理 JSON 資料。 您可以為其他內容類型新增其他格式器。

ASP.NET Core 選取以 Consumes 屬性為基礎的輸入格式器。 若無任何屬性,則它會使用 Content-Type 標頭

使用內建的 XML 輸入格式器:

使用輸入格式器自訂模型繫結

輸入格式器會完全負責從要求本文讀取資料。 若要自訂此程序,請設定輸入格式器所使用的 API。 本節描述如何自訂 System.Text.Json 型的輸入格式器,以了解一個名為 ObjectId 的自訂類型。

請考慮下列模型,其中包含自訂 ObjectId 屬性:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

若要在使用 System.Text.Json 時自訂模型繫結程序,請建立一個衍生自 JsonConverter<T> 的類別:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

若要使用自訂轉換器,請將 JsonConverterAttribute 屬性套用至該類型。 在下列範例中,ObjectId 類型會設定為使用 ObjectIdConverter 作為其自訂轉換器:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

如需詳細資訊,請參閱如何撰寫自訂轉換器

排除模型繫結中的指定類型

模型繫結和驗證系統的行為是由 ModelMetadata 所驅動。 您可以將詳細資料提供者新增至 MvcOptions.ModelMetadataDetailsProviders,藉以自訂 ModelMetadata。 內建的詳細資料提供者可用於停用模型繫結或驗證所指定類型。

若要停用指定類型之所有模型的模型繫結,請在 Program.cs 中新增 ExcludeBindingMetadataProvider。 例如,若要對類型為 System.Version 的所有模型停用模型繫結:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

若要停用指定類型屬性的驗證,請在 Program.cs 中新增 SuppressChildValidationMetadataProvider。 例如,若要針對類型為 System.Guid 的屬性停用驗證:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

自訂模型繫結器

您可以撰寫自訂的模型繫結器來擴充模型繫結,並使用 [ModelBinder] 屬性以針對特定目標來選取它。 深入了解自訂模型繫結

手動模型繫結

使用 TryUpdateModelAsync 方法即可手動叫用模型繫結。 此方法已於 ControllerBasePageModel 類別中定義。 方法多載可讓您指定要使用的前置詞和值提供者。 如果模型繫結失敗,此方法會傳回 false。 以下為範例:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync 會使用值提供者從表單本文、查詢字串和路由資料取得資料。 TryUpdateModelAsync 通常:

  • 會與使用控制器和檢視表的 Razor Pages 和 MVC 應用程式一起使用,以防止過度發佈。
  • 不會與 Web API 一起使用 (除非從表單資料、查詢字串和路由資料中取用)。 取用 JSON 的 Web API 端點會使用輸入格式器來將要求本文還原序列化為物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

此屬性的名稱會遵循指定資料來源的模型繫結屬性的模式。 但它與繫結來自值提供者的資料無關。 它從相依性插入容器取得類型的執行個體。 其目的是只有在呼叫特定方法時,才在您需要服務時提供建構函式插入的替代項目。

如果該類型的實例未在相依性插入容器中註冊,則應用程式會在嘗試繫結參數時擲回例外狀況。 若要讓參數成為選擇性參數,請使用下列其中一種方法:

  • 將參數設為可為 Null。
  • 設定參數的預設值。

對於可為 Null 的參數,請確定在存取該參數之前它不是 null

其他資源

本文會說明何謂模型繫結、其運作方式,以及如何自訂其行為。

何謂模型繫結

控制器和 Razor Pages 使用來自 HTTP 要求的資料。 例如,路由資料可能會提供記錄索引鍵,而已張貼的表單欄位可能會提供模型屬性的值。 撰寫程式碼來擷取這些值的每一個並將它們從字串轉換成 .NET 類型,不但繁瑣又容易發生錯誤。 模型繫結會自動化此程序。 模型繫結系統:

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 在方法參數和公開的屬性中,將資料提供給控制器和 Razor Pages。
  • 將字串資料轉換成 .NET 類型。
  • 更新複雜類型的屬性。

範例

假設您有下列的動作方法:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

而應用程式收到具有此 URL 的要求:

https://contoso.com/api/pets/2?DogsOnly=true

在路由傳送系統選取該動作方法之後,模型繫結會逐步執行下列的步驟:

  • 尋找第一個參數 GetById,它是名為 id 的整數。
  • 查看 HTTP 要求中所有可用的來源,在路由資料中找到 id = "2"。
  • 將字串 "2" 轉換成整數 2。
  • 尋找 GetById 的下一個參數 (一個名為 dogsOnly 的布林值)。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 "true" 轉換成布林值 true

架構接著會呼叫 GetById 方法,針對 id 參數傳送 2、dogsOnly 參數傳送 true

在上例中,模型繫結目標都是簡單型別的方法參數。 目標也可能是複雜類型的屬性。 成功繫結每個屬性之後,就會針對該屬性進行模型驗證。 哪些資料繫結至模型,以及任何繫結或驗證錯誤的記錄,都會儲存在 ControllerBase.ModelStatePageModel.ModelState。 為了解此程序是否成功,應用程式會檢查 ModelState.IsValid 旗標。

目標

模型繫結會嘗試尋找下列幾種目標的值:

  • 要求路由目標的控制器動作方法參數。
  • 將要求路由傳送到其中的目標 Razor Pages 處理常式方法的參數。
  • 控制站的公用屬性或 PageModel 類別,如由屬性指定。

[BindProperty] 屬性

可以套用至控制器或 PageModel 類別的公開屬性,以使模型繫結以該屬性為目標:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

[BindProperties] 屬性

可以套用至控制器或 PageModel 類別,以告訴模型繫結以類別的所有公開屬性為目標:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

適用於 HTTP GET 要求的模型繫結

根據預設,屬性不會針對 HTTP GET 要求繫結。 一般而言,GET 要求只需要記錄識別碼參數。 此記錄識別碼用來查詢資料庫中的項目。 因此,不需要繫結保存模型實例的屬性。 在您要從 GET 要求將屬性繫結至資料的案例中,請將 SupportsGet 屬性設為 true

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

來源

根據預設,模型繫結會從下列 HTTP 要求的來源中,取得索引鍵/值組形式的資料:

  1. 表單欄位
  2. 要求本文 (針對具有 [ApiController] 屬性的控制器。)
  3. 路由資料
  4. 查詢字串參數
  5. 已上傳的檔案

對於每個目標參數或屬性,會依照前面清單中所指示的順序掃描來源。 但也有一些例外:

  • 路由資料和查詢字串值只用於簡單型別。
  • 上傳的檔案只繫結到實作 IFormFileIEnumerable<IFormFile> 的目標類型。

如果預設來源不正確,請使用下列其中一個屬性來指定來源:

這些屬性:

  • 會個別新增至模型屬性 (而不是模型類別),如下列範例所示:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • 會選擇性地接受建構函式中的模型名稱值。 如果屬性名稱與要求中的值不相符,則會提供此選項。 例如,要求中的值可能是名稱中有連字號的標頭,如下列範例所示:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 屬性

[FromBody] 屬性套用至參數,以從 HTTP 要求的本文中填入其屬性。 ASP.NET Core 執行階段會將讀取本文的責任委派給輸入格式器。 本文稍後會說明輸入格式器。

[FromBody] 套用至複雜的類型參數時,會忽略套用至其屬性的任何繫結來源屬性。 例如,下列的 Create 動作會指定其 pet 參數從本文填入:

public ActionResult<Pet> Create([FromBody] Pet pet)

類別 Pet 會指定其 Breed 屬性從查詢字串參數填入:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

在前述範例中:

  • 會忽略 [FromQuery] 屬性。
  • Breed 屬性不會從查詢字串參數填入。

輸入格式器只會讀取本文,而不了解繫結來源屬性。 如果在本文中找到適當的值,則會使用該值來填入 Breed 屬性。

請勿將 [FromBody] 套用至每個動作方法的多個參數。 一旦輸入格式器讀取要求串流後,即無法再次讀取以用來繫結其他 [FromBody] 參數。

其他來源

來源資料會由值提供者 提供給模型繫結系統。 您可以撰寫並註冊自訂值提供者,其從其他來源取得模型繫結資料。 例如,您可能需要來自 cookie 或工作階段狀態的資料。 若要從新來源取得資料:

  • 建立會實作 IValueProvider 的類別。
  • 建立會實作 IValueProviderFactory 的類別。
  • Program.cs 中註冊 Factory 類別。

以下這個範例包含了一個值提供者factory 的範例,用於從 cookie 中取得值。 請在 Program.cs 中註冊自訂值提供者 factory:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

上面的程式碼將自訂值提供者放在所有內建值提供者之後。 若要使其成為清單中的第一個,請呼叫 Insert(0, new CookieValueProviderFactory()) 而不是 Add

無模型屬性的來源

預設情況下,如果未找到模型屬性的值,則不會建立模型狀態錯誤。 屬性設定為 null 或預設值:

  • 可為 Null 的簡單類型會設為 null
  • 不可為 Null 的實值型別會設為 default(T)。 例如,參數 int id 設為 0。
  • 對於複雜類型,模型繫結會使用預設的建構函式來建立實例,而不需要設定屬性。
  • 陣列設為 Array.Empty<T>(),但 byte[] 陣列設為 null

如果在模型屬性的表單欄位中找不到任何內容時模型狀態應無效,則請使用 [BindRequired] 屬性。

請注意,此 [BindRequired] 行為適用於來自已張貼表單資料的模型繫結,不適合要求本文中的 JSON 或 XML 資料。 要求本文資料由輸入格式器處理。

類型轉換錯誤

如果找到來源但無法轉換為目標類型,則模型狀態將被標記為無效。 目標參數或屬性會設為 null 或預設值,如上一節中所述。

在具有 [ApiController] 屬性的 API 控制器中,無效的模型狀態會導致自動 HTTP 400 回應。

在 Razor 頁面中,重新顯示有錯誤訊息的頁面:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

當上面的程式碼重新顯示該頁面時,無效的輸入不會顯示在表單欄位中。 這是因為模型屬性已設為 null 或預設值。 無效的輸入確實會出現在錯誤訊息中。 如果您想要在表單欄位中重新顯示不正確的資料,請考慮讓模型屬性成為字串,以手動方式執行資料轉換。

如果您不希望類型轉換錯誤導致模型狀態錯誤,建議使用相同的策略。 在這種情況下,請將模型屬性設為字串。

簡單型別

模型繫結器可將來源字串轉換成的簡單型別包括:

複雜類型

複雜類型必須具有公開的預設建構函式和公開的可寫入屬性才能進行繫結。 發生模型繫結時,類別會使用公用預設建構函式具現化。

對於複雜類型的每個屬性,模型繫結會在來源中尋找下列的名稱模式prefix.property_name。 如果找不到,它會只尋找沒有前置詞的 property_name。 是否使用前置詞的決定不是針對每個屬性來做出的。 例如,對於包含 ?Instructor.Id=100&Name=foo 的查詢,繫結到方法 OnGet(Instructor instructor),產生的類型 Instructor 的物件會包含:

  • 請將 Id 設定為 100
  • 請將 Name 設定為 null。 模型繫結需要 Instructor.Name,因為上面的查詢參數中使用了 Instructor.Id

若要繫結至參數,則前置詞是參數名稱。 若要繫結至 PageModel 公用屬性,則前置詞為公用屬性名稱。 某些屬性 (Attribute) 具有 Prefix 屬性 (Property),其可讓您覆寫參數或屬性名稱的預設使用方法。

例如,假設複雜類型是下列 Instructor 類別:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

前置詞 = 參數名稱

如果要繫結的模型是名為 instructorToUpdate 的參數:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

模型繫結從查看索引鍵 instructorToUpdate.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

前置詞 = 屬性名稱

如果要繫結的模型是控制器或 PageModel 類別名為 Instructor 的屬性:

[BindProperty]
public Instructor Instructor { get; set; }

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

自訂前置詞

如果要繫結的模型是名為 instructorToUpdate 的參數,且 Bind 屬性指定 Instructor 為前置詞:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

複雜類型目標的屬性

有數個內建屬性可用於控制複雜類型的模型繫結:

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們不會 影響處理已張貼 JSON 和 XML 要求本文的輸入格式器。 本文稍後會說明輸入格式器。

[Bind] 屬性

可以套用至類別或方法參數。 指定模型繫結應包含哪些模型屬性。 [Bind]不會 影響輸入格式器。

在下列範例中,當呼叫任何處理常式或動作方法時,只會繫結 Instructor 模型的指定屬性:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

在下列範例中,當呼叫 OnPost 方法時,只會繫結 Instructor 模型的指定屬性:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 屬性可用來防止「建立」案例中的大量指派。 它在編輯場景中效果不佳,因為排除的屬性會設為 null 或預設值,而不是保持不變。 建議使用檢視模型而非 [Bind] 屬性來防禦大量指派。 如需詳細資訊,請參閱關於過度發佈的安全性注意事項

[ModelBinder] 屬性

ModelBinderAttribute 可以套用至類型、屬性或參數。 它允許指定用來繫結特定實例或類型的模型繫結器類型。 例如:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

[ModelBinder] 屬性也可以用來在它進行模型繫結時變更屬性或參數的名稱:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

[BindRequired] 屬性

如果模型的屬性不能發生繫結,則會造成模型繫結新增模型狀態錯誤。 以下為範例:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

另請參閱模型驗證中對 [Required] 屬性的討論。

[BindNever] 屬性

可以套用至屬性或類型。 避免模型繫結設定模型的屬性。 套用至類型時,模型繫結系統會排除類型所定義的所有屬性。 以下為範例:

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

    // ...
}

集合

針對簡單型別集合的目標,模型繫結會尋找符合 parameter_nameproperty_name 的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設要繫結的參數是名為 selectedCourses 的陣列:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • 表單或查詢字串資料可以是下列其中一種格式:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    如果名為 indexIndex 的參數或屬性與集合值相鄰,請避免繫結該參數或屬性。 模型系結會嘗試使用 index 作為集合的索引,這可能會導致不正確的繫結。 例如,請考慮下列動作:

    public IActionResult Post(string index, List<Product> products)
    

    在上面的程式碼中,index 查詢字串參數繫結到 index 方法參數,並且也用來繫結產品集合。 重新命名 index 參數或使用模型繫結屬性來設定繫結,可避免此問題:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • 下列格式只在表單資料中受到支援:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的陣列傳遞至 selectedCourses 參數:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    使用下標數字 (... [0] ... [1] ...) 的資料格式必須確定它們從零開始按順序編號。 下標編號中如有任何間距,則會忽略間隔後的所有項目。 例如,如果下標是 0 和 2,而不是 0 和 1,則忽略第二個項目。

字典

對於 Dictionary 目標,模型繫結會尋找與 parameter_nameproperty_name 相符的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是一個名為 selectedCoursesDictionary<int, string>

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • 已張貼的表單或查詢字串資料看起來會像下列其中一個範例:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的字典傳遞至 selectedCourses 參數:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

建構函式繫結和記錄類型

模型繫結需要複雜類型具有無參數的建構函式。 System.Text.JsonNewtonsoft.Json 型的輸入格式器都支援還原序列化沒有無參數建構函式的類別。

記錄類型是一個透過網路簡潔表示資料的好方法。 ASP.NET Core 支援使用單一建構函式來進行模型繫結和驗證記錄類型:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

驗證記錄類型時,執行階段會特別針對參數 (而不是屬性) 搜尋繫結和驗證中繼資料。

以下這個架構允許對記錄類型進行繫結和驗證:

public record Person([Required] string Name, [Range(0, 100)] int Age);

若要讓上述內容能夠運作,類型必須:

  • 為記錄類型。
  • 確切只有一個公開的建構函式。
  • 包含具有相同名稱和類型之屬性的參數。 名稱的大小寫不得不同。

不含無參數之建構函式的 POCO

沒有無參數建構函式的 POCO 無法繫結。

下列程式碼會產生一個例外狀況訊息,指出類型必須具有無參數的建構函式:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

「具有手動編寫建構函式的記錄類型」

看起來像主要構造函數的「具有手動編寫建構函式的記錄類型」可以運作

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

記錄類型、驗證和繫結中繼資料

對於記錄類型,會使用參數上的驗證和繫結中繼資料。 而屬性上的任何中繼資料會被忽略

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

驗證和中繼資料

驗證會使用參數上的中繼資料,但會使用屬性來讀取值。 在具有主要建構函式的一般案例中,兩者會相同。 不過,有一些方法可以克服它:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel 不會更新記錄類型上的參數

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

在本例中,MVC 不會嘗試再次繫結 Name。 不過,會允許更新 Age

模型繫結路由資料和查詢字串的全球化行為

ASP.NET Core 路由值提供者和查詢字串值提供者:

  • 將值視為不因文化特性而異。
  • 預期 URL 不因文化特性而異。

相反地,來自表單資料的值會進行區分文化特性的轉換。 這是有意為之的,以便 URL 可以跨不同地區設定來共用。

若要讓 ASP.NET Core 路由值提供者和查詢字串值提供者能夠進行區分文化特性的轉換:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

特殊資料類型

模型繫結可以處理某些特殊資料類型。

IFormFile 和 IFormFileCollection

HTTP 要求包含上傳的檔案。 也支援多個檔案的 IEnumerable<IFormFile>

CancellationToken

動作可以選擇性地將 CancellationToken 繫結為參數。 這會繫結 RequestAborted,當 HTTP 要求底層的連線被中止時,它會發出訊號。 動作可以使用此參數來取消作為控制器動作的一部分執行的長時間執行的非同步作業。

FormCollection

用來擷取已張貼表單資料中的所有值。

輸入格式器

要求本文中的資料可以是 JSON、XML 或一些其他格式。 模型繫結會使用設定處理特定內容類型的「輸入格式器」,來剖析此資料。 預設情況下,ASP.NET Core 包含 JSON 型的輸入格式器,用於處理 JSON 資料。 您可以為其他內容類型新增其他格式器。

ASP.NET Core 選取以 Consumes 屬性為基礎的輸入格式器。 若無任何屬性,則它會使用 Content-Type 標頭

使用內建的 XML 輸入格式器:

使用輸入格式器自訂模型繫結

輸入格式器會完全負責從要求本文讀取資料。 若要自訂此程序,請設定輸入格式器所使用的 API。 本節描述如何自訂 System.Text.Json 型的輸入格式器,以了解一個名為 ObjectId 的自訂類型。

請考慮下列模型,其中包含自訂 ObjectId 屬性:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

若要在使用 System.Text.Json 時自訂模型繫結程序,請建立一個衍生自 JsonConverter<T> 的類別:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

若要使用自訂轉換器,請將 JsonConverterAttribute 屬性套用至該類型。 在下列範例中,ObjectId 類型會設定為使用 ObjectIdConverter 作為其自訂轉換器:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

如需詳細資訊,請參閱如何撰寫自訂轉換器

排除模型繫結中的指定類型

模型繫結和驗證系統的行為是由 ModelMetadata 所驅動。 您可以將詳細資料提供者新增至 MvcOptions.ModelMetadataDetailsProviders,藉以自訂 ModelMetadata。 內建的詳細資料提供者可用於停用模型繫結或驗證所指定類型。

若要停用指定類型之所有模型的模型繫結,請在 Program.cs 中新增 ExcludeBindingMetadataProvider。 例如,若要對類型為 System.Version 的所有模型停用模型繫結:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

若要停用指定類型屬性的驗證,請在 Program.cs 中新增 SuppressChildValidationMetadataProvider。 例如,若要針對類型為 System.Guid 的屬性停用驗證:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

自訂模型繫結器

您可以撰寫自訂的模型繫結器來擴充模型繫結,並使用 [ModelBinder] 屬性以針對特定目標來選取它。 深入了解自訂模型繫結

手動模型繫結

使用 TryUpdateModelAsync 方法即可手動叫用模型繫結。 此方法已於 ControllerBasePageModel 類別中定義。 方法多載可讓您指定要使用的前置詞和值提供者。 如果模型繫結失敗,此方法會傳回 false。 以下為範例:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync 會使用值提供者從表單本文、查詢字串和路由資料取得資料。 TryUpdateModelAsync 通常:

  • 會與使用控制器和檢視表的 Razor Pages 和 MVC 應用程式一起使用,以防止過度發佈。
  • 不會與 Web API 一起使用 (除非從表單資料、查詢字串和路由資料中取用)。 取用 JSON 的 Web API 端點會使用輸入格式器來將要求本文還原序列化為物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

此屬性的名稱會遵循指定資料來源的模型繫結屬性的模式。 但它與繫結來自值提供者的資料無關。 它從相依性插入容器取得類型的執行個體。 其目的是只有在呼叫特定方法時,才在您需要服務時提供建構函式插入的替代項目。

如果該類型的實例未在相依性插入容器中註冊,則應用程式會在嘗試繫結參數時擲回例外狀況。 若要讓參數成為選擇性參數,請使用下列其中一種方法:

  • 將參數設為可為 Null。
  • 設定參數的預設值。

對於可為 Null 的參數,請確定在存取該參數之前它不是 null

其他資源

本文會說明何謂模型繫結、其運作方式,以及如何自訂其行為。

檢視或下載範例程式碼 (如何下載)。

何謂模型繫結

控制器和 Razor Pages 使用來自 HTTP 要求的資料。 例如,路由資料可能會提供記錄索引鍵,而已張貼的表單欄位可能會提供模型屬性的值。 撰寫程式碼來擷取這些值的每一個並將它們從字串轉換成 .NET 類型,不但繁瑣又容易發生錯誤。 模型繫結會自動化此程序。 模型繫結系統:

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 在方法參數和公開的屬性中,將資料提供給控制器和 Razor Pages。
  • 將字串資料轉換成 .NET 類型。
  • 更新複雜類型的屬性。

範例

假設您有下列的動作方法:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

而應用程式收到具有此 URL 的要求:

http://contoso.com/api/pets/2?DogsOnly=true

在路由傳送系統選取該動作方法之後,模型繫結會逐步執行下列的步驟:

  • 尋找第一個參數 GetById,它是名為 id 的整數。
  • 查看 HTTP 要求中所有可用的來源,在路由資料中找到 id = "2"。
  • 將字串 "2" 轉換成整數 2。
  • 尋找 GetById 的下一個參數 (一個名為 dogsOnly 的布林值)。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 "true" 轉換成布林值 true

架構接著會呼叫 GetById 方法,針對 id 參數傳送 2、dogsOnly 參數傳送 true

在上例中,模型繫結目標都是簡單型別的方法參數。 目標也可能是複雜類型的屬性。 成功繫結每個屬性之後,就會針對該屬性進行模型驗證。 哪些資料繫結至模型,以及任何繫結或驗證錯誤的記錄,都會儲存在 ControllerBase.ModelStatePageModel.ModelState。 為了解此程序是否成功,應用程式會檢查 ModelState.IsValid 旗標。

目標

模型繫結會嘗試尋找下列幾種目標的值:

  • 要求路由目標的控制器動作方法參數。
  • 將要求路由傳送到其中的目標 Razor Pages 處理常式方法的參數。
  • 控制站的公用屬性或 PageModel 類別,如由屬性指定。

[BindProperty] 屬性

可以套用至控制器或 PageModel 類別的公開屬性,以使模型繫結以該屬性為目標:

public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }

[BindProperties] 屬性

在 ASP.NET Core 2.1 和更新版本中提供。 可以套用至控制器或 PageModel 類別,以告訴模型繫結以類別的所有公開屬性為目標:

[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }

適用於 HTTP GET 要求的模型繫結

根據預設,屬性不會針對 HTTP GET 要求繫結。 一般而言,GET 要求只需要記錄識別碼參數。 此記錄識別碼用來查詢資料庫中的項目。 因此,不需要繫結保存模型實例的屬性。 在您要從 GET 要求將屬性繫結至資料的案例中,請將 SupportsGet 屬性設為 true

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }

來源

根據預設,模型繫結會從下列 HTTP 要求的來源中,取得索引鍵/值組形式的資料:

  1. 表單欄位
  2. 要求本文 (針對具有 [ApiController] 屬性的控制器。)
  3. 路由資料
  4. 查詢字串參數
  5. 已上傳的檔案

對於每個目標參數或屬性,會依照前面清單中所指示的順序掃描來源。 但也有一些例外:

  • 路由資料和查詢字串值只用於簡單型別。
  • 上傳的檔案只繫結到實作 IFormFileIEnumerable<IFormFile> 的目標類型。

如果預設來源不正確,請使用下列其中一個屬性來指定來源:

這些屬性:

  • 會個別新增至模型屬性 (不是模型類別),如下列範例所示:

    public class Instructor
    {
        public int ID { get; set; }
    
        [FromQuery(Name = "Note")]
        public string NoteFromQueryString { get; set; }
    
  • 會選擇性地接受建構函式中的模型名稱值。 如果屬性名稱與要求中的值不相符,則會提供此選項。 例如,要求中的值可能是名稱中有連字號的標頭,如下列範例所示:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

[FromBody] 屬性

[FromBody] 屬性套用至參數,以從 HTTP 要求的本文中填入其屬性。 ASP.NET Core 執行階段會將讀取本文的責任委派給輸入格式器。 本文稍後會說明輸入格式器。

[FromBody] 套用至複雜的類型參數時,會忽略套用至其屬性的任何繫結來源屬性。 例如,下列的 Create 動作會指定其 pet 參數從本文填入:

public ActionResult<Pet> Create([FromBody] Pet pet)

類別 Pet 會指定其 Breed 屬性從查詢字串參數填入:

public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}

在前述範例中:

  • 會忽略 [FromQuery] 屬性。
  • Breed 屬性不會從查詢字串參數填入。

輸入格式器只會讀取本文,而不了解繫結來源屬性。 如果在本文中找到適當的值,則會使用該值來填入 Breed 屬性。

請勿將 [FromBody] 套用至每個動作方法的多個參數。 一旦輸入格式器讀取要求串流後,即無法再次讀取以用來繫結其他 [FromBody] 參數。

其他來源

來源資料會由值提供者 提供給模型繫結系統。 您可以撰寫並註冊自訂值提供者,其從其他來源取得模型繫結資料。 例如,您可能需要來自 cookie 或工作階段狀態的資料。 若要從新來源取得資料:

  • 建立會實作 IValueProvider 的類別。
  • 建立會實作 IValueProviderFactory 的類別。
  • Startup.ConfigureServices 中註冊 Factory 類別。

範例應用程式包含值提供者和從 cookie 取得值的 actory 範例。 以下是 Startup.ConfigureServices 中的註冊碼:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

顯示的程式碼會將自訂值提供者放在所有內建值提供者的後面。 若要使其成為清單中的第一個,請呼叫 Insert(0, new CookieValueProviderFactory()) 而不是 Add

無模型屬性的來源

預設情況下,如果未找到模型屬性的值,則不會建立模型狀態錯誤。 屬性設定為 null 或預設值:

  • 可為 Null 的簡單類型會設為 null
  • 不可為 Null 的實值型別會設為 default(T)。 例如,參數 int id 設為 0。
  • 對於複雜類型,模型繫結會使用預設的建構函式來建立實例,而不需要設定屬性。
  • 陣列設為 Array.Empty<T>(),但 byte[] 陣列設為 null

如果在模型屬性的表單欄位中找不到任何內容時模型狀態應無效,則請使用 [BindRequired] 屬性。

請注意,此 [BindRequired] 行為適用於來自已張貼表單資料的模型繫結,不適合要求本文中的 JSON 或 XML 資料。 要求本文資料由輸入格式器處理。

類型轉換錯誤

如果找到來源但無法轉換為目標類型,則模型狀態將被標記為無效。 目標參數或屬性會設為 null 或預設值,如上一節中所述。

在具有 [ApiController] 屬性的 API 控制器中,無效的模型狀態會導致自動 HTTP 400 回應。

在 Razor 頁面中,重新顯示有錯誤訊息的頁面:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}

用戶端驗證會擷取大多數本應提交到 Razor Pages 表單的錯誤資料。 此驗證會讓您更難觸發上述醒目提示的程式碼。 範例應用程式包含一個 [以無效日期提交] 按鈕 (該按鈕會將錯誤資料放入 [雇用日期] 欄位中並提交表單)。 此按鈕會顯示當資料轉換錯誤發生時,重新顯示頁面的程式碼如何運作。

當上面的程式碼重新顯示該頁面時,無效的輸入不會顯示在表單欄位中。 這是因為模型屬性已設為 null 或預設值。 無效的輸入確實會出現在錯誤訊息中。 但是,如果您想要在表單欄位中重新顯示不正確的資料,請考慮讓模型屬性成為字串,以手動方式執行資料轉換。

如果您不希望類型轉換錯誤導致模型狀態錯誤,建議使用相同的策略。 在這種情況下,請將模型屬性設為字串。

簡單型別

模型繫結器可將來源字串轉換成的簡單型別包括:

複雜類型

複雜類型必須具有公開的預設建構函式和公開的可寫入屬性才能進行繫結。 發生模型繫結時,類別會使用公用預設建構函式具現化。

針對複雜類型的每個屬性,模型繫結會查看名稱模式 prefix.property_name 的來源。 如果找不到,它會只尋找沒有前置詞的 property_name

若要繫結至參數,則前置詞是參數名稱。 若要繫結至 PageModel 公用屬性,則前置詞為公用屬性名稱。 某些屬性 (Attribute) 具有 Prefix 屬性 (Property),其可讓您覆寫參數或屬性名稱的預設使用方法。

例如,假設複雜類型是下列 Instructor 類別:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

前置詞 = 參數名稱

如果要繫結的模型是名為 instructorToUpdate 的參數:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

模型繫結從查看索引鍵 instructorToUpdate.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

前置詞 = 屬性名稱

如果要繫結的模型是控制器或 PageModel 類別名為 Instructor 的屬性:

[BindProperty]
public Instructor Instructor { get; set; }

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

自訂前置詞

如果要繫結的模型是名為 instructorToUpdate 的參數,且 Bind 屬性指定 Instructor 為前置詞:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

模型繫結從查看索引鍵 Instructor.ID 的來源開始。 如果找不到,它會尋找不含前置詞的 ID

複雜類型目標的屬性

有數個內建屬性可用於控制複雜類型的模型繫結:

  • [Bind]
  • [BindRequired]
  • [BindNever]

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們不會 影響處理已張貼 JSON 和 XML 要求本文的輸入格式器。 本文稍後會說明輸入格式器。

[Bind] 屬性

可以套用至類別或方法參數。 指定模型繫結應包含哪些模型屬性。 [Bind]不會 影響輸入格式器。

在下列範例中,當呼叫任何處理常式或動作方法時,只會繫結 Instructor 模型的指定屬性:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

在下列範例中,當呼叫 OnPost 方法時,只會繫結 Instructor 模型的指定屬性:

[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

[Bind] 屬性可用來防止「建立」案例中的大量指派。 它在編輯場景中效果不佳,因為排除的屬性會設為 null 或預設值,而不是保持不變。 建議使用檢視模型而非 [Bind] 屬性來防禦大量指派。 如需詳細資訊,請參閱關於過度發佈的安全性注意事項

[ModelBinder] 屬性

ModelBinderAttribute 可以套用至類型、屬性或參數。 它允許指定用來繫結特定實例或類型的模型繫結器類型。 例如:

[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

[ModelBinder] 屬性也可以用來在它進行模型繫結時變更屬性或參數的名稱:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}

[BindRequired] 屬性

只能套用至模型屬性,不能套用到方法參數。 如果模型的屬性不能發生繫結,則會造成模型繫結新增模型狀態錯誤。 以下為範例:

public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }

另請參閱模型驗證中對 [Required] 屬性的討論。

[BindNever] 屬性

只能套用至模型屬性,不能套用到方法參數。 避免模型繫結設定模型的屬性。 以下為範例:

public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }

集合

針對簡單型別集合的目標,模型繫結會尋找符合 parameter_nameproperty_name 的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設要繫結的參數是名為 selectedCourses 的陣列:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • 表單或查詢字串資料可以是下列其中一種格式:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    如果名為 indexIndex 的參數或屬性與集合值相鄰,請避免繫結該參數或屬性。 模型系結會嘗試使用 index 作為集合的索引,這可能會導致不正確的繫結。 例如,請考慮下列動作:

    public IActionResult Post(string index, List<Product> products)
    

    在上面的程式碼中,index 查詢字串參數繫結到 index 方法參數,並且也用來繫結產品集合。 重新命名 index 參數或使用模型繫結屬性來設定繫結,可避免此問題:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • 下列格式只在表單資料中受到支援:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的陣列傳遞至 selectedCourses 參數:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    使用下標數字 (... [0] ... [1] ...) 的資料格式必須確定它們從零開始按順序編號。 下標編號中如有任何間距,則會忽略間隔後的所有項目。 例如,如果下標是 0 和 2,而不是 0 和 1,則忽略第二個項目。

字典

對於 Dictionary 目標,模型繫結會尋找與 parameter_nameproperty_name 相符的項目。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是一個名為 selectedCoursesDictionary<int, string>

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • 已張貼的表單或查詢字串資料看起來會像下列其中一個範例:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • 針對前列所有範例格式,模型繫結會將有兩個項目的字典傳遞至 selectedCourses 參數:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

建構函式繫結和記錄類型

模型繫結需要複雜類型具有無參數的建構函式。 System.Text.JsonNewtonsoft.Json 型的輸入格式器都支援還原序列化沒有無參數建構函式的類別。

C# 9 引入了記錄類型 (這是一個透過網路簡潔表示資料的好方法)。 ASP.NET Core 新增了一個使用單一建構函式來進行模型繫結和驗證記錄類型的支援:

public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}

Person/Index.cshtml

@model Person

Name: <input asp-for="Name" />
...
Age: <input asp-for="Age" />

驗證記錄類型時,執行階段會特別針對參數 (而不是屬性) 搜尋繫結和驗證中繼資料。

以下這個架構允許對記錄類型進行繫結和驗證:

public record Person([Required] string Name, [Range(0, 100)] int Age);

若要讓上述內容能夠運作,類型必須:

  • 為記錄類型。
  • 確切只有一個公開的建構函式。
  • 包含具有相同名稱和類型之屬性的參數。 名稱的大小寫不得不同。

不含無參數之建構函式的 POCO

沒有無參數建構函式的 POCO 無法繫結。

下列程式碼會產生一個例外狀況訊息,指出類型必須具有無參數的建構函式:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}

「具有手動編寫建構函式的記錄類型」

看起來像主要構造函數的「具有手動編寫建構函式的記錄類型」可以運作

public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}

記錄類型、驗證和繫結中繼資料

對於記錄類型,會使用參數上的驗證和繫結中繼資料。 而屬性上的任何中繼資料會被忽略

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

驗證和中繼資料

驗證會使用參數上的中繼資料,但會使用屬性來讀取值。 在具有主要建構函式的一般案例中,兩者會相同。 不過,有一些方法可以克服它:

public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}

TryUpdateModel 不會更新記錄類型上的參數

public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

在本例中,MVC 不會嘗試再次繫結 Name。 不過,會允許更新 Age

模型繫結路由資料和查詢字串的全球化行為

ASP.NET Core 路由值提供者和查詢字串值提供者:

  • 將值視為不因文化特性而異。
  • 預期 URL 不因文化特性而異。

相反地,來自表單資料的值會進行區分文化特性的轉換。 這是有意為之的,以便 URL 可以跨不同地區設定來共用。

若要讓 ASP.NET Core 路由值提供者和查詢字串值提供者能夠進行區分文化特性的轉換:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
public class CulturedQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query != null && query.Count > 0)
        {
            var valueProvider = new QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}

特殊資料類型

模型繫結可以處理某些特殊資料類型。

IFormFile 和 IFormFileCollection

HTTP 要求包含上傳的檔案。 也支援多個檔案的 IEnumerable<IFormFile>

CancellationToken

動作可以選擇性地將 CancellationToken 繫結為參數。 這會繫結 RequestAborted,當 HTTP 要求底層的連線被中止時,它會發出訊號。 動作可以使用此參數來取消作為控制器動作的一部分執行的長時間執行的非同步作業。

FormCollection

用來擷取已張貼表單資料中的所有值。

輸入格式器

要求本文中的資料可以是 JSON、XML 或一些其他格式。 模型繫結會使用設定處理特定內容類型的「輸入格式器」,來剖析此資料。 預設情況下,ASP.NET Core 包含 JSON 型的輸入格式器,用於處理 JSON 資料。 您可以為其他內容類型新增其他格式器。

ASP.NET Core 選取以 Consumes 屬性為基礎的輸入格式器。 若無任何屬性,則它會使用 Content-Type 標頭

使用內建的 XML 輸入格式器:

  • 安裝 Microsoft.AspNetCore.Mvc.Formatters.Xml NuGet 套件。

  • Startup.ConfigureServices 中,呼叫 AddXmlSerializerFormattersAddXmlDataContractSerializerFormatters

    services.AddRazorPages()
        .AddMvcOptions(options =>
    {
        options.ValueProviderFactories.Add(new CookieValueProviderFactory());
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(System.Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
    })
    .AddXmlSerializerFormatters();
    
  • Consumes 屬性套用至要求本文應為 XML 的控制器類別或動作方法。

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    如需詳細資訊,請參閱 XML 序列化簡介

使用輸入格式器自訂模型繫結

輸入格式器會完全負責從要求本文讀取資料。 若要自訂此程序,請設定輸入格式器所使用的 API。 本節描述如何自訂 System.Text.Json 型的輸入格式器,以了解一個名為 ObjectId 的自訂類型。

請考慮下列模型,其中包含一個名為 Id 的自訂 ObjectId 屬性:

public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}

若要在使用 System.Text.Json 時自訂模型繫結程序,請建立一個衍生自 JsonConverter<T> 的類別:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}

若要使用自訂轉換器,請將 JsonConverterAttribute 屬性套用至該類型。 在下列範例中,ObjectId 類型會設定為使用 ObjectIdConverter 作為其自訂轉換器:

[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}

如需詳細資訊,請參閱如何撰寫自訂轉換器

排除模型繫結中的指定類型

模型繫結和驗證系統的行為是由 ModelMetadata 所驅動。 您可以將詳細資料提供者新增至 MvcOptions.ModelMetadataDetailsProviders,藉以自訂 ModelMetadata。 內建的詳細資料提供者可用於停用模型繫結或驗證所指定類型。

若要停用指定類型之所有模型的模型繫結,請在 Startup.ConfigureServices 中新增 ExcludeBindingMetadataProvider。 例如,若要對類型為 System.Version 的所有模型停用模型繫結:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

若要停用指定類型屬性的驗證,請在 Startup.ConfigureServices 中新增 SuppressChildValidationMetadataProvider。 例如,若要針對類型為 System.Guid 的屬性停用驗證:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

自訂模型繫結器

您可以撰寫自訂的模型繫結器來擴充模型繫結,並使用 [ModelBinder] 屬性以針對特定目標來選取它。 深入了解自訂模型繫結

手動模型繫結

使用 TryUpdateModelAsync 方法即可手動叫用模型繫結。 此方法已於 ControllerBasePageModel 類別中定義。 方法多載可讓您指定要使用的前置詞和值提供者。 如果模型繫結失敗,此方法會傳回 false。 以下為範例:

if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();

TryUpdateModelAsync 會使用值提供者從表單本文、查詢字串和路由資料取得資料。 TryUpdateModelAsync 通常:

  • 會與使用控制器和檢視表的 Razor Pages 和 MVC 應用程式一起使用,以防止過度發佈。
  • 不會與 Web API 一起使用 (除非從表單資料、查詢字串和路由資料中取用)。 取用 JSON 的 Web API 端點會使用輸入格式器來將要求本文還原序列化為物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

此屬性的名稱會遵循指定資料來源的模型繫結屬性的模式。 但它與繫結來自值提供者的資料無關。 它從相依性插入容器取得類型的執行個體。 其目的是只有在呼叫特定方法時,才在您需要服務時提供建構函式插入的替代項目。

其他資源