此文是在官方文档的基础上做的个人笔记,一些简单的内容就没用再列出来了,参考官方文档:/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0
1.模型绑定
作用:
从各种数据源(路由、表单、query string等)中按顺序检索数据。通过方法的入参和公共属性向controller和razor page提供数据。将字符串转为.net类型更新复杂类型的属性
1.1 一个简单的模型绑定过程
考虑有如下代码:
[HttpGet("{id}")]public ActionResult<Pet> GetById(int id, bool dogsOnly)
且有如下请求:/api/pets/2?DogsOnly=true
。
数据源检索和绑定过程如下:
首先查找GetById
方法的第一个参数,发现名字叫id
。然后从HTTP请求的可用源中检索数据,发现route data
中存在键为id
的数据,且值为字符串2。 然后把字符串转为int
。然后查找GetById
的第二个参数,发现名字叫dogsOnly
。然后从请求的可用源中检索数据,发现parameter string
中存在键为dogsonly
的数据(不区大小写),且值为字符串true,然后把字符串转为布尔值true
。最后,框架调用GetById
方法,传入参数2和true
。
1.2 什么情况会触发模型绑定?
请求到某个有入参的Action
上时请求到某个有入参的razor page
的某个方法上时控制器或PageModel
有公有属性且被[BindProperty]
或[BindProperties]
特性标记1.2.1[BindProperty]
特性
可以用到控制器或PageModel
上,会触发模型绑定:
public class EditModel : InstructorsPageModel{[BindProperty]public Instructor Instructor {get; set; }}
1.2.2[BindProperties]
特性
应用到控制器或PageModel
上,会对类下面所有的公共属性触发模型绑定:
[BindProperties(SupportsGet = true)]public class CreateModel : InstructorsPageModel{public Instructor Instructor {get; set; }}
1.2.3 对Get
请求启用模型绑定
默认情况下GET请求一般只需要记录一个id之类的参数,所以不会绑定到某个属性上,如果真的需要可以设置SupportsGet=true
。
[BindProperty(Name = "ai_user", SupportsGet = true)]public string ApplicationInsightsCookie {get; set; }
1.3 可用的数据源都有哪些?
默认情况下模型绑定会从以下数据源中,按照顺序检索键值对数据:
form表单request body(对于[ApiController]
来说)route data (路由数据)(仅用于简单类型)query string (查询字符串)(仅用于简单类型)上传的文件(仅绑定到实现IFormFile
或IEnumerable<IFormFile>
接口的类型上)
如果默认源不正确,则可以手动指定源:
[FromQuery]
从查询字符串获取值。[FromRoute]
从路由数据中获取值。[FromForm]
从已发布的表单字段中获取值。[FromBody]
从请求正文中获取值。[FromHeader]
从 HTTP 请求头中获取值。[FromService]
从DI容器中获取值,如public ProductModel GetProduct([FromServices] IProductModelRequestService productModelRequest)
这些特性可以添加到属性上或者方法的参数上,但是不能添加到类上:
[FromQuery(Name = "Note")]public string NoteFromQueryString {get; set; }//.............public void OnGet([FromHeader(Name = "Accept-Language")] string language)
注意
对于简单类型参数(string,int等):WebApi应用默认只会从route data或query string里进行检索,但你可以使用[FromBody]
强制其从body里检索。MVC应用,会从任何数据源检索。对于复杂类型参数(如自己定义的类):WebApi应用默认只会从Request Body检索,如果有多个复杂参数则只会有一个参数从Body检索,剩余的参数你可以使用[FromUri]
等特性检索。而MVC应用,可以从任何数据源检索。
参考:/questions/42529639/when-i-need-to-use-post-method-with-multiple-parameters-instead-of-separate-mode
1.3.1 单独把[FormBody]
拎出来说下
[FromBody]
应用于方法的参数,且只能应到一个参数上。因为输入格式化程序读取请求流之后,无法再次读取该流以绑定到其他[FromBody]
参数上。当[FromBody]
应用到某个参数上时,如果该参数内部的属性上又指定了其他的特性,则会被忽略。考虑有以下代码:
public ActionResult<Pet> Create([FromBody] Pet pet)/public class Pet{public string Name {get; set; }[FromQuery] // 这个特定会被忽略public string Breed {get; set; }}
1.3.2 自定义可用数据源
使用场景:当你需要从cookie或者session state中获取数据时,就需要自定义。
一般步骤如下:
实现IValueProvider
接口实现IValueProviderFactory
接口在ConfigureServics
里注册实现类
考虑如下代码:
public class CookieValueProvider : BindingSourceValueProvider, IEnumerableValueProvider{private readonly IRequestCookieCollection _values;private PrefixContainer _prefixContainer;public CookieValueProvider(BindingSource bindingSource, IRequestCookieCollection values, CultureInfo culture) : base(bindingSource){if (bindingSource == null){throw new ArgumentNullException(nameof(bindingSource));}if (values == null){throw new ArgumentNullException(nameof(values));}_values = values;Culture = culture;}public CultureInfo Culture {get; }protected PrefixContainer PrefixContainer{get{if (_prefixContainer == null){_prefixContainer = new PrefixContainer(_values.Keys);}return _prefixContainer;}}public override bool ContainsPrefix(string prefix){return PrefixContainer.ContainsPrefix(prefix);}public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix){if (prefix == null){throw new ArgumentNullException(nameof(prefix));}return PrefixContainer.GetKeysFromPrefix(prefix);}public override ValueProviderResult GetValue(string key){if (key == null){throw new ArgumentNullException(nameof(key));}if (key.Length == 0){return ValueProviderResult.None;}var value = _values[key];if (string.IsNullOrEmpty(value)){return ValueProviderResult.None;}else{return new ValueProviderResult(value, Culture);}}}
public class CookieValueProviderFactory : IValueProviderFactory{public Task CreateValueProviderAsync(ValueProviderFactoryContext context){if (context == null){throw new ArgumentNullException(nameof(context));}var cookies = context.ActionContext.HttpContext.Request.Cookies;if (cookies != null && cookies.Count > 0){var valueProvider = new CookieValueProvider(BindingSource.ModelBinding,cookies,CultureInfo.InvariantCulture);context.ValueProviders.Add(valueProvider);}return pletedTask;}}
注册:
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)));});
ValueProviderFactories.Add
方法,将CookieProvider
添加到了所有可用源的末尾,如果要放在最开始的位置,请使用insert(0,xxx)
。
1.3.3 无可用源的操作
当找不到任何一种可用源时:
可空类型被赋值为null
不可空类型被赋值为default(T)
复杂类型则使用默认的构造函数进行构造集合被赋值为Array.Empty<T>
, 但byte[]
是个例外会被赋值为null
如果某个属性上应用了[BindRequired]
特性,则表示此属性在表单数据源中必须有此字段。其它数据源中有没有不做要求(body里的数据是由输入格式化器处理的)。
1.4 类型转换
如果检索到了数据,但是将其转为目标类型时出错,则模型状态ModelState.IsValid
会被标记为无效。目标类型的值为默认值。具有[ApiController]
特性的控制器中,无效的模型状态会自动返回http 400错误。
框架支持将字符串转为一些简单类型,如bool
、byte/sbyte
、char
、DateTime/DateTimeOffset
、Decimal
、double
、enum
、guid
、int16/int32/int64
、single
、TimeSpan
、UInt16/32/64
、Uri
、Version
。
1.5 绑定到复杂类型
这个复杂类型需要有公有构造函数和公有可写属性。对于复杂类型的每个属性(property)来说,当绑定时会查找可用源中键为prefix.property_name
的数据,如果没找到则再查找键为property_name
的数据。
对于方法的入参来说,prefix
就是参数的名称。对于控制器或者page model
里的属性来说,前缀就是属性名。
1.5.1prefix
是参数名
public IActionResult OnPost(Person person)
当绑定person
时,会先查找键为person.Name
的数据,如果找不到,再找键为Name
的数据。
即以下url都可以请求到这个action上:
localhost/controllername/onpost?person.Name=123
localhost/controllername/onpost?Name=123
1.5.2prefix
是属性名
考虑有以下代码
public class ServerInfoController : ControllerBase{[BindProperty]public Person Person {get; set; }}
会先开始查找键为Person.Name
的数据,然后查找键为Name
的数据。
1.5.3 自定义prefix
public IActionResult OnPost(int? id, [Bind(Prefix = "Instructor")] Person person)
先查找Instructor.Name
,然后再查找Name
。
1.6[Bind]
、[ModelBinder]
、[BindRequired]
与[BindNever]
这几个特性都只会影响通过form表单提交过来的数据,不会影响body里json或xml格式的数据,因为这些数据是通过输入格式化器来处理的。
1.6.1[Bind]
可用于类、方法入参。用来指定模型绑定过程中应该包含哪些属性。考虑以下代码:
[Bind("Name,Age,Address")]public class Person{}[HttpPost]public IActionResult OnPost([Bind("Name,Age")] Person person)
[Bind]
特性可以解决过多发布(over post)的问题,因为只会从源中检索列出来的那些数据,没有列出来的那些会设置为默认值或null。
1.6.2[ModelBinder]
与自定义模型绑定
可用于类、方法入参、属性。用来指定Binder。
[HttpPost]public IActionResult OnPost([ModelBinder(typeof(MyPersonModelBinder))] Person person)
以上的MyPersonModelBinder
代码,会根据请求过来的id然后查数据库,然后返回一个person对象。
public class Instructor{[ModelBinder(Name = "instructor_id")]public string Id {get; set; }}
以上代码用来更改prefix
。
1.6.3[BindRequired]
与[BindNever]
只能应用在属性上,不能应用到类或者方法入参上。
public class Person{[BindRequired]//表示可用源中必须要有这个键public string Name{get;set;}[BindNever]//永远不从可用源中读取这个键的值,即使有也不读取public int Age{get;set;}}
1.7 集合的处理
假如要绑定的参数是selectedCourses
的数组:public IActionResult OnPost(int? id, int[] selectedCourses)
. 则以下query string源可以匹配到:
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
对于下标是从0开始的数据,则下标必须连续,否则不连续的地方之后的数据会被抛弃。
以下格式的表单数据也可以匹配到:
selectedCourses[]=1050&selectedCourses[]=2000
1.8字典的处理
假如要绑定到selectedCourses
上public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
,则以下query string
可以匹配到:
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
1.9 输入格式化器
输入格式化器用来处理请求body中的json、xml等格式的数据。框架基于[Consumes]
特性的值来选择具体的格式化器,如果没有这个特性,则根据请求头中的content-type
的值。
默认的格式化器是json,如果要处理xml,则
添加nuget包Microsoft.AspNetCore.Mvc.Formatters.Xml
,在configureservices
中进行配置:services.AddRazorPages().AddMvcOptions(options =>{...}).AddXmlSerializerFormatters();
将[Consumes]
特性添加到action上:
[HttpPost][Consumes("application/xml")]public ActionResult<Pet> Create(Pet pet)
自定义输入格式化器,/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#input-formatters
1.10 模型绑定时设置不绑定某些类型
设置Person
类不参与模型绑定ExcludeBindingMetadataProvider
services.AddRazorPages().AddMvcOptions(options =>{options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Person)));});
设置之后如果请求到public ActionResult<Person> Create(Person person)
这个action上,会报空引用异常。但是如果某个model的属性是Person
类型,则会正常绑定。
设置Person
类下所有的属性都不验证SuppressChildValidationMetadataProvider
services.AddRazorPages().AddMvcOptions(options =>{options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Person)));});
这样,如果Person
类下某个属性设置了[Required]
注解即使可用源里没有这个键也不会报错。
2. 自定义模型绑定
一般步骤:
自定义MyBinder
类实现IModelBinder
接口在类上应用[ModelBinder(BinderType=typeof(MyBinder)]
特性。在action的入参上应用[ModelBinder(Name = "id")]
。
代码参考:
public class SysUserEntityBinder : IModelBinder{private MainDbContext _context;public SysUserEntityBinder(MainDbContext context){_context = context;}public Task BindModelAsync(ModelBindingContext bindingContext){if (bindingContext == null){throw new ArgumentNullException(nameof(bindingContext));}var modelName = bindingContext.ModelName;//键名,如果不指定的话,默认就是参数名//从可用源中根据键名检索数据var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);if (valueProviderResult == ValueProviderResult.None){return pletedTask;}bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);var value = valueProviderResult.FirstValue;if (string.IsNullOrEmpty(value)){return pletedTask;}if (!long.TryParse(value, out var id)){bindingContext.ModelState.TryAddModelError(modelName, "Id must be an long.");return pletedTask;}var model = _context.SysUsers.Find(id);bindingContext.Result = ModelBindingResult.Success(model);return pletedTask;}}
在类上应用ModelBinder
:
[ModelBinder(typeof(SysUserEntityBinder))]public class SysUser{///}
在方法上写:
[HttpPost("{id}")]public async Task<IActionResult> Create([ModelBinder(Name ="id")]SysUser user)//指定name为id
2.1实现IModelBinderProvider
这种方式无须在SysUser
类或者SysUser
类的参数上写[ModelBinder]
,使用上会方便一些 。框架内置的绑定器也都是这样么实现的。
public class SysUserBinderProvider : IModelBinderProvider{public IModelBinder GetBinder(ModelBinderProviderContext context){if(context.Metadata.ModelType==typeof(SysUser)){return new BinderTypeModelBinder(typeof(SysUserEntityBinder));}return null;}}
然后配置服务:
public void ConfigureServices(IServiceCollection services){services.AddControllers(opt =>{//插入到第一个位置,最先使用这个来解析SysUser对象opt.ModelBinderProviders.Insert(0, new SysUserBinderProvider());})}
对于如下action来说:
[HttpGet]public ActionResult<SysUser> Get(SysUser user);
请求的url为localhost/controllername/get?user=123
,就会返回Id为123的数据。
3.模型验证
模型绑定和模型验证在执行action或者razor page前发生。web应用可以通过检查ModelState.IsValid
判断是否验证通过。web api应用不需要开发人员手动判断,当验证不通过时会自动返回http 400
.
3.1手动触发验证TryValidateModel
使用场景:一般是框架验证过了,自己又手动修改了属性的值,需要触发验证看是否通过。
if(!ModelState.IsValid){return Page();}Movie.ReleaseDate = modifiedReleaseDate;if (!TryValidateModel(Movie, nameof(Movie))){return Page();}///
3.2 框架内置的模型验证特性
[CreditCard]
:验证属性是否具有信用卡格式。 需要 JQuery 验证其他方法。[Compare]
:验证模型中的两个属性是否匹配。[EmailAddress]
:验证属性是否具有电子邮件格式。[Phone]
:验证属性是否具有电话号码格式。[Range]
:验证属性值是否在指定的范围内。[RegularExpression]
:验证属性值是否与指定的正则表达式匹配。[Required]
:验证字段是否不为 null。[StringLength]
:验证字符串属性值是否不超过指定长度限制。[Url]
:验证属性是否具有 URL 格式。[Remote]
:通过在服务器上调用操作方法来验证客户端上的输入。这些特性可以用到模型类的公有属性上,也可以用在action的入参上,也可以用在controller的公有属性上。
3.3 扩展模型验证特性
3.3.1 通过自定义验证特性的方式
使用场景:框架内置验证特性无法满足需求。可以继承ValidationAttribute
类,并替代IsValid
方法,实现自定义验证特性。
public class ValidDateTimeAttribute:ValidationAttribute{int Year;public ValidDateTimeAttribute(int minYear){Year = minYear;}protected override ValidationResult IsValid(object value, ValidationContext validationContext){//可以通过validationContext.ObjectInstance获取模型类的实例var dt = (DateTime)value;if(dt.Year>=Year){return ValidationResult.Success;}else{return new ValidationResult($"Year can't less than {Year}");}}}
[ValidDateTime(1960)]public DateTime? CreateTime {get; set; }
3.3.2 实现IValidatableObject
接口
上面那种方法适用于所有的模型类,但是如果IsValid
方法里需要获取模型类的实例时,就变得不通用了。可以通过让模型类实现IValidatableObject
接口来采用模型类级别的验证:
public class ValidatableMovie : IValidatableObject{///。。。。public IEnumerable<ValidationResult> Validate(ValidationContext validationContext){if (this.Year > 1960){yield return new ValidationResult($"Classic movies must have a release year no later than {1960}.",new[] {nameof(ReleaseDate) });}}}
3.4 模型验证时设置最大错误数和最大递归层数
services.AddRazorPages().AddMvcOptions(options =>{options.MaxModelValidationErrors = 50;//最大错误数,默认是200options.MaxValidationDepth=40;//设置验证递归层数,默认是32});
3.5 禁用模型验证
新建类实现IObjectModelValidator
接口:
public class NullObjectModelValidator : IObjectModelValidator{public void Validate(ActionContext actionContext, ValidationStateDictionary validationState, string prefix, object model){}}
然后配置服务:
services.AddSingleton<IObjectModelValidator, NullObjectModelValidator>();
此时设置了[Required]
即使不填写也不会报错,但是模型绑定的错误还会正常显示。
如果觉得《ASP.NET Core 基础(十三)——模型绑定与模型验证》对你有帮助,请点赞、收藏,并留下你的观点哦!