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 请求只需一个记录 ID 参数。 记录 ID 用于查找数据库中的项。 因此,无需绑定包含模型实例的属性。 在需要将属性绑定到 GET 请求中的数据的情况下,请将 SupportsGet 属性设置为 true

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

模型绑定简单和复杂类型

模型绑定为其操作对象的类型使用特定定义。 简单类型转换自使用 TypeConverterTryParse 方法的单个字符串。 复杂类型转换自多个输入值。 框架基于是否存在 TypeConverterTryParse 来确定差异。 建议为不需要外部资源或多个输入的 stringSomeType 转换创建类型转换器或使用 TryParse

默认情况下,模型绑定以键值对的形式从 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 中注册工厂类。

示例包括从 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] 行为适用于已发布的表单数据中的模型绑定,而不适用于请求正文中的 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。 如果未找到,它将仅查找不含前缀的 properties_name。 使用前缀的决定不是针对每个属性做出的。 例如,当查询包含 ?Instructor.Id=100&Name=foo 且绑定到方法 OnGet(Instructor instructor) 时,类型 Instructor 的生成对象将会包含:

  • Id 设置为 100
  • Name 设置为 null。 模型绑定预期为 Instructor.Name,因为前面的查询参数中使用了 Instructor.Id

对于绑定到参数,前缀是参数名称。 对于绑定到 PageModel 公共属性,前缀是公共属性名称。 某些属性具有 Prefix 属性,让你可以替代参数或属性名称的默认用法。

例如,假设复杂类型是以下 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_name 或 property_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_name 或 property_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 请求只需一个记录 ID 参数。 记录 ID 用于查找数据库中的项。 因此,无需绑定包含模型实例的属性。 在需要将属性绑定到 GET 请求中的数据的情况下,请将 SupportsGet 属性设置为 true

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

模型绑定简单和复杂类型

模型绑定为其操作对象的类型使用特定定义。 简单类型转换自使用 TypeConverterTryParse 方法的单个字符串。 复杂类型转换自多个输入值。 框架基于是否存在 TypeConverterTryParse 来确定差异。 建议为不需要外部资源或多个输入的 stringSomeType 转换创建类型转换器或使用 TryParse

默认情况下,模型绑定以键值对的形式从 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 中注册工厂类。

示例包括从 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] 行为适用于已发布的表单数据中的模型绑定,而不适用于请求正文中的 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。 如果未找到,它将仅查找不含前缀的 properties_name。 使用前缀的决定不是针对每个属性做出的。 例如,当查询包含 ?Instructor.Id=100&Name=foo 且绑定到方法 OnGet(Instructor instructor) 时,类型 Instructor 的生成对象将会包含:

  • Id 设置为 100
  • Name 设置为 null。 模型绑定预期为 Instructor.Name,因为前面的查询参数中使用了 Instructor.Id

对于绑定到参数,前缀是参数名称。 对于绑定到 PageModel 公共属性,前缀是公共属性名称。 某些属性具有 Prefix 属性,让你可以替代参数或属性名称的默认用法。

例如,假设复杂类型是以下 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_name 或 property_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_name 或 property_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 请求只需一个记录 ID 参数。 记录 ID 用于查找数据库中的项。 因此,无需绑定包含模型实例的属性。 在需要将属性绑定到 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 中注册工厂类。

示例包括从 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] 行为适用于已发布的表单数据中的模型绑定,而不适用于请求正文中的 JSON 或 XML 数据。 请求正文数据由输入格式化程序进行处理。

类型转换错误

如果找到源,但无法将其转换为目标类型,则模型状态将被标记为无效。 目标参数或属性设置为 NULL 或默认值,如上一部分所述。

在具有 [ApiController] 属性的 API 控制器中,无效的模型状态会导致自动 HTTP 400 响应。

在 Razor 页面中,重新显示带有错误消息的页面:

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

    // ...

    return RedirectToPage("./Index");
}

在使用先前的代码重新显示页时,表单域中不会显示无效的输入。 这是因为模型属性已设置为 NULL 或默认值。 无效输入会出现在错误消息中。 如果要在表单域中重新显示错误数据,可以考虑将模型属性设置为字符串并手动执行数据转换。

如果不希望发生类型转换错误导致模型状态错误的情况,建议使用相同的策略。 在这种情况下,将模型属性设置为字符串。

简单类型

模型绑定器可以将源字符串转换为以下简单类型:

复杂类型

复杂类型必须具有要绑定的公共默认构造函数和公共可写属性。 进行模型绑定时,将使用公共默认构造函数来实例化类。

对于复杂类型的每个属性,模型绑定会在源中查找名称模式prefix.property_name。 如果未找到,它将仅查找不含前缀的 properties_name。 使用前缀的决定不是针对每个属性做出的。 例如,当查询包含 ?Instructor.Id=100&Name=foo 且绑定到方法 OnGet(Instructor instructor) 时,类型 Instructor 的生成对象将会包含:

  • Id 设置为 100
  • Name 设置为 null。 模型绑定预期为 Instructor.Name,因为前面的查询参数中使用了 Instructor.Id

对于绑定到参数,前缀是参数名称。 对于绑定到 PageModel 公共属性,前缀是公共属性名称。 某些属性具有 Prefix 属性,让你可以替代参数或属性名称的默认用法。

例如,假设复杂类型是以下 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_name 或 property_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_name 或 property_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 请求只需一个记录 ID 参数。 记录 ID 用于查找数据库中的项。 因此,无需绑定包含模型实例的属性。 在需要将属性绑定到 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 中注册工厂类。

示例应用包括从 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] 行为适用于已发布的表单数据中的模型绑定,而不适用于请求正文中的 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 的源。 如果未找到,它将仅查找不含前缀的 properties_name

对于绑定到参数,前缀是参数名称。 对于绑定到 PageModel 公共属性,前缀是公共属性名称。 某些属性具有 Prefix 属性,让你可以替代参数或属性名称的默认用法。

例如,假设复杂类型是以下 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_name 或 property_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_name 或 property_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

以包含自定义 ObjectId 属性 Id 的模型为例:

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] 属性

此属性的名称遵循指定数据源的模型绑定属性的模式。 但这与绑定来自值提供程序的数据无关。 它从依赖关系注入容器中获取类型的实例。 其目的在于,在仅当调用特定方法时需要服务的情况下,提供构造函数注入的替代方法。

其他资源