ASP.NET Core 中的資料繫結

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

何謂模型繫結

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

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 將資料提供給方法參數和公用屬性中的控制器和 Razor 頁面。
  • 將字串資料轉換成 .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。
  • 尋找 的下一個參數,這是名為 dogsOnlyGetById 布林值。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 「true」 轉換成布林值 true

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

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

Targets

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

  • 要求路由目標的控制器動作方法參數。
  • 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. 已上傳的檔案

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

  • 路由資料和查詢字串值只用於簡單型別。
  • 上傳的檔案只會系結至實 IFormFile 作 或 IEnumerable<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 s 或 會話狀態的資料。 若要從新來源取得資料:

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

此範例包含值提供者處理站範例,可取得 的值。 cookie 在 中 Program.cs 註冊自訂值提供者處理站:

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] 行為適用于來自張貼表單資料的模型系結,不適用於 JS 要求本文中的 ON 或 XML 資料。 要求本文資料是由 輸入格式器處理。

類型轉換錯誤

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

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

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

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

    // ...

    return RedirectToPage("./Index");
}

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

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

簡單型別

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

使用 系結 IParsable<T>.TryParse

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

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

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

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)
    {
        if (string.IsNullOrEmpty(value) || value.Split('-').Length != 2)
        {
            result = default;
            return false;
        }

        var range = value.Split(',');
        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

下列控制器動作會 DateRDateRangeTPange 使用 類別來系結日期範圍:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([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);
}

複雜類型

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

針對複雜類型的每個屬性, 模型系結會查看名稱模式的來源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 沒有前置詞。

複雜類型目標的屬性

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

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們 不會影響 輸入格式器,這些格式器會處理張貼 JS ON 和 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
    

請避免系結名為 index 的參數或屬性,如果 Index 它與集合值相鄰,則為 。 模型系結嘗試使用 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相符專案。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是 Dictionary<int, string> 名為 selectedCourses 的 :

    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」]=「經濟」

建構函式系結和記錄類型

模型系結需要複雜類型具有無參數建構函式。 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);

若要讓上述專案正常運作,類型必須:

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

不含無參數建構函式的 POC

沒有無參數建構函式的 POC 無法系結。

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

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

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

輸入格式器

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

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

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

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

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

請考慮下列模型,其中包含名為 的 Id 自訂 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 使用。 使用 JS ON 的 Web API 端點會使用 輸入格式器 ,將要求主體還原序列化為 物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

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

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

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

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

其他資源

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

何謂模型繫結

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

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 將資料提供給方法參數和公用屬性中的控制器和 Razor 頁面。
  • 將字串資料轉換成 .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。
  • 尋找 的下一個參數,這是名為 dogsOnlyGetById 布林值。
  • 查看來源,在查詢字串中找到 "DogsOnly=true"。 名稱比對不區分大小寫。
  • 將字串 「true」 轉換成布林值 true

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

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

Targets

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

  • 要求路由目標的控制器動作方法參數。
  • 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. 已上傳的檔案

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

  • 路由資料和查詢字串值只用於簡單型別。
  • 上傳的檔案只會系結至實 IFormFile 作 或 IEnumerable<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 s 或 會話狀態的資料。 若要從新來源取得資料:

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

此範例包含值提供者處理站範例,可取得 的值。 cookie 在 中 Program.cs 註冊自訂值提供者處理站:

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] 行為適用于來自張貼表單資料的模型系結,不適用於 JS 要求本文中的 ON 或 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 沒有前置詞。

複雜類型目標的屬性

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

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們 不會影響 輸入格式器,這些格式器會處理張貼 ON JS 和 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
    

避免系結名為 index 的參數或屬性,如果它與集合值相鄰,則 Index 為 。 模型系結嘗試當做集合的索引使用 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相符專案。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是 Dictionary<int, string> 名為 selectedCourses 的 :

    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」]=「經濟」

建構函式系結和記錄類型

模型系結需要複雜類型具有無參數建構函式。 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);

若要讓上述專案運作,類型必須:

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

不含無參數建構函式的 POPO

沒有無參數建構函式的 POC 無法系結。

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

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

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

輸入格式器

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

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

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

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

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

請考慮下列模型,其中包含名為 Id 的自訂 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 搭配使用。 使用 JS ON 的 Web API 端點會使用 輸入格式器 將要求主體還原序列化為 物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

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

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

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

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

其他資源

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

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

何謂模型繫結

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

  • 從各種來源擷取資料,例如路由資料、表單欄位和查詢字串。
  • 提供資料給方法參數和 Razor 公用屬性中的控制器和頁面。
  • 將字串資料轉換成 .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 旗標。

Targets

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

  • 要求路由目標的控制器動作方法參數。
  • 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. 已上傳的檔案

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

  • 路由資料和查詢字串值只用於簡單型別。
  • 上傳的檔案只會系結至實 IFormFile 作 或 IEnumerable<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 取得值。 以下是 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] 行為適用于來自張貼表單資料的模型系結,不適用於 JS 要求本文中的 ON 或 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]

警告

當張貼的表單資料為值來源時,這些屬性會影響模型繫結。 它們 不會影響 輸入格式器,這些格式器會處理張貼 ON JS 和 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
    

    請避免系結名為 index 的參數或屬性,如果 Index 它與集合值相鄰,則為 。 模型系結嘗試使用 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相符專案。 如果找不到相符項目,它會尋找其中一種沒有前置詞的受支援格式。 例如:

  • 假設目標參數是 Dictionary<int, string> 名為 selectedCourses 的 :

    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」]=「經濟」

建構函式系結和記錄類型

模型系結需要複雜類型具有無參數建構函式。 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);

若要讓上述專案正常運作,類型必須:

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

不含無參數建構函式的 POC

沒有無參數建構函式的 POC 無法系結。

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

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

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

輸入格式器

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

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 使用。 使用 JS ON 的 Web API 端點會使用 輸入格式器 ,將要求主體還原序列化為 物件。

如需詳細資訊,請參閱 TryUpdateModelAsync

[FromServices] 屬性

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

其他資源