此文是在官方文档的基础上做的个人笔记,一些简单的内容就没用再列出来了,参考官方文档: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

数据源检索和绑定过程如下:

  1. 首先查找GetById方法的第一个参数,发现名字叫id。然后从HTTP请求的可用源中检索数据,发现route data中存在键为id的数据,且值为字符串2。 然后把字符串转为int
  2. 然后查找GetById的第二个参数,发现名字叫dogsOnly。然后从请求的可用源中检索数据,发现parameter string中存在键为dogsonly的数据(不区大小写),且值为字符串true,然后把字符串转为布尔值true
  3. 最后,框架调用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 可用的数据源都有哪些?

默认情况下模型绑定会从以下数据源中,按照顺序检索键值对数据:

  1. form表单
  2. request body(对于[ApiController]来说)
  3. route data (路由数据)(仅用于简单类型)
  4. query string (查询字符串)(仅用于简单类型)
  5. 上传的文件(仅绑定到实现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)

注意

  1. 对于简单类型参数(string,int等):WebApi应用默认只会从route data或query string里进行检索,但你可以使用[FromBody]强制其从body里检索。MVC应用,会从任何数据源检索。
  2. 对于复杂类型参数(如自己定义的类):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中获取数据时,就需要自定义。

一般步骤如下:

  1. 实现IValueProvider接口
  2. 实现IValueProviderFactory接口
  3. 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错误。

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

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字典的处理

假如要绑定到selectedCoursespublic 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,则

  1. 添加nuget包Microsoft.AspNetCore.Mvc.Formatters.Xml
  2. configureservices中进行配置:services.AddRazorPages().AddMvcOptions(options =>{...}).AddXmlSerializerFormatters();
  3. [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 模型绑定时设置不绑定某些类型

  1. 设置Person类不参与模型绑定 ExcludeBindingMetadataProvider
services.AddRazorPages().AddMvcOptions(options =>
{options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Person)));
});

设置之后如果请求到public ActionResult<Person> Create(Person person)这个action上,会报空引用异常。但是如果某个model的属性是Person类型,则会正常绑定。

  1. 设置Person类下所有的属性都不验证 SuppressChildValidationMetadataProvider
services.AddRazorPages().AddMvcOptions(options =>
{options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Person)));
});

这样,如果Person类下某个属性设置了[Required]注解即使可用源里没有这个键也不会报错。

2. 自定义模型绑定

一般步骤:

  1. 自定义MyBinder类实现IModelBinder接口
  2. 在类上应用[ModelBinder(BinderType=typeof(MyBinder)]特性。
  3. 在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 基础(十三)——模型绑定与模型验证相关推荐

  1. ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单编程

    ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core 登录登出 - ASP.NET Core 基础教程 - 简单教程,简单 ...

  2. ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单编程

    ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core 动作结果 - ASP.NET Core 基础教程 - 简单教程,简单 ...

  3. ASP.NET Core 用户注册 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 用户注册 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 用户注册 上一章节我们终于迁移完了 Identity 的数据,也创建 ...

  4. ASP.NET Core Razor 布局视图 - ASP.NET Core 基础教程 - 简单教程,简单编程

    ASP.NET Core Razor 布局视图 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core Razor 布局视图 - ASP.NET Core 基础 ...

  5. ASP.NET Core 异常和错误处理 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 异常和错误处理 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 异常和错误处理 上一章节中,我们学习了 ASP.NET Cor ...

  6. ASP.NET Core macOS 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程

    ASP.NET Core macOS 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core macOS 环境配置 - ASP.NET Core 基础 ...

  7. ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Razor 标签助手 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Razor 标签助手 上一章节我们介绍了视图导入,学习了 ...

  8. ASP.NET Core Identity 迁移数据 - ASP.NET Core 基础教程 - 简单教程,简单编程

    ASP.NET Core Identity 迁移数据 - ASP.NET Core 基础教程 - 简单教程,简单编程 原文:ASP.NET Core Identity 迁移数据 - ASP.NET C ...

  9. ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Windows 环境配置 ASP.NET Core ...

最新文章

  1. 计算机更改本地用户,win10电脑更改本机账户名的详细步骤(图文)
  2. Pig 0.12.1安装和使用
  3. 信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1065:奇数求和
  4. YBTOJ洛谷P2839:最大中位数(主席树、二分答案)
  5. 第三届“空间信息网络”学术论坛诚邀您的参加
  6. vc6 设置静态文本框透明_微信还能这么玩?半透明的微信背景主题用起来!
  7. 【嵌入式】使用Cross Toolchain构建交叉工具链
  8. PHP图形图像的典型应用 --常用图像的应用(统计图)
  9. 详解百度地图API之地图操作
  10. makefile编写---.c .cpp 混合编译makefile 模板
  11. 2018 Multi-University Training Contest 10 hdu 6432 Problem G. Cyclic(oeis题)
  12. 麻省理工成立金融科技实验室,蚂蚁金服成唯一中国创始企业
  13. Mysql基本语法及其操作(三)
  14. IP地址、路由器、数据分片、地址管理、子网掩码、路由选择、公网与私网
  15. 【Python数据可视化(五)】创建3D可视化图表
  16. C Primer Plus (第六版) 第十四章_编程练习答案
  17. 光伏mppt扰动观察法仿真,matlab2018a
  18. c语言指针数组分配内存,指针数组数组指针的分配内存及函数参数 C语言版
  19. 概率论与数理统计(3.4) 相互独立的随机变量
  20. IOS开发百度地图API-用点生成路线,导航,气泡响应

热门文章

  1. java 生成 word文档 导出附带图片 已实现
  2. Administrator privileges required for OLE Remote Procedure Call debugging: this feature will not wor
  3. spring常见面试题(2023最新)
  4. pycharm批量注释
  5. 基本触发器和钟控触发器
  6. JavaScript系列文章:变量提升和函数提升
  7. 【目标】新学期计划与目标
  8. Java 获取当前年,前几年(之前年),后几年(之后年)
  9. 广度优先搜索:迷宫问题
  10. HTML DOM nextSibling 和nextElementSibling属性