糖尿病康复,内容丰富有趣,生活中的好帮手!
糖尿病康复 > ASP.NET Core 基础(十三)——模型绑定与模型验证

ASP.NET Core 基础(十三)——模型绑定与模型验证

时间:2023-08-02 12:02:14

相关推荐

ASP.NET Core 基础(十三)——模型绑定与模型验证

此文是在官方文档的基础上做的个人笔记,一些简单的内容就没用再列出来了,参考官方文档:/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 (查询字符串)(仅用于简单类型)上传的文件(仅绑定到实现IFormFileIEnumerable<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错误。

框架支持将字符串转为一些简单类型,如boolbyte/sbytecharDateTime/DateTimeOffsetDecimaldoubleenumguidint16/int32/int64singleTimeSpanUInt16/32/64UriVersion

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=123localhost/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=2000selectedCourses[0]=1050&selectedCourses[1]=2000[0]=1050&[1]=2000selectedCourses[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字典的处理

假如要绑定到selectedCoursespublic IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses),则以下query string可以匹配到:

selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics[1050]=Chemistry&selectedCourses[2000]=EconomicsselectedCourses[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 基础(十三)——模型绑定与模型验证》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。