zoukankan      html  css  js  c++  java
  • ASP.NET Core Library – FluentValidation

    前言

    之前就有写过学习笔记: Asp.net core 学习笔记 Fluent Validation

    但都是用一点记入一点,零零散散不好读, 这一篇来稍微整理一下.

    主要参考:

    Fluent Validation 官网

    Simple Use

    要验证的类

    public class Person
    {
        public string Email { get; set; } = "";
    }

    对应这个类的 Validator

    public class PersonValidator : AbstractValidator<Person>
    {
        public PersonValidator() 
        {
            RuleFor(e => e.Email).EmailAddress();
        }
    }

    继承 AbstractValidator, 然后再构造函数里添加上验证逻辑. 

    调用验证方式

    public static async Task Main()
    {
        var person = new Person { Email = "test..." };
        var personValidator = new PersonValidator();
        var validationResult = personValidator.Validate(person);
        if (!validationResult.IsValid) 
        {
            foreach (var error in validationResult.Errors)
            {
                Console.WriteLine(error.PropertyName); // Email
                Console.WriteLine(error.ErrorMessage); // 'Email' is not a valid email address.
            }
        }
    }

    创建 validator 然后调用 validate, 把实例丢进去就会返回验证结果了.

    如果是在 Web API controller 还可以直接 add to ModelState 哦.

    validationResult.AddToModelState(ModelState, prefix: null);

    常用 Validator (Build-in)

    参考: Built-in Validators

    RuleFor(e => e.Email).Null();    // == null
    RuleFor(e => e.Email).NotNull(); // != null

    常用 1

    RuleFor(e => e.Email).Equal("some value");    // == "some value"
    RuleFor(e => e.Email).NotEqual("some value"); // != "some value"
    RuleFor(e => e.Email).Matches("regex expression", RegexOptions.IgnoreCase); // 正则表达式
    RuleFor(e => e.Email).EmailAddress();         // 封装好的 email 正则, empty string 也是 invalid 哦

    常用 2

    RuleFor(e => e.Salary).LessThan(1);                         // < 1
    RuleFor(e => e.Salary).LessThanOrEqualTo(1);                // <= 1
    RuleFor(e => e.Salary).GreaterThan(1);                      // > 1
    RuleFor(e => e.Salary).GreaterThanOrEqualTo(1);             // >= 1
    RuleFor(e => e.Salary).ExclusiveBetween(from: 1, to: 100);  // > 1 and < 100
    RuleFor(e => e.Salary).InclusiveBetween(from: 1, to: 100);  // >= 1 and <= 100

    常用 3

    RuleFor(e => e.Email).Length(10);        // .Length == 10 (string, Array 都可以)
    RuleFor(e => e.Email).MinimumLength(10); // .Length > 10
    RuleFor(e => e.Email).MaximumLength(10); // .Length < 10

    decimal 专用

    RuleFor(e => e.Salary).ScalePrecision(scale: 19, precision: 2, ignoreTrailingZeros: true);

    允许 19 位数, 有 2 个位数可以是小数. ignoreTrailingZeros 指 15.0000 结尾 4 个 0 不会占据位数

    理解 Emtpty

    RuleFor(e => e.Email).Empty();
    RuleFor(e => e.Email).NotEmpty();

    Empty 的意思是, 不能是 default value, 不能 length = 0 (string 会先 trim 才看 length 哦)

    int = 0  – failed

    enum = first enum value – failed

    int? = null – failed

    string = "" – failed

    string = "   " – failed

    List<string> = new() – failed

    Date = default – failed

    不常用的

    // Credit Card Validator
    // Enum Validator
    // Enum Name Validator
    // Predicate Validator

    Cross Field

    直接用就可以了. 很直观

    RuleFor(e => e.Salary).GreaterThan(e => e.Age);

    Conditional

    参考: Conditions

    有 2 种 conditional 

    1. 当满足条件时才验证

    RuleFor(e => e.Salary).LessThan(10).When(e => e.Email == "test");

    2. if ... else 配置

    When(e => e.Email == "test", () =>
    {
    // 这里不要乱放代码, 只放 setup validation 代码就好, 因为它一定会执行 RuleFor(e
    => e.Salary).LessThan(500); }).Otherwise(() => { RuleFor(e => e.Salary).InclusiveBetween(from: 1, to: 100); });

    很可惜, 它没有提供 swtich 和 else if, 写起来不那么直观.

    注: 它的运行机制是, validation setup 一定会跑 (和 if 概念不同哦, 所以不要乱吧代码放进 setup validation scope 里面), 在做 validation 的时候才调用 when 去判断是否要执行

    Include Properties

    参考: Validator customization

    var validationResult = personValidator.Validate(
        person, 
        options => options.IncludeProperties("Email", "Salary") // params string[] properties
    );

    适用于 partial update 场景.

    Custom Validator

    参考: Custom Validators

    除了使用 build-in 的 validator, 想要自己写逻辑验证有 2 个方法. 

    1. Must

    RuleFor(e => e.Email).Must((rootObject, propertyValue) => {
        return rootObject.Salary == 1 && propertyValue == "email@email.com";
    });

    直接写验证逻辑. 通过实例和属性值做判断.

    2. PropertyValidator

    public class MoneyValidator<T> : PropertyValidator<T, decimal> where T : Person
    {
        public override string Name => "MoneyValidator";
        public override bool IsValid(ValidationContext<T> context, decimal propertyValue)
        {
            var person = context.InstanceToValidate; // 可以拿到 instance, 如果没有用到, 泛型 T 就好了, 不需要 where
            return propertyValue == 1;
        }
    }

    使用

    RuleFor(e => e.Salary).SetValidator(new MoneyValidator<Person>());

    Friendly call

    RuleFor(e => e.Salary).Money();

    extension method

    public static class ValidatorExtensions
    {
        public static IRuleBuilderOptions<T, decimal> Money<T>(this IRuleBuilder<T, decimal> ruleBuilder) 
        {
            return ruleBuilder.SetValidator(new MoneyValidator<T>());
        }
    
        public static IRuleBuilderOptions<T, decimal?> Money<T>(this IRuleBuilder<T, decimal?> ruleBuilder,)  
        {
            return ruleBuilder.SetValidator(new MoneyValidator<T>());
        }
    }

    注: decimal 和 decimal? 要 2 个方法重载. 内部 set 同一个 validator 就可以了, 当遇到 null 的时候它会直接 pass, 估计内部有做了处理, 这个方式是源码学来的.

    Dependency Injection

    参考: Dependency Injection

    和 EF Core 类似的做法, 通过反射 Assembly 找出 Validator 然后 AddScope.

    mvcBuilder.AddFluentValidation(options =>
    {
        options.RegisterValidatorsFromAssembly(assembly);
    });

    Web API Controller

    private readonly CreateProjectDtoValidator _createProjectDtoValidator;public ProjectController(CreateProjectDtoValidator createProjectDtoValidator)
    {
        _createProjectDtoValidator = createProjectDtoValidator
    }

    这样 Validator 就可以注入 DbContext 和其它 service 了.

    Asynchronous

    参考: Asynchronous Validation

    有几个方法都可以异步.

    1. WhenAsync

    WhenAsync((person, cancellationToken) => {
        return Task.FromResult(true);
    }, () => {
        RuleFor(e => e.Email).EmailAddress();
    });
    RuleFor(p => p.Email).EmailAddress().WhenAsync((person, cancellationToken) => Task.FromResult(true));

    2. MustAsync

    RuleFor(e => e.Email).MustAsync((rootObject, propertyValue, context, cancellationToken) => {
        return Task.FromResult(true);
    });

    3. Customer AsyncValidator

    public class MoneyAsyncValidator<T> : AsyncPropertyValidator<T, decimal>
    {
        public override string Name => "MoneyValidator";
        public override Task<bool> IsValidAsync(ValidationContext<T> context, decimal propertyValue, CancellationToken cancellation)
        {
            return Task.FromResult(propertyValue == 1);
        }
    }

    调用

    RuleFor(e => e.Salary).SetAsyncValidator(new MoneyAsyncValidator<Person>());

    4. ValidateAsync

    如果验证规则里用到了 async, 那在调用 Validate 的时候要用 Async 版本哦.

    var validationResult = await personValidator.ValidateAsync(person);

    PropertyName, DisplayName

    参考: Overriding the Property Name

    参考之前的: ASP.NET Core – Case Style Conversion FluentValidation 的部分.

    Manually set error with property name and display name

    Fluent Validation 当有 Children 的时候, 它的返回是这样的

    property name 会是一个 path 的形式. array 就配上 [n].

    如果我们有需求动态添加 error 的话, 就必须符合它的格式哦. 比如: 

    var validator = new PersonValidator();
    var person = new Person { Children = new List<Child> { new Child(), new Child() } };
    var personResult = validator.Validate(person);
    for (int i = 0; i < person.Children.Count; i++)
    {
        var child = person.Children[i];
        var childValidator = new ChildValidator();
        var childResult = childValidator.Validate(child);
        foreach (var error in childResult.Errors)
        {
            var eExp = Expression.Parameter(person.GetType(), "e");
            var eDotNameExp = Expression.Property(eExp, nameof(person.Children));
            var lambda = Expression.Lambda(eDotNameExp, eExp);
            var propertyName = ValidatorOptions.Global.PropertyNameResolver(person.GetType(), person.GetType().GetProperty(nameof(person.Children)), lambda);
            error.PropertyName = $"{propertyName}[{i}].{error.PropertyName}";
            personResult.Errors.Add(error);
        }
    }
    Console.WriteLine(JsonSerializer.Serialize(personResult.Errors.Select(e => new { e.PropertyName, e.ErrorMessage }), new JsonSerializerOptions { WriteIndented = true } ));

    需要特别注意的是, PropertyName 必须经过正确的 ValidatorOptions.Global.PropertyNameResolver 处理. 

    第 1 个参数是 root class type, 第 2 个参数是 last depth PropertyInfo, 最后一个是从 Root 到 deepest propertyInfo 的路径 lambda 表达式

    这样它才能 generate 到对的 Property Name

    FluentValidation parse expression 的源码是这样的

    就这样看的话, 应该是没有 cover 到 Children[0].Name 这种 [0] 的处理的. 所以估计它是通过外部累加做到的. 所以使用 PropertyNameResolver 的时候, 可不要放入 [0] 这种 expression 哦.

    Cascade mode

    参考: Setting the Cascade mode

    默认情况下, 当一个错误发生以后, 其它的验证依然会执行, 然后返回所有的错误.

    有时候这不一定是我们期望的模式. 

    举例, email address 正则验证

    当 empty string 的时候, 算不算 invalid email address ?

    通常是不算的, 都没有填, 验个毛. 应该要跑错 required 必填.

    那怎样处理? 

    3 个思路. 

    1. email validator 遇到 emtpy string 算 pass

    2. email validator + when string.IsNullOrEmpty(value)

    3. 当 1 个 error 发生, 停止后续的验证.

    cascade mode 就是只第 3 种情况. 

    RuleFor(e => e.Email).Cascade(CascadeMode.Stop).NotEmpty().EmailAddress();

    另一种写法是 depend rule, 当 a rule ok 了才执行 b rule, 这也可以算一种 Conditional 的手法.

    RuleFor(e => e.Email).NotEmpty().DependentRules(() =>
    {
        RuleFor(e => e.Email).EmailAddress();
    });

    要 set global 或者 by validator 就这样: 

    Error Message

    参考: Overriding the Message

    WithMessage

    RuleFor(e => e.EmailSalary).EmailAddress().WithMessage("{PropertyName} {PropertyValue} is no ok!");

    ValidationContext

    在 Must, CustomPropertyValidator 内操作 context 也可以设置更多的参数.

    RuleFor(e => e.Email).Must((rootObject, propertyValue, context) =>
    {
        context.MessageFormatter.AppendArgument("MyValue", "value");
        return false;
    }).WithMessage("{MyValue}");

    customer property validator default message template

    public class MoneyValidator<T> : PropertyValidator<T, decimal>
    {
        public override string Name => "MoneyValidator";
        public override bool IsValid(ValidationContext<T> context, decimal value)
        {
            return true;
        }
        protected override string GetDefaultMessageTemplate(string errorCode) // errorCode 都是 null, 不清楚怎么用
        {
            return "{MyValue} is wrong.";
        }
    }
  • 相关阅读:
    Palindrome Linked List 解答
    Word Break II 解答
    Array vs Linked List
    Reverse Linked List II 解答
    Calculate Number Of Islands And Lakes 解答
    Sqrt(x) 解答
    Find Median from Data Stream 解答
    Majority Element II 解答
    Binary Search Tree DFS Template
    188. Best Time to Buy and Sell Stock IV
  • 原文地址:https://www.cnblogs.com/keatkeat/p/15720983.html
Copyright © 2011-2022 走看看