关于验证(Validation) 这件事
小明来问说, 他想要检查输入内容是否验证通过,
检查模型(Model)的所有属性(Property)内容,
查看里面某些属性(Property) 内容或是格式是不是有问题?
以下是小明提供的程式码片段, 它想要验证customer 内容是不是有问题
public void Process(Customer customer){ if( customer.UserId <= 0 ) { throw new Exception("UserId must be a positive number"); } if( customer.Email == null ){ throw new Exception("Email can't be null"); } ...}
验证输入内容的方法有很多种, 这没有唯一正确的标準答案,
因此与团队合作了解哪种方法最适合解决目前遭遇的问题,
就是好的方法.
通常验证输入内容有三种方法
抛出异常验证规则模式带有验证的结果物件抛出异常
此方法使用 Exception 涉及直接系统中断, 该模式是最常用的验证模式, 它包括直接检查输入和引发异常.
就像小明来询问, 所提供的程式码片段写法.
此方法的特色如下
一旦引发异常, 方法的执行就结束, 处理起来更快, 但是您只会得到第一个无效输入的结果.在小明的例子中, 如果 userId 为 0, 则您只会得到 userId 无效输入的结果.它很灵活, 因为您可以在任何方法中撰写指定任何规则.有可能在多个地方重複进行验证.
验证规则模式
验证规则模式源自Visitor 设计模式, 我们可以使用dotnet 提供的Validator,
dotnet 提供了许多常用的ValidationAttribute , 常见的有
dotnet Validator 範例如下
public class Customer { public int UserId { get;set; } [Required] public string Name { get; set;} [EmailAddress] public string Email { get; set; }}public void Process(Customer customer){ var context = new ValidationContext(customer, null, null); Validator.ValidateObject(customer, context, true); ...}
透过dotnet 提供的常用Atturibute 就能应付一般大部分的验证情况,
不过有时候我们还是需要自订的验证方式.
第一种是自订自己的Validation Attribute, 下面示範用来检查栏位的内容是否为 null
public class MyRequiredValidationAttribute : ValidationAttribute{protected override ValidationResult IsValid(object value, ValidationContext validationContext){var text = (string) value;if ( String.IsNullOrEmpty(text) ){return new ValidationResult("Name can't be null!", new[] { validationContext.MemberName });}return base.IsValid(value, validationContext);}}
然后在Customer 物件中, 挂载MyRequired Attribute
public class Customer{ [MyRequired] public string Name { get; set;}}
也有与 Required 同样的效果.
也许有的人会不喜欢在Customer 中挂载许多验证 Attribute 来设定验证方法,
想要统一在一个地方做设定验证方法.
那你可以用第二种方式, 实作IValidatableObject , 範例如下
public class Customer : IValidatableObject{ public int UserId { get;set; } public string Name { get; set;} public string Email { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if ( String.IsNullOrEmpty(Name) ) { yield return new ValidationResult("Name can't be null", new[] {" Name "}); } }}
此模式具有以下优点
所有验证规则都是分开的, 可以分别维护Process 方法中的验证程式码很小并且易于阅读它允许您一次套用多个验证规则缺点
对于特定的内容或条件, 必须创建单独的验证规则物件.带有验证的结果物件
用dotnet Validator 的使用方式如下
public void Process(Customer customer) { Validator.ValidateObject(customer, context, true); var validationResults = new Collection<ValidationResult>(); var isSuccess = Validator.TryValidateObject(customer, context, validationResults, true); ...}
上述程式码 validationResults 变数储存所有验证的结果.
dotnet Validator 提供两种方式 ValidateObject 和 TryValidateObject
我们也能利用 FluentValidation 来达到这个方式, 它的使用方式如下
public class MyValidator : AbstractValidator<Customer>{public MyValidator(){ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure; RuleFor(c => c.Name).NotNull().WithMessage("{PropertyName} can't be null"); ...}}public void Process(Customer customer){var validator = new MyValidator();var validationResult = validator.Validate(customer);if (!validationResult.IsValid){ var errorMssage = string.Join("\r\n", validationResult.Errors.Select(e => e.ErrorMessage)); throw new Exception(errorMessage);}}
你可以在一个地方添加许多RuleFor 验证规则.
当然FluentValidation 也提供我们自订验证规则, 自订验证规则方式如下
public class MyRequiredValidator : PropertyValidator { public MyRequiredValidator() : base("'{PropertyValue}' can't not be null.") { } protected override bool IsValid(PropertyValidatorContext context) { var text = (string) context.PropertyValue; if (string.IsNullOrEmpty(text)) { return false; } return true; }}
写好自订的 PropertyValidator 之后, 透过 SetValidator 照下面示範
public class MyValidator : AbstractValidator<Customer>{public MyValidator(){ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure; RuleFor(c => c.Name).SetValidator(new MyRequiredValidator()); ...}}
到这里你可以发现dotnet 和FluentValidation 套用 MyRequired 验证规则的方式不同.
dotnet Validation 通常方式是在Customer 物件中挂载验证规则
[MyRequired]public string Name{ get; set;}
FluentValidation 通常方式是在自订的MyValidator 验证器中挂载验证规则
public class MyValidator : AbstractValidator<Customer> { public MyValidator(){ RuleFor(c => c.Name).SetValidator(new MyRequiredValidator()); ...}}
而FluentValidation 只有这一种执行验证方式
public void Process(Customer customer) { var validator = new MyValidator();var validationResult = validator.Validate(customer);if (!validationResult.IsValid){ var errorMssage = string.Join("\r\n", validationResult.Errors.Select(e => e.ErrorMessage)); ...}}
但你会发现, 假如应用程式有很多个模型(models),
用 FluentValidation 就必须为每个模型(model)编写 Validators.
也许有人会觉得很麻烦. 会想要写一个通用验证器(Validator).
而dotnet Validator 恰恰就正好是一个通用的验证器, 两者作法不同.
另外在FluentValidation 中, 或许你会觉得每增加自订验证规则还要多写 PropertyValidator 很麻烦,
你也可以用 Custom 方法来提供自订验证规则.
public class MyValidator : AbstractValidator<Customer> { public MyValidator(){ RuleFor(c => c.Name) .Custom((value, context) =>{if (string.IsNullOrEmpty(value)){context.AddFailure("Name", "Name can't be null");}});}}
一旦用Custom 撰写自订验证规则, 你就很难在其他地方重複使用.
无论採用PropertyValidator 或是Custom 方法编写自订验证规则,
我认为这取决于你要放入其中的逻辑类型.
如果您想以更容易重複使用的方式编写它, 请坚持使用PropertyValidator.
如果要使用Custom 的样式编写, 请使用类似 AbstractValidator 派生类.
在很多种情况下, 确实没有对/错的方法.
这种 "带有验证的结果物件" 模式像是前两种方法的组合. 即使有一条验证规则不通过, 但是我们仍然可以链接错误讯息, 继续往下执行其他的验证规则.