失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > ASP.NET Core MVC 中的模型验证

ASP.NET Core MVC 中的模型验证

时间:2019-10-30 04:02:08

相关推荐

ASP.NET Core MVC 中的模型验证

数据模型的验证被视为是数据合法性的第一步,要求满足类型、长度、校验等规则,有了MVC的模型校验能够省却很多前后端代码,为代码的简洁性也做出了不少贡献。

原文地址:/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-2.1

作者:Rachel Appel

模型验证简介

在将数据存储到数据库之前,应用必须先验证数据。 必须检查数据是否存在潜在的安全威胁,确保数据已设置适当的类型和大小格式,并且必须符合相关规则。 实施验证的过程可能有些单调乏味,但却必不可少。 在 MVC 中,验证发生在客户端和服务器上。

幸运的是,.NET 已将验证抽象化为验证属性。 这些属性包含验证代码,从而减少了所需编写的代码量。

在 Core 2.2 及更高版本中,如果能够确定给定模型关系图不需要进行验证, Core 运行时便会简化(跳过)验证。 验证无法或没有关联任何验证程序的模型时,跳过验证可能会显著提升性能。 已跳过的验证包括诸如基元集合(byte[]string[]Dictionary<string, string>等)之类的对象,或没有任何验证程序的复杂对象关系图。

查看或下载 GitHub 中的示例。

验证属性

验证属性用于配置模型验证,因此,在概念上类似于数据库表中字段上的验证。 它包括诸如分配数据类型或必填字段之类的约束。 其他类型的验证包括将向数据应用模式以强制实施业务规则,比如信用卡、电话号码或电子邮件地址。 验证属性更易使用,并使这些要求的实施变得更简单。

验证特性在属性级别指定:

C#

[Required]public string MyProperty { get; set; }

下面是一个应用的已批注Movie模型,该应用用于存储电影和电视节目的相关信息。 大多数属性都是必需属性,多个字符串属性具有长度要求。 此外,还有一个针对·Price属性设置的从 0 到 $999.99 的数值范围限制,以及一个自定义验证特性。

C#

public class Movie{public int Id { get; set; }[Required][StringLength(100)]public string Title { get; set; }[ClassicMovie(1960)][DataType(DataType.Date)]public DateTime ReleaseDate { get; set; }[Required][StringLength(1000)]public string Description { get; set; }[Range(0, 999.99)]public decimal Price { get; set; }[Required]public Genre Genre { get; set; }public bool Preorder { get; set; }}

通过读取整个模型即可显示有关此应用的数据的规则,从而使代码维护变得更轻松。 下面是几个常用的内置验证属性:

[CreditCard]:验证属性是否具有信用卡格式。

[Compare]:验证某个模型中的两个属性是否匹配。

[EmailAddress]:验证属性是否具有电子邮件格式。

[Phone]:验证属性是否具有电话格式。

[Range]:验证属性值是否落在给定范围内。

[RegularExpression]:验证数据是否与指定的正则表达式匹配。

[Required]:将属性设置为必需属性。

[StringLength]:验证字符串属性是否最多具有给定的最大长度。

[Url]:验证属性是否具有 URL 格式。

MVC 支持从ValidationAttribute派生的所有用于验证的属性。 在 ponentModel.DataAnnotations 命名空间中可找到许多有用的验证属性。

在某些情况下,内置属性可能无法提供所需的功能。 这时,就可以通过从ValidationAttribute派生或将模型更改为实现IValidatableObject,来创建自定义验证属性。

必需属性的使用说明

从本质上来说,需要不可以为 null 的值类型(如decimalintfloatDateTime),但不需要Required特性。 应用不会对标记为Required的不可为 null 的类型执行任何服务器端验证检查。

对于不可为 null 的类型,MVC 模型绑定(与验证和验证属性无关)会拒绝包含缺失值或空白的表单域提交。 如果目标属性上缺少BindRequired特性,模型绑定会忽略不可为 null 的类型的缺失数据,导致传入表单数据中缺少表单域。

BindRequired 特性(另请参阅 Core 中的模型绑定)可用于确保表单数据完整。 当应用于某个属性时,模型绑定系统要求该属性具有值。 当应用于某个类型时,模型绑定系统要求该类型的所有属性都具有值。

使用 Nullable<T> 类型(例如,decimal?System.Nullable<decimal>)并将其标记为Required时,将执行服务器端验证检查,就像该属性是标准的可以为 null 的类型(例如,string)一样。

客户端验证要求与标记为Required的模型属性对应的表单域以及未标记为Required的不可为 null 的类型属性具有值。Required可用于控制客户端验证错误消息。

模型状态

模型状态表示已提交的 HTML 表单值中的验证错误。

MVC 将继续验证字段,直至达到错误数上限(默认为 200 个)。 可以使用Startup.ConfigureServices中的以下代码配置该数字:

C#

services.AddMvc(options => options.MaxModelValidationErrors = 50);

处理模型状态错误

在执行控制器操作之前进行模型验证。 该操作负责检查ModelState.IsValid并做出相应响应。 在许多情况下,正确的反应是返回错误响应,理想状况下会详细说明模型验证失败的原因。

如果在使用[ApiController]属性的 web API 控制器中,ModelState.IsValid的计算结果为false,将返回包含问题详细信息的自动 HTTP 400 响应。 有关详细信息,请参阅自动 HTTP 400 响应。

某些应用会选择遵循标准约定来处理模型验证错误,在这种情况下,可以在筛选器中实现此类策略。 应测试操作在有效模型状态和无效模型状态下的行为方式。

手动验证

完成模型绑定和验证后,可能需要重复其中的某些步骤。 例如,用户可能在应输入整数的字段中输入了文本,或者你可能需要计算模型的某个属性的值。

你可能需要手动运行验证。 为此,请调用TryValidateModel方法,如下所示:

C#

TryValidateModel(movie);

自定义验证

验证属性适用于大多数验证需求。 但是,某些验证规则特定于你的业务。 你的规则可能不是常见的数据验证技术,比如确保字段是必填字段或符合一系列值。 在这些情况下,自定义验证属性是一种不错的解决方案。 在 MVC 中创建你自己的自定义验证属性很简单。 只需从ValidationAttribute继承并重写IsValid方法。IsValid方法采用两个参数,第一个是名为value的对象,第二个是名为validationContext的ValidationContext对象。Value引用自定义验证程序要验证的字段中的实际值。

在下面的示例中,一项业务规则规定,用户不能将 1960 年以后发行的电影的流派设置为Classic。[ClassicMovie]属性会先检查流派,如果是经典流派,则查看发行日期是否晚于 1960 年。 如果晚于 1960 年,则验证失败。 此属性采用一个表示年份的整数参数,可用于验证数据。 可以在该属性的构造函数中捕获该参数的值,如下所示:

C#

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator{private int _year;public ClassicMovieAttribute(int year){_year = year;}protected override ValidationResult IsValid(object value, ValidationContext validationContext){Movie movie = (Movie)validationContext.ObjectInstance;if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year){return new ValidationResult(GetErrorMessage());}return ValidationResult.Success;}

上面的movie变量表示一个Movie对象,其中包含要验证的表单提交中的数据。 在此例中,验证代码会根据规则检查ClassicMovieAttribute类的IsValid方法中的日期和流派。 验证成功时,IsValid返回ValidationResult.Success代码。 验证失败时,返回ValidationResult和错误消息:

C#

private string GetErrorMessage(){return $"Classic movies must have a release year earlier than {_year}.";}

当用户修改Genre字段并提交表单时,ClassicMovieAttributeIsValid方法将验证该电影是否为经典电影。 将ClassicMovieAttribute像所有内置特性一样应用于属性(如ReleaseDate)以确保执行验证,如前面的代码示例所示。 由于此示例仅适用于Movie类型,因此建议使用IValidatableObject,如下一段中所示。

也可以通过实现IValidatableObject接口上的Validate方法,将这段代码直接放入模型中。 如果自定义验证特性可用于验证各个属性,则可使用IValidatableObject来实现类级别的验证,如下所示。

C#

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext){if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear){yield return new ValidationResult($"Classic movies must have a release year earlier than {_classicYear}.",new[] { "ReleaseDate" });}}

客户端验证

客户端验证极大地方便了用户。 它节省了时间,让用户不必浪费时间等待服务器往返。 从商业角度而言,即使每次只有几分之一秒,但如果每天有几百次,也会耗费大量的时间和成本,带来很多不必要的烦恼。 简单直接的验证能够提高用户的工作效率和投入产出比。

你必须有一个包含适当的 JavaScript 脚本引用的视图,才能让客户端验证正常工作,如下所示。

CSHTML

<script src="/ajax/jQuery/jquery-2.2.0.min.js"></script>

CSHTML

<script src="/ajax/jquery.validate/1.16.0/jquery.validate.min.js"></script><script src="/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"></script>

jQuery 非介入式验证脚本是一个基于热门 jQuery Validate 插件的自定义 Microsoft 前端库。 如果没有 jQuery 非介入式验证,则必须在两个位置编码相同的验证逻辑:一次是在模型属性上的服务器端验证特性中,一次是在客户端脚本中(jQuery Validate 的validate()方法示例展示了这种情况可能的复杂程度)。 MVC 的标记帮助程序和 HTML 帮助程序则能够使用模型属性中的验证特性和类型元数据,呈现需要验证的表单元素中的 HTML 5 data- 特性。 MVC 为内置属性和自定义属性生成data-属性。 然后,jQuery 非介入式验证分析data-属性并将逻辑传递给 jQuery Validate,从而将服务器端验证逻辑有效地“复制”到客户端。 可以使用相关标记帮助程序在客户端上显示验证错误,如下所示:

CSHTML

<div class="form-group"><label asp-for="ReleaseDate" class="col-md-2 control-label"></label><div class="col-md-10"><input asp-for="ReleaseDate" class="form-control" /><span asp-validation-for="ReleaseDate" class="text-danger"></span></div></div>

上面的标记帮助程序将呈现以下 HTML。 请注意,HTML 输出中的data-特性与ReleaseDate属性的验证特性相对应。 下面的data-val-required属性包含在用户未填写发行日期字段时将显示的错误消息。 jQuery 非介入式验证将此值传递给 jQuery Validaterequired()方法,该方法随后在随附的<span>元素中显示该消息。

HTML

<form action="/Movies/Create" method="post"><div class="form-horizontal"><h4>Movie</h4><div class="text-danger"></div><div class="form-group"><label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label><div class="col-md-10"><input class="form-control" type="datetime"data-val="true" data-val-required="The ReleaseDate field is required."id="ReleaseDate" name="ReleaseDate" value="" /><span class="text-danger field-validation-valid"data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span></div></div></div></form>

客户端验证将阻止提交,直到表单变为有效为止。 “提交”按钮运行 JavaScript:要么提交表单要么显示错误消息。

MVC 基于属性的 .NET 数据类型确定类型特性值(有可能使用[DataType]特性进行重写)。[DataType]基本特性不执行真正的服务器端验证。 浏览器选择自己的错误消息,并根据需要显示这些错误,但 jQuery 非介入式验证包可以重写消息,并使它们与其他消息的显示保持一致。 当用户应用[DataType]子类(比如[EmailAddress])时,最常发生这种情况。

向动态表单添加验证

由于 jQuery 非介入式验证会在第一次加载页面时将验证逻辑和参数传递到 jQuery Validate,因此,动态生成的表单不会自动展示验证。 你必须指示 jQuery 非介入式验证在创建动态表单后立即对其进行分析。 例如,下面的代码展示如何对通过 AJAX 添加的表单设置客户端验证。

JavaScript

$.get({url: "https://url/that/returns/a/form",dataType: "html",error: function(jqXHR, textStatus, errorThrown) {alert(textStatus + ": Couldn't add form. " + errorThrown);},success: function(newFormHTML) {var container = document.getElementById("form-container");container.insertAdjacentHTML("beforeend", newFormHTML);var forms = container.getElementsByTagName("form");var newForm = forms[forms.length - 1];$.validator.unobtrusive.parse(newForm);}})

$.validator.unobtrusive.parse()方法采用 jQuery 选择器作为它的一个参数。 此方法指示 jQuery 非介入式验证分析该选择器内表单的data-属性。 这些属性的值随后传递到 jQuery Validate 插件中,以便表单展示所需的客户端验证规则。

向动态控件添加验证

也可以在动态生成各个控件(比如<input/><select/>)时,更新表单上的验证规则。 不能将用于这些元素的选择器直接传递到parse()方法,因为周围表单已进行分析并且不会更新。 应当先删除现有的验证数据,然后重新分析整个表单,如下所示:

JavaScript

$.get({url: "https://url/that/returns/a/control",dataType: "html",error: function(jqXHR, textStatus, errorThrown) {alert(textStatus + ": Couldn't add control. " + errorThrown);},success: function(newInputHTML) {var form = document.getElementById("my-form");form.insertAdjacentHTML("beforeend", newInputHTML);$(form).removeData("validator") // Added by jQuery Validate.removeData("unobtrusiveValidation"); // Added by jQuery Unobtrusive Validation$.validator.unobtrusive.parse(form);}})

IClientModelValidator

可为自定义属性创建客户端逻辑,创建 jQuery 验证的适配器的非介入式验证将在验证过程中,在客户端上自动为你执行此逻辑。 第一步是通过实现IClientModelValidator接口来控制要添加哪些 data- 属性,如下所示:

C#

public void AddValidation(ClientModelValidationContext context){if (context == null){throw new ArgumentNullException(nameof(context));}MergeAttribute(context.Attributes, "data-val", "true");MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());var year = _year.ToString(CultureInfo.InvariantCulture);MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);}

实现此接口的属性可以将 HTML 属性添加到生成的字段。 检查ReleaseDate元素的输出时,将显示与上一示例类似的 HTML,唯一不同的是,此示例包含一个已在IClientModelValidatorAddValidation方法中定义的data-val-classicmovie属性。

HTML

<input class="form-control" type="datetime"data-val="true"data-val-classicmovie="Classic movies must have a release year earlier than 1960."data-val-classicmovie-year="1960"data-val-required="The ReleaseDate field is required."id="ReleaseDate" name="ReleaseDate" value="" />

非介入式验证使用data-属性中的数据来显示错误消息。 不过,除非将规则或消息添加到 jQuery 的validator对象,否则 jQuery 并不知道它们的存在。 如以下示例所示,将一个自定义classicmovie客户端验证方法添加到validator对象。 有关unobtrusive.adapters.add方法的说明,请参阅 MVC 中的非介入式客户端验证。

JavaScript

$.validator.addMethod('classicmovie',function (value, element, params) {// Get element value. Classic genre has value '0'.var genre = $(params[0]).val(),year = params[1],date = new Date(value);if (genre && genre.length > 0 && genre[0] === '0') {// Since this is a classic movie, invalid if release date is after given year.return date.getFullYear() <= year;}return true;});$.validator.unobtrusive.adapters.add('classicmovie',['year'],function (options) {var element = $(options.form).find('select#Genre')[0];options.rules['classicmovie'] = [element, parseInt(options.params['year'])];options.messages['classicmovie'] = options.message;});

classicmovie方法使用前面的代码对电影发行日期执行客户端验证。 如果该方法返回false,则显示错误消息。

远程验证

远程验证是一项非常不错的功能,可在需要根据服务器上的数据验证客户端上的数据时使用。 例如,应用可能需要验证某个电子邮件或用户名是否已被使用,并且它必须为此查询大量数据。 为验证一个或几个字段而下载大量数据会占用过多资源。 它还有可能暴露敏感信息。 一种替代方法是发出往返请求来验证字段。

可以分两步实现远程验证。 首先,必须使用[Remote]属性为模型添加批注。[Remote]属性采用多个重载,可用于将客户端 JavaScript 定向到要调用的相应代码。 下面的示例指向Users控制器的VerifyEmail操作方法。

C#

[Remote(action: "VerifyEmail", controller: "Users")]public string Email { get; set; }

第二步是按照[Remote]属性中的定义,将验证代码放入相应的操作方法。 根据 jQuery Validate remote 方法文档,服务器响应必须是符合以下条件的 JSON 字符串:

对于有效元素,为"true"。对于无效元素,为"false"undefinednull,使用默认错误消息。

如果服务器响应是一个字符串(例如,"That name is already taken, try peter123 instead"),则该字符串显示为一条自定义错误消息来替代默认字符串。

VerifyEmail方法的定义遵循这些规则,如下所示。 如果电子邮件已被占用,它会返回验证错误消息;如果电子邮件可用,则返回true,并将结果包装在JsonResult对象中。 然后,客户端可以使用返回的值,继续进行下一步操作或根据需要显示错误。

C#

[AcceptVerbs("Get", "Post")]public IActionResult VerifyEmail(string email){if (!_userRepository.VerifyEmail(email)){return Json($"Email {email} is already in use.");}return Json(true);}

现在,当用户输入电子邮件时,视图中的 JavaScript 会发出远程调用,以了解该电子邮件是否已被占用,如果是,则显示错误消息。 如果不是,用户就可以像往常一样提交表单。

[Remote]特性的AdditionalFields属性可用于根据服务器上的数据验证字段组合。 例如,如果上面的User模型具有两个附加属性,名为FirstNameLastName,你可能想要验证该名称对尚未被现有用户占用。 按以下代码所示定义新属性:

C#

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]public string FirstName { get; set; }[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]public string LastName { get; set; }

AdditionalFields可能已显式设置为字符串"FirstName""LastName",但使用nameof这样的操作符可简化稍后的重构过程。 然后,用于执行验证的操作方法必须采用两个参数,一个用于FirstName的值,一个用于LastName的值。

C#

[AcceptVerbs("Get", "Post")]public IActionResult VerifyName(string firstName, string lastName){if (!_userRepository.VerifyName(firstName, lastName)){return Json(data: $"A user named {firstName} {lastName} already exists.");}return Json(data: true);}

现在,当用户输入名和姓时,JavaScript 会:

发出远程调用,以了解该名称对是否已被占用。如果被占用,则显示一条错误消息。如果未被占用,则用户可以提交表单。

如果需要使用[Remote]特性验证两个或更多附加字段,可将其以逗号分隔的列表形式列出。 例如,若要向模型中添加MiddleName属性,可按以下代码所示设置[Remote]特性:

C#

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]public string MiddleName { get; set; }

AdditionalFields与所有属性参数一样,必须是常量表达式。 因此,不能使用内插字符串或调用string.Join()来初始化AdditionalFields。 对于添加到[Remote]特性的每个附加字段,都必须向相应的控制器操作方法另外添加一个参数。

如果觉得《ASP.NET Core MVC 中的模型验证》对你有帮助,请点赞、收藏,并留下你的观点哦!

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