ASP.NET Core 基础(十三)——模型绑定与模型验证
此文是在官方文档的基础上做的个人笔记,一些简单的内容就没用再列出来了,参考官方文档:https://docs.microsoft.com/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)
且有如下请求:http://contoso.com/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应用,可以从任何数据源检索。
参考:https://stackoverflow.com/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 Task.CompletedTask;}
}
注册:
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.1 prefix
是参数名
public IActionResult OnPost(Person person)
当绑定person
时,会先查找键为person.Name
的数据,如果找不到,再找键为Name
的数据。
即以下url都可以请求到这个action上:
localhost/controllername/onpost?person.Name=123
localhost/controllername/onpost?Name=123
1.5.2 prefix
是属性名
考虑有以下代码
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)
自定义输入格式化器,https://docs.microsoft.com/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 Task.CompletedTask;}bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);var value = valueProviderResult.FirstValue;if (string.IsNullOrEmpty(value)){return Task.CompletedTask;}if (!long.TryParse(value, out var id)){bindingContext.ModelState.TryAddModelError(modelName, "Id must be an long.");return Task.CompletedTask;}var model = _context.SysUsers.Find(id);bindingContext.Result = ModelBindingResult.Success(model);return Task.CompletedTask;}
}
在类上应用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 基础(十三)——模型绑定与模型验证相关推荐
- ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单编程
ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单 ...
- ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单编程
ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单 ...
- ASP.NET Core 用户注册 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 用户注册 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 用户注册 上一章节我们终于迁移完了 Identity 的数据,也创建 ...
- ASP.NET Core Razor 布局视图 - ASP.NET Core 基础教程 - 简单教程,简单编程
ASP.NET Core Razor 布局视图 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core Razor 布局视图 - ASP.NET Core 基础 ...
- ASP.NET Core 异常和错误处理 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 异常和错误处理 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 异常和错误处理 上一章节中,我们学习了 ASP.NET Cor ...
- ASP.NET Core macOS 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程
ASP.NET Core macOS 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core macOS 环境配置 - ASP.NET Core 基础 ...
- ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Razor 标签助手 上一章节我们介绍了视图导入,学习了 ...
- ASP.NET Core Identity 迁移数据 - ASP.NET Core 基础教程 - 简单教程,简单编程
ASP.NET Core Identity 迁移数据 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core Identity 迁移数据 - ASP.NET C ...
- ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Windows 环境配置 ASP.NET Core ...
最新文章
- 计算机更改本地用户,win10电脑更改本机账户名的详细步骤(图文)
- Pig 0.12.1安装和使用
- 信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1065:奇数求和
- YBTOJ洛谷P2839:最大中位数(主席树、二分答案)
- 第三届“空间信息网络”学术论坛诚邀您的参加
- vc6 设置静态文本框透明_微信还能这么玩?半透明的微信背景主题用起来!
- 【嵌入式】使用Cross Toolchain构建交叉工具链
- PHP图形图像的典型应用 --常用图像的应用(统计图)
- 详解百度地图API之地图操作
- makefile编写---.c .cpp 混合编译makefile 模板
- 2018 Multi-University Training Contest 10 hdu 6432 Problem G. Cyclic(oeis题)
- 麻省理工成立金融科技实验室,蚂蚁金服成唯一中国创始企业
- Mysql基本语法及其操作(三)
- IP地址、路由器、数据分片、地址管理、子网掩码、路由选择、公网与私网
- 【Python数据可视化(五)】创建3D可视化图表
- C Primer Plus (第六版) 第十四章_编程练习答案
- 光伏mppt扰动观察法仿真,matlab2018a
- c语言指针数组分配内存,指针数组数组指针的分配内存及函数参数 C语言版
- 概率论与数理统计(3.4) 相互独立的随机变量
- IOS开发百度地图API-用点生成路线,导航,气泡响应
热门文章
- java 生成 word文档 导出附带图片 已实现
- Administrator privileges required for OLE Remote Procedure Call debugging: this feature will not wor
- spring常见面试题(2023最新)
- pycharm批量注释
- 基本触发器和钟控触发器
- JavaScript系列文章:变量提升和函数提升
- 【目标】新学期计划与目标
- Java 获取当前年,前几年(之前年),后几年(之后年)
- 广度优先搜索:迷宫问题
- HTML DOM nextSibling 和nextElementSibling属性