ASP.NET Core MVC 和 Razor Pages 中的模型验证

本文介绍如何在 ASP.NET Core MVC 或 Razor Pages 应用中验证用户输入。

查看或下载示例代码如何下载)。

模型状态

模型状态表示两个子系统的错误:模型绑定和模型验证。 源自模型绑定的错误通常是数据转换错误。 例如,在一个整数字段中输入一个“x”。 模型验证在模型绑定后发生,并报告数据不符合业务规则的错误。 例如,在需要 1 到 5 之间评分的字段中输入 0。

模型绑定和模型验证都在执行控制器操作或 Razor Pages 处理程序方法之前进行。 Web 应用负责检查 ModelState.IsValid 并做出相应响应。 Web 应用通常会重新显示包含错误消息的页面,如以下 Razor Pages 示例所示:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

对于具有控制器和视图的 ASP.NET Core MVC,以下示例演示如何在控制器操作内部检查 ModelState.IsValid

public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}

如果 Web API 控制器具有 [ApiController] 特性,则它们不必检查 ModelState.IsValid。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 HTTP 400 响应。 有关详细信息,请参阅自动 HTTP 400 响应

重新运行验证

验证自动进行,但是可能需要手动进行重复验证。 例如,你可能为属性计算一个值,并且希望将属性设置为所计算的值后,再重新运行验证。 若要重新运行验证,请调用 ModelStateDictionary.ClearValidationState 来清除特定于模型的验证,然后再调用 TryValidateModel 对模型进行验证:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

验证特性

通过验证特性可以为模型属性指定验证规则。 示例应用的以下示例显示使用验证特性进行注释的模型类。 [ClassicMovie] 特性为自定义的验证特性,其他特性为内置的验证特性。 [ClassicMovieWithClientValidator] 未显示,它表示实现自定义特性的另一种方法。

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

内置特性

以下是一些内置验证特性:

System.ComponentModel.DataAnnotations 命名空间中可找到验证特性的完整列表。

错误消息

通过验证特性可以指定要为无效输入显示的错误消息。 例如:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

在内部,特性使用用于字段名的某个占位符调用 String.Format,有时还使用额外占位符。 例如:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

应用于 Name 属性时,上述代码创建的错误消息将为“名称长度必须介于 6 到 8 之间”。

若要查找为特定特性的错误消息而传递给 String.Format 的参数,请参阅 DataAnnotations 源代码

在验证错误中使用 JSON 属性名称

默认情况下,当发生验证错误时,模型验证会生成一个 ModelStateDictionary,其中属性名用作错误键。 某些应用(例如单页应用)受益于使用 JSON 属性名来处理 Web API 生成的验证错误。 以下代码将验证配置为使用 SystemTextJsonValidationMetadataProvider 以使用 JSON 属性名:

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider());
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

以下代码将验证配置为使用 NewtonsoftJsonValidationMetadataProvider 以在使用 Json.NET 时使用 JSON 属性名:

using Microsoft.AspNetCore.Mvc.NewtonsoftJson;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider());
}).AddNewtonsoftJson();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

有关使用驼峰式大小写的策略示例,请参阅 GitHub 上的 Program.cs

不可为 null 的引用类型和 [Required] 特性

验证系统将不可为 null 的参数或绑定属性视为它们具有 [Required(AllowEmptyStrings = true)] 特性。 通过启用 Nullable 上下文,MVC 将隐式开始对不可为 null 的属性或参数进行验证,就像它们已使用 [Required(AllowEmptyStrings = true)] 特性进行了特性化一样。 考虑下列代码:

public class Person
{
    public string Name { get; set; }
}

如果应用是使用 <Nullable>enable</Nullable> 构建的,则 JSON 或表单发布中缺少 Name 的值会导致验证错误。 这似乎是矛盾的,因为 [Required(AllowEmptyStrings = true)] 属性是隐含的,但这是预期的行为,因为空字符串默认转换为 null。 使用可为 null 的引用类型来实现为 Name 属性指定 NULL 或缺少的值:

public class Person
{
    public string? Name { get; set; }
}

可以通过在 Program.cs 中配置 SuppressImplicitRequiredAttributeForNonNullableReferenceTypes 来禁用此行为:

builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

[必需] 服务器上的验证

在服务器上,如果属性为 null,则认为所需值缺失。 不可为 null 的字段始终有效,并且从不显示 [Required] 属性的错误消息。

但是,不可为 null 的属性的模型绑定可能会失败,从而导致 The value '' is invalid 等错误消息。 若要为不可为 null 的类型的服务器端验证指定自定义错误消息,可使用以下选项:

  • 将字段设置为可以为 null(例如,decimal?而不是 decimal)。 Nullable<T> 值类型被视为标准的可以为 null 的类型。

  • 指定模型绑定要使用的默认错误消息,如以下示例所示:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

    有关模型绑定错误(可以为其设置默认消息)的更多信息,请参阅 DefaultModelBindingMessageProvider

[必需] 客户端上的验证

在客户端上处理不可为 null 类型和字符串的方式与在服务器上不同。 在客户端上:

  • 只有在为值输入一个输入时,才认为该值存在。 因此,客户端验证处理不可为 null 类型的方式与处理可以为 null 类型的方式相同。
  • jQuery 验证必需方法将字符串字段中的空格视为有效输入。 如果只输入空格,服务器端验证会将必需的字符串字段视为无效。

如前所述,将不可为 null 类型视为具有 [Required(AllowEmptyStrings = true)] 特性。 这意味着即使不应用 [Required(AllowEmptyStrings = true)] 特性,也可进行客户端验证。 但如果不使用该特性,将收到默认错误消息。 若要指定自定义错误消息,使用该特性。

[远程] 特性

[Remote] 特性实现客户端验证,该验证需要在服务器上调用方法,以确定字段输入是否有效。 例如,应用可能需要验证用户名是否已在使用。

若要实现远程验证:

  1. 创建可供 JavaScript 调用的操作方法。 jQuery Validation remote 方法接受 JSON 响应:

    • true 表示输入数据有效。
    • falseundefinednull 表示输入无效。 显示默认错误消息。
    • 任何其他字符串都表示输入无效。 将字符串显示为自定义错误消息。

    以下是返回自定义错误消息的操作方法示例:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. 在模型类中,使用指向验证操作方法的 [Remote] 特性注释属性,如下例所示:

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

还需要为已禁用 JavaScript 的客户端实现服务器端验证

其他字段

通过 [Remote] 特性的 AdditionalFields 属性可以根据服务器上的数据验证字段组合。 例如,如果 User 模型具有 FirstNameLastName 属性,可能需要验证该名称对尚未被现有用户占用。 下面的示例演示如何使用 AdditionalFields

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

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

AdditionalFields 可以显式设置为字符串 "FirstName" 和 "LastName",但使用 nameof 运算符可简化稍后的重构过程。 此验证的操作方法必须接受 firstNamelastName 参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

用户输入名字或姓氏时,JavaScript 会进行远程调用,查看该名称对是否已占用。

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

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

AdditionalFields 与所有属性参数一样,必须是常量表达式。 因此,请勿使用内插字符串或调用 Join 来初始化 AdditionalFields

内置特性的替代特性

如果需要并非由内置属性提供的验证,可以:

自定义特性

对于内置验证特性无法处理的情况,可以创建自定义验证特性。 创建继承自 ValidationAttribute 的类,并替代 IsValid 方法。

IsValid 方法接受名为 value 的对象,该对象是要进行验证的输入。 重载还接受 ValidationContext 对象,该对象提供其他信息,例如模型绑定创建的模型实例。

以下示例验证“经典”流派电影的发行日期是否不晚于指定年份[ClassicMovie] 属性:

  • 仅在服务器上运行。
  • 对于经典电影,验证发行日期:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

上述示例中的 movie 变量表示 Movie 对象,其中包含表单提交中的数据。 验证失败时,返回 ValidationResult 和错误消息。

IValidatableObject

上述示例只适用于 Movie 类型。 类级别验证的另一方式是在模型类中实现 IValidatableObject,如下例所示:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

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

自定义验证

以下代码演示如何在检查模型后添加模型错误:

if (Contact.Name == Contact.ShortName)
{
    ModelState.AddModelError("Contact.ShortName", 
                             "Short name can't be the same as Name.");
}

以下代码在控制器中实现验证测试:

if (contact.Name == contact.ShortName)
{
    ModelState.AddModelError(nameof(contact.ShortName),
                             "Short name can't be the same as Name.");
}

以下代码验证电话号码和电子邮件是否唯一:

public async Task<IActionResult> OnPostAsync()
{
    // Attach Validation Error Message to the Model on validation failure.          

    if (Contact.Name == Contact.ShortName)
    {
        ModelState.AddModelError("Contact.ShortName", 
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == Contact.PhoneNumber))
    {
        ModelState.AddModelError("Contact.PhoneNumber",
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == Contact.Email))
    {
        ModelState.AddModelError("Contact.Email", "The Email is already in use.");
    }

    if (!ModelState.IsValid || _context.Contact == null || Contact == null)
    {
        // if model is invalid, return the page with the model state errors.
        return Page();
    }
    _context.Contact.Add(Contact);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

以下代码在控制器中实现验证测试:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Name,ShortName,Email,PhoneNumber")] Contact contact)
{
    // Attach Validation Error Message to the Model on validation failure.
    if (contact.Name == contact.ShortName)
    {
        ModelState.AddModelError(nameof(contact.ShortName),
                                 "Short name can't be the same as Name.");
    }

    if (_context.Contact.Any(i => i.PhoneNumber == contact.PhoneNumber))
    {
        ModelState.AddModelError(nameof(contact.PhoneNumber),
                                  "The Phone number is already in use.");
    }
    if (_context.Contact.Any(i => i.Email == contact.Email))
    {
        ModelState.AddModelError(nameof(contact.Email), "The Email is already in use.");
    }

    if (ModelState.IsValid)
    {
        _context.Add(contact);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(contact);
}

通常也可以通过远程验证来检查电话号码或电子邮件的唯一性。

ValidationResult

请考虑以下自定义 ValidateNameAttribute

public class ValidateNameAttribute : ValidationAttribute
{
    public ValidateNameAttribute()
    {
        const string defaultErrorMessage = "Error with Name";
        ErrorMessage ??= defaultErrorMessage;
    }

    protected override ValidationResult? IsValid(object? value,
                                         ValidationContext validationContext)
    {
        if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
        {
            return new ValidationResult("Name is required.");
        }

        if (value.ToString()!.ToLower().Contains("zz"))
        {

            return new ValidationResult(
                        FormatErrorMessage(validationContext.DisplayName));
        }

        return ValidationResult.Success;
    }
}

在以下代码中,应用自定义 [ValidateName] 属性:

public class Contact
{
    public Guid Id { get; set; }

    [ValidateName(ErrorMessage = "Name must not contain `zz`")] 
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
}

当模型包含 zz 时,将返回新的 ValidationResult

顶级节点验证

顶级节点包括:

  • 操作参数
  • 控制器属性
  • 页处理程序参数
  • 页模型属性

除了验证模型属性之外,还验证了模型绑定的顶级节点。 在示例应用的以下示例中,VerifyPhone 方法使用 RegularExpressionAttribute 验证 phone 操作参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

顶级节点可以将 BindRequiredAttribute 与验证属性结合使用。 在示例应用的以下示例中,CheckAge 方法指定在提交表单时必须从查询字符串绑定 age 参数:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

在“检查年限”页 (CheckAge.cshtml) 中,有两个表单CheckAge.cshtml。 第一个表单将 99Age 值作为查询字符串参数提交:https://localhost:5001/Users/CheckAge?Age=99

当提交查询字符串中格式设置正确的 age 参数时,表单将进行验证。

“检查年限”页面上的第二个表单提交请求正文中的 Age 值,验证失败。 绑定失败,因为 age 参数必须来自查询字符串。

最大错误数

达到最大错误数(默认为 200)时,验证停止。 可以使用 Program.cs 中的以下代码配置该数字:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

最大递归次数

ValidationVisitor 遍历所验证模型的对象图。 对于深度或无限递归的模型,验证可能会导致堆栈溢出。 MvcOptions.MaxValidationDepth 提供了在访问者递归超过配置深度时提前停止验证的方法。 MvcOptions.MaxValidationDepth 的默认值为 32。

自动短路

如果模型图不需要验证,验证将自动短路(跳过)。 运行时为其跳过验证的对象包括基元集合(如 byte[]string[]Dictionary<string, string>)和不具有任何验证器的复杂对象图。

客户端验证

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

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 _Layout.cshtml 和 _ValidationScriptsPartial.cshtml 中的以下脚本引用支持客户端验证_Layout.cshtml_ValidationScriptsPartial.cshtml

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>

jQuery Unobtrusive Validation 脚本是一个基于热门 jQuery Validation 插件构建的自定义 Microsoft 前端库。 如果没有 jQuery 非介入式验证,则必须在两个位置编码相同的验证逻辑:一次是在模型属性上的服务器端验证特性中,一次是在客户端脚本中。 标记帮助程序HTML 帮助程序则使用模型属性中的验证特性和类型元数据,呈现需要验证的表单元素的 HTML 5 data- 特性。 jQuery Unobtrusive Validation 分析 data- 特性并将逻辑传递给 jQuery Validation,从而将服务器端验证逻辑有效地“复制”到客户端。 可以使用标记帮助程序在客户端上显示验证错误,如下所示:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

上述标记帮助程序呈现以下 HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

请注意,HTML 输出中的 data- 特性与 Movie.ReleaseDate 属性的验证特性相对应。 data-val-required 特性包含在用户未填写上映日期字段时将显示的错误消息。 jQuery Unobtrusive Validation 将此值传递给 jQuery Validation required() 方法,该方法随后在随附的 <span> 元素中显示该消息。

如果 [DataType] 特性未替代属性的 .NET 类型,则数据类型验证基于该类型。 浏览器具有自己的默认错误消息,但是 jQuery 验证非介入式验证包可以替代这些消息。 [DataType] 属性和 [EmailAddress] 等子类可让你指定错误消息。

非介入式验证

有关非介入式验证的信息,请参阅此 GitHub 问题

向动态表单添加验证

jQuery Unobtrusive Validation 会在当页面第一次加载时将验证逻辑和参数传递到 jQuery Validation。 因此,不会对动态生成的表单自动执行验证。 若要启用验证,指示 jQuery 非介入式验证在创建动态表单后立即对其进行分析。 例如,以下代码在通过 AJAX 添加的表单上设置客户端验证。

$.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 Validation 插件。

向动态控件添加验证

$.validator.unobtrusive.parse() 方法适用于整个表单,而不是 <input><select/> 等单个动态生成的控件。 若要重新分析表单,删除之前分析表单时添加的验证数据,如下例所示:

$.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 Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

自定义客户端验证

自定义客户端验证是通过生成适用于自定义 jQuery Validation 适配器的 data- HTML 特性来完成的。 以下示例适配器代码是为本文前面部分介绍的 [ClassicMovie][ClassicMovieWithClientValidator] 特性编写的:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

有关如何编写适配器的信息,请参阅 jQuery Validation 文档

给定字段的适配器的使用由 data- 特性触发,这些特性:

  • 将字段标记为正在验证 (data-val="true")。
  • 确定验证规则名称和错误消息文本(例如,data-val-rulename="Error message.")。
  • 提供验证器所需的任何其他参数(例如,data-val-rulename-param1="value")。

以下示例显示示例应用ClassicMovie 特性的 data- 特性:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

如前所述,标记帮助程序HTML 帮助程序使用验证特性的信息呈现 data- 特性。 编写用于创建自定义 data- HTML 特性的代码有以下两种方式:

用于客户端验证的 AttributeAdapter

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovie 特性使用。 若要使用此方法添加客户端验证:

  1. 为自定义验证特性创建特性适配器类。 从 AttributeAdapterBase<TAttribute>派生类。 创建将 data- 特性添加到所呈现输出中的 AddValidation 方法,如下例所示:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(
            ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
            => Attribute.GetErrorMessage();
    }
    
  2. 创建实现 IValidationAttributeAdapterProvider 的适配器提供程序类。 使用 GetAttributeAdapter 方法,将自定义属性传递给适配器的构造函数,如下例所示:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter? GetAttributeAdapter(
            ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Program.cs 中为 DI 注册适配器提供程序:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

用于客户端验证的 IClientModelValidator

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovieWithClientValidator 特性使用。 若要使用此方法添加客户端验证:

  • 在自定义验证特性中,实现 IClientModelValidator 接口并创建 AddValidation 方法。 使用 AddValidation 方法,添加 data- 特性进行验证,如下例所示:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
            => Year = year;
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext 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);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult? IsValid(
            object? value, ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value!).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

禁用客户端验证

以下代码禁用 Razor Pages 中的客户端验证:

builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

可禁用客户端验证的其他选项:

  • 在所有 .cshtml 文件中注释禁止对 _ValidationScriptsPartial 的引用。
  • 删除 Pages\Shared_ValidationScriptsPartial.cshtml 文件的内容。

上述方法不会阻止 ASP.NET Core IdentityRazor 类库的客户端验证。 有关详细信息,请参阅 ASP.NET Core 项目中的基架 Identity

问题详细信息

问题详细信息并不是描述 HTTP API 错误的唯一响应格式,但它们通常用于报告 HTTP API 的错误。

问题详细信息服务实现 IProblemDetailsService 接口,该接口支持在 ASP.NET Core 中创建问题详细信息。 IServiceCollection 上的 AddProblemDetails 扩展方法注册默认 IProblemDetailsService 实现。

在 ASP.NET Core 应用中,下列中间件会在调用 AddProblemDetails 时生成问题详细信息 HTTP 响应,除非 Accept 请求 HTTP 标头不包含注册的 IProblemDetailsWriter 支持的内容类型之一(默认:application/json):

其他资源

本文介绍如何在 ASP.NET Core MVC 或 Razor Pages 应用中验证用户输入。

查看或下载示例代码如何下载)。

模型状态

模型状态表示两个子系统的错误:模型绑定和模型验证。 源自模型绑定的错误通常是数据转换错误。 例如,在一个整数字段中输入一个“x”。 模型验证在模型绑定后发生,并报告数据不符合业务规则的错误。 例如,在需要 1 到 5 之间评分的字段中输入 0。

模型绑定和模型验证都在执行控制器操作或 Razor Pages 处理程序方法之前进行。 Web 应用负责检查 ModelState.IsValid 并做出相应响应。 Web 应用通常会重新显示包含错误消息的页面,如以下 Razor Pages 示例所示:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

对于具有控制器和视图的 ASP.NET Core MVC,以下示例演示如何在控制器操作内部检查 ModelState.IsValid

public async Task<IActionResult> Create(Movie movie)
{
    if (!ModelState.IsValid)
    {
        return View(movie);
    }

    _context.Movies.Add(movie);
    await _context.SaveChangesAsync();

    return RedirectToAction(nameof(Index));
}

如果 Web API 控制器具有 [ApiController] 特性,则它们不必检查 ModelState.IsValid。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 HTTP 400 响应。 有关详细信息,请参阅自动 HTTP 400 响应

重新运行验证

验证自动进行,但是可能需要手动进行重复验证。 例如,你可能为属性计算一个值,并且希望将属性设置为所计算的值后,再重新运行验证。 若要重新运行验证,请调用 ModelStateDictionary.ClearValidationState 来清除特定于模型的验证,然后再调用 TryValidateModel 对模型进行验证:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

验证特性

通过验证特性可以为模型属性指定验证规则。 示例应用的以下示例显示使用验证特性进行注释的模型类。 [ClassicMovie] 特性为自定义的验证特性,其他特性为内置的验证特性。 [ClassicMovieWithClientValidator] 未显示,它表示实现自定义特性的另一种方法。

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

内置特性

以下是一些内置验证特性:

System.ComponentModel.DataAnnotations 命名空间中可找到验证特性的完整列表。

错误消息

通过验证特性可以指定要为无效输入显示的错误消息。 例如:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

在内部,特性使用用于字段名的某个占位符调用 String.Format,有时还使用额外占位符。 例如:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

应用于 Name 属性时,上述代码创建的错误消息将为“名称长度必须介于 6 到 8 之间”。

若要查找为特定特性的错误消息而传递给 String.Format 的参数,请参阅 DataAnnotations 源代码

不可为 null 的引用类型和 [Required] 特性

验证系统将不可为 null 的参数或绑定属性视为它们具有 [Required(AllowEmptyStrings = true)] 特性。 通过启用 Nullable 上下文,MVC 将隐式开始对泛型类型上的不可为 null 属性或参数进行验证,就像它们已使用 [Required(AllowEmptyStrings = true)] 特性进行了特性化一样。 考虑下列代码:

public class Person
{
    public string Name { get; set; }
}

如果应用是使用 <Nullable>enable</Nullable> 构建的,则 JSON 或表单发布中缺少 Name 的值会导致验证错误。 使用可为 null 的引用类型来实现为 Name 属性指定 NULL 或缺少的值:

public class Person
{
    public string? Name { get; set; }
}

可以通过在 Program.cs 中配置 SuppressImplicitRequiredAttributeForNonNullableReferenceTypes 来禁用此行为:

builder.Services.AddControllers(
    options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

泛型类型和 [Required] 属性上的不可为 null 属性

泛型类型上的不可为 null 属性必须在需要该类型时包含 [Required] 特性。 在以下代码中,TestRequired 不是必需的:

public class WeatherForecast<T>
{
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}

在以下代码中,TestRequired 显式标记为必需:

using System.ComponentModel.DataAnnotations;

public class WeatherForecast<T>
{
    [Required]
    public string TestRequired { get; set; } = null!;
    public T? Inner { get; set; }
}

[必需] 服务器上的验证

在服务器上,如果属性为 null,则认为所需值缺失。 不可为 null 的字段始终有效,并且从不显示 [Required] 属性的错误消息。

但是,不可为 null 的属性的模型绑定可能会失败,从而导致 The value '' is invalid 等错误消息。 若要为不可为 null 的类型的服务器端验证指定自定义错误消息,可使用以下选项:

  • 将字段设置为可以为 null(例如,decimal?而不是 decimal)。 Nullable<T> 值类型被视为标准的可以为 null 的类型。

  • 指定模型绑定要使用的默认错误消息,如以下示例所示:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

    有关模型绑定错误(可以为其设置默认消息)的更多信息,请参阅 DefaultModelBindingMessageProvider

[必需] 客户端上的验证

在客户端上处理不可为 null 类型和字符串的方式与在服务器上不同。 在客户端上:

  • 只有在为值输入一个输入时,才认为该值存在。 因此,客户端验证处理不可为 null 类型的方式与处理可以为 null 类型的方式相同。
  • jQuery 验证必需方法将字符串字段中的空格视为有效输入。 如果只输入空格,服务器端验证会将必需的字符串字段视为无效。

如前所述,将不可为 null 类型视为具有 [Required(AllowEmptyStrings = true)] 特性。 这意味着即使不应用 [Required(AllowEmptyStrings = true)] 特性,也可进行客户端验证。 但如果不使用该特性,将收到默认错误消息。 若要指定自定义错误消息,使用该特性。

[远程] 特性

[Remote] 特性实现客户端验证,该验证需要在服务器上调用方法,以确定字段输入是否有效。 例如,应用可能需要验证用户名是否已在使用。

若要实现远程验证:

  1. 创建可供 JavaScript 调用的操作方法。 jQuery Validation remote 方法接受 JSON 响应:

    • true 表示输入数据有效。
    • falseundefinednull 表示输入无效。 显示默认错误消息。
    • 任何其他字符串都表示输入无效。 将字符串显示为自定义错误消息。

    以下是返回自定义错误消息的操作方法示例:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. 在模型类中,使用指向验证操作方法的 [Remote] 特性注释属性,如下例所示:

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

其他字段

通过 [Remote] 特性的 AdditionalFields 属性可以根据服务器上的数据验证字段组合。 例如,如果 User 模型具有 FirstNameLastName 属性,可能需要验证该名称对尚未被现有用户占用。 下面的示例演示如何使用 AdditionalFields

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

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

AdditionalFields 可以显式设置为字符串 "FirstName" 和 "LastName",但使用 nameof 运算符可简化稍后的重构过程。 此验证的操作方法必须接受 firstNamelastName 参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

用户输入名字或姓氏时,JavaScript 会进行远程调用,查看该名称对是否已占用。

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

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

AdditionalFields 与所有属性参数一样,必须是常量表达式。 因此,请勿使用内插字符串或调用 Join 来初始化 AdditionalFields

内置特性的替代特性

如果需要并非由内置属性提供的验证,可以:

自定义特性

对于内置验证特性无法处理的情况,可以创建自定义验证特性。 创建继承自 ValidationAttribute 的类,并替代 IsValid 方法。

IsValid 方法接受名为 value 的对象,该对象是要进行验证的输入。 重载还接受 ValidationContext 对象,该对象提供其他信息,例如模型绑定创建的模型实例。

以下示例验证“经典”流派电影的发行日期是否不晚于指定年份[ClassicMovie] 属性:

  • 仅在服务器上运行。
  • 对于经典电影,验证发行日期:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
        => Year = year;

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value!).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

上述示例中的 movie 变量表示 Movie 对象,其中包含表单提交中的数据。 验证失败时,返回 ValidationResult 和错误消息。

IValidatableObject

上述示例只适用于 Movie 类型。 类级别验证的另一方式是在模型类中实现 IValidatableObject,如下例所示:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; } = null!;

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; } = null!;

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

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

顶级节点验证

顶级节点包括:

  • 操作参数
  • 控制器属性
  • 页处理程序参数
  • 页模型属性

除了验证模型属性之外,还验证了模型绑定的顶级节点。 在示例应用的以下示例中,VerifyPhone 方法使用 RegularExpressionAttribute 验证 phone 操作参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

顶级节点可以将 BindRequiredAttribute 与验证属性结合使用。 在示例应用的以下示例中,CheckAge 方法指定在提交表单时必须从查询字符串绑定 age 参数:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

在“检查年限”页 (CheckAge.cshtml) 中,有两个表单CheckAge.cshtml。 第一个表单将 99Age 值作为查询字符串参数提交:https://localhost:5001/Users/CheckAge?Age=99

当提交查询字符串中格式设置正确的 age 参数时,表单将进行验证。

“检查年限”页面上的第二个表单提交请求正文中的 Age 值,验证失败。 绑定失败,因为 age 参数必须来自查询字符串。

最大错误数

达到最大错误数(默认为 200)时,验证停止。 可以使用 Program.cs 中的以下代码配置该数字:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

builder.Services.AddSingleton
    <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

最大递归次数

ValidationVisitor 遍历所验证模型的对象图。 对于深度或无限递归的模型,验证可能会导致堆栈溢出。 MvcOptions.MaxValidationDepth 提供了在访问者递归超过配置深度时提前停止验证的方法。 MvcOptions.MaxValidationDepth 的默认值为 32。

自动短路

如果模型图不需要验证,验证将自动短路(跳过)。 运行时为其跳过验证的对象包括基元集合(如 byte[]string[]Dictionary<string, string>)和不具有任何验证器的复杂对象图。

客户端验证

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

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 _Layout.cshtml 和 _ValidationScriptsPartial.cshtml 中的以下脚本引用支持客户端验证_Layout.cshtml_ValidationScriptsPartial.cshtml

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>

jQuery Unobtrusive Validation 脚本是一个基于热门 jQuery Validation 插件构建的自定义 Microsoft 前端库。 如果没有 jQuery 非介入式验证,则必须在两个位置编码相同的验证逻辑:一次是在模型属性上的服务器端验证特性中,一次是在客户端脚本中。 标记帮助程序HTML 帮助程序则使用模型属性中的验证特性和类型元数据,呈现需要验证的表单元素的 HTML 5 data- 特性。 jQuery Unobtrusive Validation 分析 data- 特性并将逻辑传递给 jQuery Validation,从而将服务器端验证逻辑有效地“复制”到客户端。 可以使用标记帮助程序在客户端上显示验证错误,如下所示:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

上述标记帮助程序呈现以下 HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

请注意,HTML 输出中的 data- 特性与 Movie.ReleaseDate 属性的验证特性相对应。 data-val-required 特性包含在用户未填写上映日期字段时将显示的错误消息。 jQuery Unobtrusive Validation 将此值传递给 jQuery Validation required() 方法,该方法随后在随附的 <span> 元素中显示该消息。

如果 [DataType] 特性未替代属性的 .NET 类型,则数据类型验证基于该类型。 浏览器具有自己的默认错误消息,但是 jQuery 验证非介入式验证包可以替代这些消息。 [DataType] 属性和 [EmailAddress] 等子类可让你指定错误消息。

非介入式验证

有关非介入式验证的信息,请参阅此 GitHub 问题

向动态表单添加验证

jQuery Unobtrusive Validation 会在当页面第一次加载时将验证逻辑和参数传递到 jQuery Validation。 因此,不会对动态生成的表单自动执行验证。 若要启用验证,指示 jQuery 非介入式验证在创建动态表单后立即对其进行分析。 例如,以下代码在通过 AJAX 添加的表单上设置客户端验证。

$.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 Validation 插件。

向动态控件添加验证

$.validator.unobtrusive.parse() 方法适用于整个表单,而不是 <input><select/> 等单个动态生成的控件。 若要重新分析表单,删除之前分析表单时添加的验证数据,如下例所示:

$.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 Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

自定义客户端验证

自定义客户端验证是通过生成适用于自定义 jQuery Validation 适配器的 data- HTML 特性来完成的。 以下示例适配器代码是为本文前面部分介绍的 [ClassicMovie][ClassicMovieWithClientValidator] 特性编写的:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

有关如何编写适配器的信息,请参阅 jQuery Validation 文档

给定字段的适配器的使用由 data- 特性触发,这些特性:

  • 将字段标记为正在验证 (data-val="true")。
  • 确定验证规则名称和错误消息文本(例如,data-val-rulename="Error message.")。
  • 提供验证器所需的任何其他参数(例如,data-val-rulename-param1="value")。

以下示例显示示例应用ClassicMovie 特性的 data- 特性:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

如前所述,标记帮助程序HTML 帮助程序使用验证特性的信息呈现 data- 特性。 编写用于创建自定义 data- HTML 特性的代码有以下两种方式:

用于客户端验证的 AttributeAdapter

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovie 特性使用。 若要使用此方法添加客户端验证:

  1. 为自定义验证特性创建特性适配器类。 从 AttributeAdapterBase<TAttribute>派生类。 创建将 data- 特性添加到所呈现输出中的 AddValidation 方法,如下例所示:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(
            ClassicMovieAttribute attribute, IStringLocalizer? stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
            => Attribute.GetErrorMessage();
    }
    
  2. 创建实现 IValidationAttributeAdapterProvider 的适配器提供程序类。 使用 GetAttributeAdapter 方法,将自定义属性传递给适配器的构造函数,如下例所示:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter? GetAttributeAdapter(
            ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Program.cs 中为 DI 注册适配器提供程序:

    builder.Services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    builder.Services.AddSingleton
        <IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();
    

用于客户端验证的 IClientModelValidator

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovieWithClientValidator 特性使用。 若要使用此方法添加客户端验证:

  • 在自定义验证特性中,实现 IClientModelValidator 接口并创建 AddValidation 方法。 使用 AddValidation 方法,添加 data- 特性进行验证,如下例所示:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
            => Year = year;
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext 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);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult? IsValid(
            object? value, ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value!).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

禁用客户端验证

以下代码禁用 Razor Pages 中的客户端验证:

builder.Services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

可禁用客户端验证的其他选项:

  • 在所有 .cshtml 文件中注释禁止对 _ValidationScriptsPartial 的引用。
  • 删除 Pages\Shared_ValidationScriptsPartial.cshtml 文件的内容。

上述方法不会阻止 ASP.NET Core IdentityRazor 类库的客户端验证。 有关详细信息,请参阅 ASP.NET Core 项目中的基架 Identity

其他资源

本文介绍如何在 ASP.NET Core MVC 或 Razor Pages 应用中验证用户输入。

查看或下载示例代码如何下载)。

模型状态

模型状态表示两个子系统的错误:模型绑定和模型验证。 源自模型绑定的错误通常是数据转换错误。 例如,在一个整数字段中输入一个“x”。 模型验证在模型绑定后发生,并报告数据不符合业务规则的错误。 例如,在需要 1 到 5 之间评分的字段中输入 0。

模型绑定和模型验证都在执行控制器操作或 Razor Pages 处理程序方法之前进行。 Web 应用负责检查 ModelState.IsValid 并做出相应响应。 Web 应用通常会重新显示带有错误消息的页面:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

如果 Web API 控制器具有 [ApiController] 特性,则它们不必检查 ModelState.IsValid。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 HTTP 400 响应。 有关详细信息,请参阅自动 HTTP 400 响应

重新运行验证

验证自动进行,但是可能需要手动进行重复验证。 例如,你可能为属性计算一个值,并且希望将属性设置为所计算的值后,再重新运行验证。 若要重新运行验证,请调用 ModelStateDictionary.ClearValidationState 来清除特定于模型的验证,然后再调用 TryValidateModel 对模型进行验证:

public async Task<IActionResult> OnPostTryValidateAsync()
{
    var modifiedReleaseDate = DateTime.Now.Date;
    Movie.ReleaseDate = modifiedReleaseDate;

    ModelState.ClearValidationState(nameof(Movie));
    if (!TryValidateModel(Movie, nameof(Movie)))
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

验证特性

通过验证特性可以为模型属性指定验证规则。 示例应用的以下示例显示使用验证特性进行注释的模型类。 [ClassicMovie] 特性为自定义的验证特性,其他特性为内置的验证特性。 [ClassicMovieWithClientValidator] 未显示,它表示实现自定义特性的另一种方法。

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

内置特性

以下是一些内置验证特性:

System.ComponentModel.DataAnnotations 命名空间中可找到验证特性的完整列表。

错误消息

通过验证特性可以指定要为无效输入显示的错误消息。 例如:

[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]

在内部,特性使用用于字段名的某个占位符调用 String.Format,有时还使用额外占位符。 例如:

[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]

应用于 Name 属性时,上述代码创建的错误消息将为“名称长度必须介于 6 到 8 之间”。

若要查找为特定特性的错误消息而传递给 String.Format 的参数,请参阅 DataAnnotations 源代码

不可为 null 的引用类型和 [Required] 特性

验证系统将不可为 null 的参数或绑定属性视为它们具有 [Required(AllowEmptyStrings = true)] 特性。 通过启用 Nullable 上下文,MVC 将隐式开始对不可为 null 的属性或参数进行验证,就像它们已使用 [Required(AllowEmptyStrings = true)] 特性进行了特性化一样。 考虑下列代码:

public class Person
{
    public string Name { get; set; }
}

如果应用是使用 <Nullable>enable</Nullable> 构建的,则 JSON 或表单发布中缺少 Name 的值会导致验证错误。 使用可为 null 的引用类型来实现为 Name 属性指定 NULL 或缺少的值:

public class Person
{
    public string? Name { get; set; }
}

可以通过在 Startup.ConfigureServices 中配置 SuppressImplicitRequiredAttributeForNonNullableReferenceTypes 来禁用此行为:

services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

[必需] 服务器上的验证

在服务器上,如果属性为 null,则认为所需值缺失。 不可为 null 的字段始终有效,并且从不显示 [Required] 属性的错误消息。

但是,不可为 null 的属性的模型绑定可能会失败,从而导致 The value '' is invalid 等错误消息。 若要为不可为 null 的类型的服务器端验证指定自定义错误消息,可使用以下选项:

  • 将字段设置为可以为 null(例如,decimal?而不是 decimal)。 Nullable<T> 值类型被视为标准的可以为 null 的类型。

  • 指定模型绑定要使用的默认错误消息,如以下示例所示:

    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    services.AddSingleton<IValidationAttributeAdapterProvider,
        CustomValidationAttributeAdapterProvider>();
    

    有关模型绑定错误(可以为其设置默认消息)的更多信息,请参阅 DefaultModelBindingMessageProvider

[必需] 客户端上的验证

在客户端上处理不可为 null 类型和字符串的方式与在服务器上不同。 在客户端上:

  • 只有在为值输入一个输入时,才认为该值存在。 因此,客户端验证处理不可为 null 类型的方式与处理可以为 null 类型的方式相同。
  • jQuery 验证必需方法将字符串字段中的空格视为有效输入。 如果只输入空格,服务器端验证会将必需的字符串字段视为无效。

如前所述,将不可为 null 类型视为具有 [Required(AllowEmptyStrings = true)] 特性。 这意味着即使不应用 [Required(AllowEmptyStrings = true)] 特性,也可进行客户端验证。 但如果不使用该特性,将收到默认错误消息。 若要指定自定义错误消息,使用该特性。

[远程] 特性

[Remote] 特性实现客户端验证,该验证需要在服务器上调用方法,以确定字段输入是否有效。 例如,应用可能需要验证用户名是否已在使用。

若要实现远程验证:

  1. 创建可供 JavaScript 调用的操作方法。 jQuery Validation remote 方法接受 JSON 响应:

    • true 表示输入数据有效。
    • falseundefinednull 表示输入无效。 显示默认错误消息。
    • 任何其他字符串都表示输入无效。 将字符串显示为自定义错误消息。

    以下是返回自定义错误消息的操作方法示例:

    [AcceptVerbs("GET", "POST")]
    public IActionResult VerifyEmail(string email)
    {
        if (!_userService.VerifyEmail(email))
        {
            return Json($"Email {email} is already in use.");
        }
    
        return Json(true);
    }
    
  2. 在模型类中,使用指向验证操作方法的 [Remote] 特性注释属性,如下例所示:

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

其他字段

通过 [Remote] 特性的 AdditionalFields 属性可以根据服务器上的数据验证字段组合。 例如,如果 User 模型具有 FirstNameLastName 属性,可能需要验证该名称对尚未被现有用户占用。 下面的示例演示如何使用 AdditionalFields

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

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

AdditionalFields 可以显式设置为字符串 "FirstName" 和 "LastName",但使用 nameof 运算符可简化稍后的重构过程。 此验证的操作方法必须接受 firstNamelastName 参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userService.VerifyName(firstName, lastName))
    {
        return Json($"A user named {firstName} {lastName} already exists.");
    }

    return Json(true);
}

用户输入名字或姓氏时,JavaScript 会进行远程调用,查看该名称对是否已占用。

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

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

AdditionalFields 与所有属性参数一样,必须是常量表达式。 因此,请勿使用内插字符串或调用 Join 来初始化 AdditionalFields

内置特性的替代特性

如果需要并非由内置属性提供的验证,可以:

自定义特性

对于内置验证特性无法处理的情况,可以创建自定义验证特性。 创建继承自 ValidationAttribute 的类,并替代 IsValid 方法。

IsValid 方法接受名为 value 的对象,该对象是要进行验证的输入。 重载还接受 ValidationContext 对象,该对象提供其他信息,例如模型绑定创建的模型实例。

以下示例验证“经典”流派电影的发行日期是否不晚于指定年份[ClassicMovie] 属性:

  • 仅在服务器上运行。
  • 对于经典电影,验证发行日期:
public class ClassicMovieAttribute : ValidationAttribute
{
    public ClassicMovieAttribute(int year)
    {
        Year = year;
    }

    public int Year { get; }

    public string GetErrorMessage() =>
        $"Classic movies must have a release year no later than {Year}.";

    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        var movie = (Movie)validationContext.ObjectInstance;
        var releaseYear = ((DateTime)value).Year;

        if (movie.Genre == Genre.Classic && releaseYear > Year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }
}

上述示例中的 movie 变量表示 Movie 对象,其中包含表单提交中的数据。 验证失败时,返回 ValidationResult 和错误消息。

IValidatableObject

上述示例只适用于 Movie 类型。 类级别验证的另一方式是在模型类中实现 IValidatableObject,如下例所示:

public class ValidatableMovie : IValidatableObject
{
    private const int _classicYear = 1960;

    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    [Display(Name = "Release Date")]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    public Genre Genre { get; set; }

    public bool Preorder { get; set; }

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

顶级节点验证

顶级节点包括:

  • 操作参数
  • 控制器属性
  • 页处理程序参数
  • 页模型属性

除了验证模型属性之外,还验证了模型绑定的顶级节点。 在示例应用的以下示例中,VerifyPhone 方法使用 RegularExpressionAttribute 验证 phone 操作参数:

[AcceptVerbs("GET", "POST")]
public IActionResult VerifyPhone(
    [RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
{
    if (!ModelState.IsValid)
    {
        return Json($"Phone {phone} has an invalid format. Format: ###-###-####");
    }

    return Json(true);
}

顶级节点可以将 BindRequiredAttribute 与验证属性结合使用。 在示例应用的以下示例中,CheckAge 方法指定在提交表单时必须从查询字符串绑定 age 参数:

[HttpPost]
public IActionResult CheckAge([BindRequired, FromQuery] int age)
{

在“检查年限”页 (CheckAge.cshtml) 中,有两个表单CheckAge.cshtml。 第一个表单将 99Age 值作为查询字符串参数提交:https://localhost:5001/Users/CheckAge?Age=99

当提交查询字符串中格式设置正确的 age 参数时,表单将进行验证。

“检查年限”页面上的第二个表单提交请求正文中的 Age 值,验证失败。 绑定失败,因为 age 参数必须来自查询字符串。

最大错误数

达到最大错误数(默认为 200)时,验证停止。 可以使用 Startup.ConfigureServices 中的以下代码配置该数字:

services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.MaxModelValidationErrors = 50;
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
            _ => "The field is required.");
    });

services.AddSingleton<IValidationAttributeAdapterProvider,
    CustomValidationAttributeAdapterProvider>();

最大递归次数

ValidationVisitor 遍历所验证模型的对象图。 对于深度或无限递归的模型,验证可能会导致堆栈溢出。 MvcOptions.MaxValidationDepth 提供了在访问者递归超过配置深度时提前停止验证的方法。 MvcOptions.MaxValidationDepth 的默认值为 32。

自动短路

如果模型图不需要验证,验证将自动短路(跳过)。 运行时为其跳过验证的对象包括基元集合(如 byte[]string[]Dictionary<string, string>)和不具有任何验证器的复杂对象图。

客户端验证

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

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 _Layout.cshtml 和 _ValidationScriptsPartial.cshtml 中的以下脚本引用支持客户端验证_Layout.cshtml_ValidationScriptsPartial.cshtml

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.js"></script>

jQuery Unobtrusive Validation 脚本是一个基于热门 jQuery Validation 插件构建的自定义 Microsoft 前端库。 如果没有 jQuery 非介入式验证,则必须在两个位置编码相同的验证逻辑:一次是在模型属性上的服务器端验证特性中,一次是在客户端脚本中。 标记帮助程序HTML 帮助程序则使用模型属性中的验证特性和类型元数据,呈现需要验证的表单元素的 HTML 5 data- 特性。 jQuery Unobtrusive Validation 分析 data- 特性并将逻辑传递给 jQuery Validation,从而将服务器端验证逻辑有效地“复制”到客户端。 可以使用标记帮助程序在客户端上显示验证错误,如下所示:

<div class="form-group">
    <label asp-for="Movie.ReleaseDate" class="control-label"></label>
    <input asp-for="Movie.ReleaseDate" class="form-control" />
    <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>

上述标记帮助程序呈现以下 HTML:

<div class="form-group">
    <label class="control-label" for="Movie_ReleaseDate">Release Date</label>
    <input class="form-control" type="date" data-val="true"
        data-val-required="The Release Date field is required."
        id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
    <span class="text-danger field-validation-valid"
        data-valmsg-for="Movie.ReleaseDate" data-valmsg-replace="true"></span>
</div>

请注意,HTML 输出中的 data- 特性与 Movie.ReleaseDate 属性的验证特性相对应。 data-val-required 特性包含在用户未填写上映日期字段时将显示的错误消息。 jQuery Unobtrusive Validation 将此值传递给 jQuery Validation required() 方法,该方法随后在随附的 <span> 元素中显示该消息。

如果 [DataType] 特性未替代属性的 .NET 类型,则数据类型验证基于该类型。 浏览器具有自己的默认错误消息,但是 jQuery 验证非介入式验证包可以替代这些消息。 [DataType] 属性和 [EmailAddress] 等子类可让你指定错误消息。

非介入式验证

有关非介入式验证的信息,请参阅此 GitHub 问题

向动态表单添加验证

jQuery Unobtrusive Validation 会在当页面第一次加载时将验证逻辑和参数传递到 jQuery Validation。 因此,不会对动态生成的表单自动执行验证。 若要启用验证,指示 jQuery 非介入式验证在创建动态表单后立即对其进行分析。 例如,以下代码在通过 AJAX 添加的表单上设置客户端验证。

$.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 Validation 插件。

向动态控件添加验证

$.validator.unobtrusive.parse() 方法适用于整个表单,而不是 <input><select/> 等单个动态生成的控件。 若要重新分析表单,删除之前分析表单时添加的验证数据,如下例所示:

$.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 Validation
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

自定义客户端验证

自定义客户端验证是通过生成适用于自定义 jQuery Validation 适配器的 data- HTML 特性来完成的。 以下示例适配器代码是为本文前面部分介绍的 [ClassicMovie][ClassicMovieWithClientValidator] 特性编写的:

$.validator.addMethod('classicmovie', function (value, element, params) {
    var genre = $(params[0]).val(), year = params[1], date = new Date(value);

    // The Classic genre has a value of '0'.
    if (genre && genre.length > 0 && genre[0] === '0') {
        // The release date for a Classic is valid if it's no greater than the given year.
        return date.getUTCFullYear() <= year;
    }

    return true;
});

$.validator.unobtrusive.adapters.add('classicmovie', ['year'], function (options) {
    var element = $(options.form).find('select#Movie_Genre')[0];

    options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
    options.messages['classicmovie'] = options.message;
});

有关如何编写适配器的信息,请参阅 jQuery Validation 文档

给定字段的适配器的使用由 data- 特性触发,这些特性:

  • 将字段标记为正在验证 (data-val="true")。
  • 确定验证规则名称和错误消息文本(例如,data-val-rulename="Error message.")。
  • 提供验证器所需的任何其他参数(例如,data-val-rulename-param1="value")。

以下示例显示示例应用ClassicMovie 特性的 data- 特性:

<input class="form-control" type="date"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year no later than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The Release Date field is required."
    id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">

如前所述,标记帮助程序HTML 帮助程序使用验证特性的信息呈现 data- 特性。 编写用于创建自定义 data- HTML 特性的代码有以下两种方式:

用于客户端验证的 AttributeAdapter

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovie 特性使用。 若要使用此方法添加客户端验证:

  1. 为自定义验证特性创建特性适配器类。 从 AttributeAdapterBase<TAttribute>派生类。 创建将 data- 特性添加到所呈现输出中的 AddValidation 方法,如下例所示:

    public class ClassicMovieAttributeAdapter : AttributeAdapterBase<ClassicMovieAttribute>
    {
        public ClassicMovieAttributeAdapter(ClassicMovieAttribute attribute,
            IStringLocalizer stringLocalizer)
            : base(attribute, stringLocalizer)
        {
    
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage(context));
    
            var year = Attribute.Year.ToString(CultureInfo.InvariantCulture);
            MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
        }
    
        public override string GetErrorMessage(ModelValidationContextBase validationContext) =>
            Attribute.GetErrorMessage();
    }
    
  2. 创建实现 IValidationAttributeAdapterProvider 的适配器提供程序类。 使用 GetAttributeAdapter 方法,将自定义属性传递给适配器的构造函数,如下例所示:

    public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    {
        private readonly IValidationAttributeAdapterProvider baseProvider =
            new ValidationAttributeAdapterProvider();
    
        public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute,
            IStringLocalizer stringLocalizer)
        {
            if (attribute is ClassicMovieAttribute classicMovieAttribute)
            {
                return new ClassicMovieAttributeAdapter(classicMovieAttribute, stringLocalizer);
            }
    
            return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
    
  3. Startup.ConfigureServices 中为 DI 注册适配器提供程序:

    services.AddRazorPages()
        .AddMvcOptions(options =>
        {
            options.MaxModelValidationErrors = 50;
            options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(
                _ => "The field is required.");
        });
    
    services.AddSingleton<IValidationAttributeAdapterProvider,
        CustomValidationAttributeAdapterProvider>();
    

用于客户端验证的 IClientModelValidator

在 HTML 中呈现 data- 特性的方法在示例应用中由 ClassicMovieWithClientValidator 特性使用。 若要使用此方法添加客户端验证:

  • 在自定义验证特性中,实现 IClientModelValidator 接口并创建 AddValidation 方法。 使用 AddValidation 方法,添加 data- 特性进行验证,如下例所示:

    public class ClassicMovieWithClientValidatorAttribute :
        ValidationAttribute, IClientModelValidator
    {
        public ClassicMovieWithClientValidatorAttribute(int year)
        {
            Year = year;
        }
    
        public int Year { get; }
    
        public void AddValidation(ClientModelValidationContext 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);
        }
    
        public string GetErrorMessage() =>
            $"Classic movies must have a release year no later than {Year}.";
    
        protected override ValidationResult IsValid(object value,
            ValidationContext validationContext)
        {
            var movie = (Movie)validationContext.ObjectInstance;
            var releaseYear = ((DateTime)value).Year;
    
            if (movie.Genre == Genre.Classic && releaseYear > Year)
            {
                return new ValidationResult(GetErrorMessage());
            }
    
            return ValidationResult.Success;
        }
    
        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
    
            attributes.Add(key, value);
            return true;
        }
    }
    

禁用客户端验证

以下代码禁用 Razor Pages 中的客户端验证:

services.AddRazorPages()
    .AddViewOptions(options =>
    {
        options.HtmlHelperOptions.ClientValidationEnabled = false;
    });

可禁用客户端验证的其他选项:

  • 在所有 .cshtml 文件中注释禁止对 _ValidationScriptsPartial 的引用。
  • 删除 Pages\Shared_ValidationScriptsPartial.cshtml 文件的内容。

上述方法不会阻止 ASP.NET Core IdentityRazor 类库的客户端验证。 有关详细信息,请参阅 ASP.NET Core 项目中的基架 Identity

其他资源