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 控制器不必检查 ModelState.IsValid 它们是否具有 [ApiController] 属性。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 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();

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

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

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

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

如果应用是使用 <Nullable>enable</Nullable>生成的,则 ON 或表单帖子中 JS缺少 的值Name会导致验证错误。 使用可为 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 远程 方法需要 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) ,有两种形式。 第一个表单将 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:要么提交表单要么显示错误消息。

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 以下脚本引用 和 _ValidationScriptsPartial.cshtml_Layout.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> 元素中显示该消息。

数据类型验证基于属性的 .NET 类型,除非该类型被 [DataType] 属性重写。 浏览器具有自己的默认错误消息,但是 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")。

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

<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

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

  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

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

  • 在自定义验证特性中,实现 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;
    });

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

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

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

其他资源

本文介绍如何在 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 控制器不必检查 ModelState.IsValid 它们是否具有 [ApiController] 属性。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 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(AllowEmptyStrings = true)] 特性。 通过 启用 Nullable 上下文,MVC 隐式开始验证 非泛型类型 或参数上的不可为空属性,就好像它们已使用 [Required(AllowEmptyStrings = true)] 特性进行特性化一样。 请考虑以下代码:

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

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

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

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

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

泛型类型和 [必需] 属性上的不可为 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 远程 方法需要 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) ,有两种形式。 第一个表单将 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:要么提交表单要么显示错误消息。

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 以下脚本引用 和 _ValidationScriptsPartial.cshtml_Layout.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> 元素中显示该消息。

数据类型验证基于属性的 .NET 类型,除非该类型被 [DataType] 属性重写。 浏览器具有自己的默认错误消息,但是 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")。

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

<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

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

  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

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

  • 在自定义验证特性中,实现 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;
    });

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

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

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

其他资源

本文介绍如何在 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 控制器不必检查 ModelState.IsValid 它们是否具有 [ApiController] 属性。 在此情况下,如果模型状态无效,将返回包含错误详细信息的自动 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>生成的,则 ON 或表单帖子中 JS缺少 的值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 验证 远程 方法需要 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 接受名为 的对象,该值是要验证的输入。 重载还接受 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) 中,有两种形式。 第一个表单将 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:要么提交表单要么显示错误消息。

表单上存在输入错误时,客户端验证会避免到服务器的不必要往返。 以下脚本引用 和 _ValidationScriptsPartial.cshtml_Layout.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> 元素中显示该消息。

数据类型验证基于属性的 .NET 类型,除非它被 [DataType] 属性重写。 浏览器具有自己的默认错误消息,但是 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")。

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

<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

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

  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

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

  • 在自定义验证特性中,实现 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;
    });

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

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

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

其他资源