DataAnnotations
DataAnnotations是.Net(Core)框架内置的表单验证中间件,当前它的版本基本与.Net(Core)框架一致,.Net6框架最后一次更新的日期是:2022/4/13;版本号是:.NET 6.0.4(https://github.com/dotnet/core/releases/tag/v6.0.4),由于 .Net6框架是开源的DataAnnotations也应该是开源的,但我没有在.NET 6.0.4找到DataAnnotations。
DataAnnotations的定义和使用遵循了微软的一贯特征:使用简单(傻瓜)、封装严密(基本处于黑盒状态)、扩展简单,但可扩展性差,所以DataAnnotations表单验证的实现是学习和使用成本最低的。在一系列微软所定义的组件中,使用最为复杂的就是.Net(Core)框架,这极大可能是由于 .Net(Core)框架开源的因素,但这种使用的复杂性随着版本的迭代而降低,同时封装严密程序也随着版本的迭代而增加这从.Net(Core)框架1-6版本中的管道和依赖注入的配置实例就能直接直观的感受到这种不断改变的趋势。
模型类与DataAnnotations中的标记(过滤/令牌)属性相结合,来实现对表单输入操作进行验证,因此模型类与DataAnnotations之间的耦合程序是极高的,另外标记(过滤/令牌)虽然在VisualStudio开发中十分常见,但使用起来总是不如类和方法的调用或继承使用起来那么自然。
远程验证
远程验证是DataAnnotations表单输入验证的1个相对复杂的使用和实现,在.Net(Core)框架中有两种方法来实现表单的输入验证它们分别是:[AcceptVerbs("GET", "POST")]和JsonResult
1、使用远程验证需要先在WebDataAnnotations.Models.DefaultModel模型类的属性成员上定义相应的标记(过滤/令牌)
[Remote(action: "VerifyName", controller: "Home", AdditionalFields = nameof(Name))]
public string Name { get; set; }
[Remote(action: "VerifyEmail", controller: "Home", AdditionalFields = nameof(Email))]
public string Email { get; set; }
2、在WebDataAnnotations.Controllers.HomeController类中定义[AcceptVerbs("GET", "POST")]和JsonResult方法。
[AcceptVerbs("GET", "POST")]
/// <param name="name">指定用户的姓名。</param>
/// <summary>
/// 【验证姓名】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,远程验证表单中所输入的用户名是否已经被注册。
/// </remarks>
/// <returns>
/// 返回:
/// JSON编号格式的验证结果状态信息。
/// </returns>
/// </summary>
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyName(string name)
{
if (name.Trim().Equals("zz", StringComparison.InvariantCultureIgnoreCase))
{
return Json($"该用户名:{name}已经被注册。");
}
return Json(true);
} }
JsonResult
/// <param name="name">指定用户的电子邮箱。</param>
/// <summary>
/// 【验证电子邮箱】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,远程验证表单中所输入的电子邮箱是否已经被注册。
/// </remarks>
/// <returns>
/// 返回:
/// JSON编码格式的验证结果状态信息。
/// </returns>
/// </summary>
public JsonResult VerifyEmail(string email)
{
if (email.Trim().Equals("admin@yourStore.com", StringComparison.InvariantCultureIgnoreCase))
{
return Json($"该电子邮箱:{email}已经被注册。");
}
return Json(true);
}
下拉框验证
1、对下拉框控件进行远程验证需要先在WebDataAnnotations.Models.DefaultModel模型类的属性成员上定义相应的标记(过滤/令牌)
[Required, Range(1, int.MaxValue, ErrorMessage = "必须选择1个城市")]
public int CityId { get; set; }
复选框验证 (MustBeTrue(自定义扩展标记(过滤/令牌)))
1、自定义MustBeTrueAttribute标记:
/// <summary>
/// 【必须复选属性--类】
/// <remarks>
/// 摘要:
/// 通过该实体类及其方法成员实现,将页面表单中指定(复选框控件)属性与指定的验证标签及其样式进行绑定,为验证信息的呈现输出提供方法支撑。
/// </remarks>
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MustBeTrueAttribute : ValidationAttribute, IClientModelValidator
{
#region 方法--私有/保护
/// <param name="attributes">用于存储键/值对项的字典实例。</param>
/// <param name="key">指键的常量字符串。</param>
/// <param name="value">指定键所对应的字符串类型的值。</param>
/// <summary>
/// 【拼接属性?】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,获取1个值false(包含)/true(不包含),该值指示字典实例中是否不包含1个指定的键/值对项,如果不包含则把该键/值对项存储到字典实例中。
/// </remarks>
/// <returns>
/// 返回:
/// 1个值false(包含)/true(不包含)。
/// </returns>
/// </summary>
private bool MergeAttribute(
IDictionary<string, string> attributes,
string key,
string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
#endregion
#region 方法--接口实现--IClientModelValidator
/// <param name="context">客户端模型验证上下文实例。</param>
/// <summary>
/// 【添加验证】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,将页面表单中指定属性与指定的验证标签及其样式进行绑定,为验证信息的呈现输出提供方法支撑。
/// </remarks>
/// </summary>
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMsg = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-mustbetrue", errorMsg);
}
#endregion
#region 方法--覆写--ValidationAttribute
/// <param name="value">1个指定的泛型实例的实例值。</param>
/// <summary>
/// 【有效?】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,获取1个值false(不为真)/true(为真),该值指示1个指定的泛型实例的实例值是否为真(true)。
/// </remarks>
/// <returns>
/// 返回:
/// 1个值false(不为真)/true(为真)
/// </returns>
/// </summary>
public override bool IsValid(object value)
{
return value != null && (bool)value == true;
}
#endregion
}
2、 @*自定义验证信息呈现标签*@
<div class="form-check-label">
<span asp-validation-for="PrivacyPolicy" class="text-danger"></span>
</div>
3、自定义Jquery验证代码
section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$.validator.addMethod("MustBeTrue",
function (value, element, parameters) {
return element.checked;
});
$.validator.unobtrusive.adapters.add("MustBeTrue", [], function (options) {
options.rules.mustbetrue = {};
options.messages["MustBeTrue"] = options.message;
});
</script>
}
注意:
自定义Jquery验证代码必须依赖于:“jquery.validate.min.js”和“jquery.validate.unobtrusive.min.js”,才能正常执行自定义Jquery验证代码,由于_ValidationScriptsPartial.cshtml页面对“jquery.validate.min.js”和“jquery.validate.unobtrusive.min.js”进行了预先的绑定,所以<script type="text/javascript">中的自定义Jquery验证代码,必须定义在“@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}”之后,否则自定义Jquery验证代码将不会起到任何的验证效果。
本地化 /全球化DataAnnotations 验证
DataAnnotations最为简单的本地化实现是,通过“DisplayName”标记例如:[DisplayName("姓名")],引用于“System.ComponentModel”;或“Display”标记例如:[Display(Name = "姓名")],引用于“Microsoft.AspNetCore.Mvc.Rendering”,本人建议使用第2种标记方式。另外DataAnnotations也可以与多个指定的xml文件进行绑定结合特定客户端所使用浏览器中的语言(中文)和文化特征(简/繁体中文)数据信息,来动态的来实现程序中表单属性成员及其表单验证信息的本地化 /全球化的渲染和呈现,nopCommerce程序的本地化 /全球化就是这些做的。这里我只讲述其实现原理,至于具体实现我将另行进行讲述。
FluentValidation
FluentValidation表单输入验证分离了模型类与验证实现,从而降低了相关的耦合程序度;验证和规则操作被定义在类中这比标记(过滤/令牌)更加符合使用习惯再加上开源的因素,从而使FluentValidation更加容易理解。我不知识FluentAPI是否与FluentValidation是同1个开发团队进行开发且维护的两者的实现机制和绝大部分的具体实现是相同的。
FluentValidation在8.0以后的版本中进行了重构式的更改和.Net(Core)框架的推出,现在针对这些两个重大改变的示例还比较少,特别是针对自定义验证,相对来说FluentValidation在10.x版本以后对比于DataAnnotations的实现还是比较复杂的,另外需要特别注意的是:到目前为止:FluentValidation在10.4版本中还没有对远程验证的内置验证方法,虽然可以通过Must或MustAsync内置方法来实现远程验证,但是这种远程验证在第1次输入时,并不能在失去焦点后,触发页面上的验证,只有在提交操作被执行了后,才能触发页面上的验证信息,这种页面验证体验并不是那么完美,实际上通过Must或MustAsync内置方法来实现的所有验证都存在上述这种异常,我不知道Must或MustAsync是否是因为表单级的而不是属性级的而特定设计成这样,还是FluentValidation的bug。还管怎样我们必须通过自定义来完美实现对输入操作的远程验证。
远程验证
Must
RuleFor(x => x.Name)
.NotEmpty().WithMessage($"姓名不能为空。")
.Length(2, 18).WithMessage("姓名的长度必须在:{0}~{1}个字符之间。")
.Must(_usersService.IsUniqueName).WithMessage("该用户名已经被注册。");
注意:
Must或MustAsync内置方法只有执行了提交后,才能触发页面上的验证信息。
PropertyValidator<T, TProperty>
1、自定义IUniquePropertyValidator接口
//NuGet
//NuGet--FluentValidation.AspNetCore
using FluentValidation.Validators;
namespace Web.FluentValidationValidator.Unique
{
/// <summary>
/// 【唯一属性验证器--接口】
/// <remarks>
/// 摘要:
/// 通过该接口的具体实现类及其属性成员,对客户端(浏览器)所输入的数据,在服务器端的数据库中是否已经存在;如果服务器端的数据库中已经存在指定的数据,获取相应的验证信息。
/// </remarks>
/// </summary>
public interface IUniquePropertyValidator : IPropertyValidator
{
#region 属性
/// <summary>
/// 【控制器】
/// <remarks>
/// 摘要:
/// 获取(基于MVC模板)指定的控制器类的名称。
/// </remarks>
/// </summary>
public string Controller { get; }
/// <summary>
/// 【行为】
/// <remarks>
/// 摘要:
/// 获取(基于MVC模板)指定的控制器类中指定行为方法的名称。
/// </remarks>
/// </summary>
public string Action { get; }
#endregion
}
}
2、自定义UniquePropertyValidator<T>具体实现类
//NuGet
//NuGet--FluentValidation.AspNetCore
using FluentValidation;
using FluentValidation.Validators;
namespace Web.FluentValidationValidator.Unique
{
///<typeparam name="T">泛型类型实例(这里主要指:1个指定模型类的类型实例)。</typeparam>
/// <summary>
/// 【唯一属性验证器--接口】
/// <remarks>
/// 摘要:
/// 通过该接口的具体实现类及其属性成员,对客户端(浏览器)所输入的数据,在服务器端的数据库中是否已经存在;如果服务器端的数据库中已经存在指定的数据,获取相应的验证信息。
/// </remarks>
/// </summary>
public class UniquePropertyValidator<T> : PropertyValidator<T, string>, IUniquePropertyValidator
{
#region 拷贝构造方法
/// <param name="max">对上传文件大小进行上传限定的最大值(无符号整型)。</param>
/// <summary>
/// 【拷贝构造方法】
/// <remarks>
/// 摘要:
/// 拷贝构造方法通过其参数实例,初始化/实例化该类中所有同一类型的属性成员。
/// </remarks>
/// </summary>
public UniquePropertyValidator(string controller, string action)
{
Controller = controller;
Action = action;
}
#endregion
#region 属性--接口实现
/// <summary>
/// 【控制器】
/// <remarks>
/// 摘要:
/// 获取(基于MVC模板)指定的控制器类的名称。
/// </remarks>
/// </summary>
public string Controller { get; }
/// <summary>
/// 【行为】
/// <remarks>
/// 摘要:
/// 获取(基于MVC模板)指定的控制器类中指定行为方法的名称。
/// </remarks>
/// </summary>
public string Action { get; }
#endregion
#region 属性--覆写--PropertyValidator<T, string>
/// <summary>
/// 【名称】
/// <remarks>
/// 摘要:
/// 获取当前类的名称。
/// </remarks>
/// </summary>
public override string Name => "UniquePropertyValidator"; // 4
#endregion
#region 方法--覆写--PropertyValidator<T, string>
/// <param name="errorCode">自定义的错误验证信息。</param>
/// <summary>
/// 【获取默认信息模版】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,如果该参数实例不为空字符串,则获取自定义的错误验证信息;如果为空字符串,则获取下列默认的错误验证信息。
/// </remarks>
/// <returns>
/// 返回:
/// 自定义的错误验证信息;或下列默认的错误验证信息。
/// </returns>
/// </summary>
protected override string GetDefaultMessageTemplate(string errorCode) // 5
=> "A value is registered for {PropertyName}.";
/// <param name="context">泛型模型类的验证上下文实例。</param>
/// <param name="value">1个指定的上传文件的实例。</param>
/// <summary>
/// 【有效?】
/// <remarks>
/// 摘要:
/// 由于该类是通过指定的MVC方法进行验证操作的,所在当前方法中对客户端(浏览器)中的输入定义任何的验证规则,验证规则的定义和结果是通过指定的MVC方法获取的。
/// </remarks>
/// <returns>
/// 返回:
/// true
/// </returns>
/// </summary>
public override bool IsValid(ValidationContext<T> context, string value)
{
//由于该类是通过指定的MVC方法进行验证操作的,所在当前方法中对客户端(浏览器)中的输入定义任何的验证规则,验证规则的定义和结果是通过指定的MVC方法获取的。
return true;
}
#endregion
}
}
3、自定义UniqueClientValidator
//NuGet
//NuGet--FluentValidation.AspNetCore
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
//.Net框架直接引用
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Web.FluentValidationValidator.Unique
{
/// <summary>
/// 【唯一客户端验证器--类】
/// <remarks>
/// 摘要:
/// 通过该实体类及其方法成员实现,将页面表单中上传文件(文件上传控件中的文件)属性与指定的验证标签及其样式进行绑定;;如果用户在客户端(浏览器)中所输入数据没有通过服务器端中MVC方法的验证,页面该标签中显示出相应的错误信息提供方法支撑。
/// </remarks>
/// </summary>
public class UniqueClientValidator : ClientValidatorBase
{
#region 拷贝构造方法
/// <param name="rule">验证规则接口实例。</param>
/// <param name="component">规则组件接口实例。</param>
/// <summary>
/// 【拷贝构造方法】
/// <remarks>
/// 摘要:
/// 拷贝构造方法通过其参数实例,初始化/实例化其基类中的同一类型的变量/属性成员。
/// </remarks>
/// </summary>
public UniqueClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component)
{ }
#endregion
#region 方法--覆写--ClientValidatorBase
/// <param name="context">客户端模型验证上下文实例。</param>
/// <summary>
/// 【添加验证】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,将页面表单中指定属性及其相应的验证标签及其样式进行绑定;如果用户在客户端(浏览器)中所输入数据没有通过服务器端中MVC方法的验证,页面该标签中显示出相应的错误信息提供方法支撑。
/// </remarks>
/// </summary>
public override void AddValidation(ClientModelValidationContext context)
{
var validator = (IUniquePropertyValidator)Validator; // 1
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-remote", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-remote-url", $"/{validator.Controller}/{validator.Action}");
}
#endregion
#region 方法--私有/保护
/// <param name="context">模型验证上下文基类实例。</param>
/// <summary>
/// 【有效?】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,如果该参数实例不为空字符串,则获取自定义的错误验证信息;如果为空字符串,则获取下列默认的错误验证信息。
/// </remarks>
/// <returns>
/// 返回:
/// 自定义的错误验证信息;或下列默认的错误验证信息。
/// </returns>
/// </summary>
private string GetErrorMessage( ModelValidationContextBase context)
{
var cfg = context.ActionContext.HttpContext.RequestServices.GetRequiredService<ValidatorConfiguration>(); // 1
//获取拼接字符:{PropertyName},所对应的实例值。
var formatter = cfg.MessageFormatterFactory() // 2
.AppendPropertyName(Rule.GetDisplayName(null))
.AppendArgument("PropertyName", context.ModelMetadata.DisplayName);
string message;
try
{
message = Component.GetUnformattedErrorMessage();
}
catch (NullReferenceException)
{
message = "A value is registered for {PropertyName}."; // 3
}
message = formatter.BuildMessage(message);
return message;
}
#endregion
}
}
4、自定义Web.FluentValidationValidator
//NuGet
//NuGet--FluentValidation.AspNetCore
using FluentValidation;
//项目
using Web.FluentValidationValidator.Unique;
using Web.FluentValidationValidator.Upload;
namespace Web.FluentValidationValidator
{
/// <summary>
/// 【默认Nop地址录--类】
/// <remarks>
/// 摘要:
/// 通过该类对验证规则生成器接口实例(this IRuleBuilder<T, TProperty> )进行自定义扩展,为模型类中的属性成员提供额外的验证操作,提供相应的方法支撑。
/// 注意:
/// 该类及其所有成员都被限定为静态,即该类及其成员在程序执行前已经被实例化(不需要通过显式的实例化关键字“new”,就可以获取该类及其成员的实例)。
/// </remarks>
/// </summary>
public static class ValidatorExtension
{
#region 方法
///<typeparam name="T">泛型类型实例(这里主要指:1个指定模型类的类型实例)。</typeparam>
/// <param name="ruleBuilder">验证规则生成器接口实例。</param>
/// <param name="max">上传文件最大的限定值(无符号整型)。</param>
/// <summary>
/// 【文件小于】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,对验证规则生成器接口实例(this IRuleBuilder<T, TProperty> )进行上传文件大小验证的自定义扩展,为模型类中的上传文件属性成员提供额外的验证操作,提供相应的方法支撑。
/// </remarks>
/// </summary>
/// <returns>
/// 返回:
/// 验证规则生成器接口实例(this IRuleBuilder<T, TProperty> )。
/// </returns>
public static IRuleBuilderOptions<T, IFormFile> FileSmallerThan<T>(this IRuleBuilder<T, IFormFile> ruleBuilder,uint max)
{
return ruleBuilder.SetValidator(new FileSizePropertyValidator<T>(max));
}
///<typeparam name="T">泛型类型实例(这里主要指:1个指定模型类的类型实例)。</typeparam>
/// <param name="ruleBuilder">验证规则生成器接口实例。</param>
/// <param name="controller">(基于MVC模板)指定的控制器类的名称。</param>
/// <param name="action">(基于MVC模板)指定的控制器类中指定行为方法的名称。</param>
/// <summary>
/// 【唯一】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,对验证规则生成器接口实例(this IRuleBuilder<T, TProperty> )调用MVC方法进行所输入数据的唯一性自定义扩展,为模型类中的属性成员提供所输入数据的唯一性的额外验证操作,提供相应的方法支撑。
/// </remarks>
/// </summary>
/// <returns>
/// 返回:
/// 验证规则生成器接口实例(this IRuleBuilder<T, TProperty> )。
/// </returns>
public static IRuleBuilderOptions<T, string> IsUnique<T>(this IRuleBuilder<T, string> ruleBuilder, string controller, string action)
{
return ruleBuilder.SetValidator(new UniquePropertyValidator<T>(controller, action));
}
#endregion
}
}
5、把自定义验证依赖注入到内置容器中
builder.Services.AddTransient<IValidator<FluentValidationLocalizationModel>, FluentValidationLocalizationModelValidation>();
// Add services to the container.
builder.Services.AddControllersWithViews()
.AddFluentValidation(options =>
{
options.ConfigureClientsideValidation(clientside => // 1
{
clientside.ClientValidatorFactories[typeof(IFileSizePropertyValidator)] = (_, rule, component) => // 2
new FileSizeClientValidator(rule, component); // 3
clientside.ClientValidatorFactories[typeof(IUniquePropertyValidator)] = (_, rule, component) => // 2
new UniqueClientValidator(rule, component); // 3
});
})
.AddRazorRuntimeCompilation();
上传控件验证
在本示例中自定义了基于FluentValidation的上传验证,这只是针对FluentValidation的研究和验证,但是对工程性的集成而言,1种久经考验的插件上传更加符合工程软件的功能需求,本人推荐基于Jquery的“bootstrap fileinput”文件上传插件或从jQuery插件库-收集最全最新最好的jQuery插件中搜索相应的文件上传插件,所以在工程性软件中就不要对模型类中的上传文件的属性成员定义基于的DataAnnotations或FluentValidation验证,而是最好使用基于Jquery验证,本人会在下面的章节中单独讲述怎样通过基于Jquery上传插件实现文件的上传操作。
对以上功能更为具体实现和注释见:22-05-04-047_Validation(表单验证之DataAnnotations与FluentValidation)。