• Util应用框架基础(四) - 验证


    本节介绍Util应用框架如何进行验证.

    概述

    验证是业务健壮性的基础.

    .Net 提供了一套称为 DataAnnotations 数据注解的方法,可以对属性进行一些基本验证,比如必填项验证,长度验证等.

    Util应用框架使用标准的数据注解作为基础验证,并对自定义验证进行扩展.

    基础用法

    引用Nuget包

    Nuget包名: Util.Validation.

    通常不需要手工引用它.

    数据注解

    数据注解是一种.Net 特性 Attribute,可以在属性上应用它们.

    常用数据注解

    下面列出一些常用数据注解,如果还不能满足需求,可以创建自定义的数据注解.

    • RequiredAttribute 必填项验证

      [Required] 验证属性不能是空值.

      范例:

        public class Test {
            [Required]
            public string Name { get; set; }
        }
      

      [Required] 支持一些参数,可以设置验证失败的提示消息.

        public class Test {
            [Required(ErrorMessage = "名称不能为空")]
            public string Name { get; set; }
        }
      
    • StringLengthAttribute 字符串长度验证

      [StringLength] 可以对字符串长度进行验证.

      下面的例子验证 Name 属性的字符串最大长度为 5.

        public class Test {
            [StringLength(5)]
            public string Name { get; set; }
        }
      

      还可以同时设置最小长度.

      下面验证 Name 属性字符串最小长度为1,最大长度为 5.

        public class Test {
            [StringLength(5,MinimumLength = 1)]
            public string Name { get; set; }
        }
      
    • MaxLengthAttribute 字符串最大长度验证

      [MaxLength] 也可以用来验证字符串最大长度.

      验证 Name 属性的字符串最大长度为 5.

        public class Test {
            [MaxLength(5)]
            public string Name { get; set; }
        }
      
    • MinLengthAttribute 字符串最小长度验证

      [MinLength] 也可以用来验证字符串最小长度.

      验证 Name 属性的字符串最小长度为 1.

        public class Test {
            [MinLength(1)]
            public string Name { get; set; }
        }
      
    • RangeAttribute 数值范围验证

      [Range] 用于验证数值范围.

      下面验证 Money 属性的值必须在 1 到 5 之间的范围.

        public class Test {
            [Range( 1, 5 )]
            public int Money { get; set; }
        }
      
    • EmailAddressAttribute 电子邮件验证

      [EmailAddress] 用于验证电子邮件的格式.

        public class Test {
            [EmailAddress]
            public int Email { get; set; }
        }
      
    • PhoneAttribute 手机号验证

      [Phone] 用于验证手机号的格式.

        public class Test {
            [Phone]
            public int Tel { get; set; }
        }
      
    • IdCardAttribute 身份证验证

      [IdCard] 用于验证身份证的格式.

      它是一个Util应用框架自定义的数据注解.

        public class Test {
            [IdCard]
            public int IdCard { get; set; }
        }
      
    • UrlAttribute Url验证

      [Url] 用于验证网址格式.

        public class Test {
            [Url]
            public int Url { get; set; }
        }
      
    • RegularExpressionAttribute 正则表达式验证

      [RegularExpression] 可以使用正则表达式进行验证.

      由于正则表达式比较复杂,对于经常使用的场景,应封装成自定义数据注解.

      下面使用正则表达式验证身份证,可以封装到 [IdCard] 数据注解,从而避免正则表达式的复杂性.

        public class Test {
            [RegularExpression( @"(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)" )]
            public string IdCard { get; set; }
        }
      

    验证数据注解

    虽然在对象属性上添加了数据注解,但它们并不会自动触发验证.

    你可以使用 Asp.Net Core 提供的方法验证对象上的数据注解.

    Util 提供了一个辅助方法 Util.Validation.DataAnnotationValidation.Validate 用来验证数据注解.

    DataAnnotationValidation.Validate 方法接收一个对象参数,只需将要验证的对象实例传入即可.

    返回类型为验证结果集合,包含所有验证失败的消息.

        public class Test {
            [Required]
            public string Name { get; set; }
    
            public ValidationResultCollection Validate() {
                return DataAnnotationValidation.Validate( this );
            }
        }
    

    大部分情况下,你并不需要调用 DataAnnotationValidation.Validate 方法验证数据注解.

    实体,值对象,DTO等对象已经内置了 Validate 方法,它们会自动验证数据注解.

    Util Angular UI 数据注解验证支持

    Util Angular UI支持 Razor TagHelper服务端标签语法.

    可以在表单组件使用 Lambda表达式绑定 DTO 对象属性.

    TestDto参数对象 Name 属性使用 [Required] 设置必填项验证.

        public class TestDto : DtoBase {
            [Required]
            [Display(Name = "name")]
            public string Name { get; set; }
        }
    

    Razor 页面声明 TestDto 模型, 定义输入框 util-input,使用 for 属性绑定到 TestDto 参数对象的 Name 属性.

    @page
    @model TestDto
    
    <util-form>
        <util-input id="input_Name" for="Name" />
    util-form>
    

    Razor页面最终会生成html,表单标签 nz-form-label 添加了 nzRequired 必填项属性, 输入框 input 添加了 required 必填项属性.

    "true">name "vt_input_Name"> "" #v_input_Name="xValidationExtend" name="name" nz-input="" x-validation-extend="" [(ngModel)]="model.name" [required]="true" /> "">{{v_input_Name.getErrorMessage()}}

    通过将DTO数据注解转换成标签的验证属性,可以让 Web Api 和 UI 的验证同步.

    自定义验证

    数据注解可以解决一些常见的验证场景.

    但业务上可能需要编写自定义代码以更灵活的方式验证.

    Util应用框架定义了一个验证接口 Util.Validation.IValidation.

    IValidation 接口定义了 Validate 方法,执行该方法返回验证结果集合.

    /// 
    /// 验证操作
    /// 
    public interface IValidation {
        /// 
        /// 验证
        /// 
        ValidationResultCollection Validate();
    }
    

    实体,值对象,DTO等对象类型实现了 IValidation 接口,意味着这些对象可以通过标准的 Validate 方法进行验证.

    var entity = new TestEntity();
    entity.Validate();
    

    不论对象内部多么复杂,要验证它只需调用 Validate 方法即可.

    验证逻辑被完全封装到对象内部.

    DTO自定义验证

    DTO参数对象 Validate 方法默认仅验证数据注解,如果有错误将抛出 Warning 异常.

    Warning 异常代表业务错误,它的错误消息会返回给客户端.

    Validate 是一个虚方法,可以进行重写.

        public class TestDto : DtoBase {
            [Required]
            public string Name { get; set; }
    
            public override ValidationResultCollection Validate() {
                base.Validate();
                if ( Name.Contains( "test" ) )
                    throw new Warning( "名称不能包含test" );
                return ValidationResultCollection.Success;
            }
        }
    

    TestDto 重写了 Validate 方法.

    首先调用 base.Validate(); ,保证数据注解得到验证.

    如果数据注解验证通过, 判断 Name 属性是否包含 test 字符串,如果包含则抛出 Warning 异常.

    由于DTO参数仅用来传递数据,不应包含复杂的验证逻辑,通过重写 Validate 方法添加简单自定义验证逻辑应能满足需求.

    另外, DTO参数验证失败,可直接抛出 Warning 异常,让全局异常处理器进行处理.

    领域对象自定义验证

    领域对象包含实体和值对象等.

    对于较复杂的业务场景,与DTO不同的是,领域对象可用于业务处理,而不是传递数据.

    需要为领域对象提供更多的验证支持.

    领域对象有多种方式进行自定义验证.

    • 重写 Validate 方法

      领域对象最简单的自定义验证方式是重写 Validate 方法,并提供额外的验证逻辑.

          public class TestEntity : AggregateRoot {
              public TestEntity() : this( Guid.Empty ) {
              }
              public TestEntity( Guid id ) : base( id ) {
              }
      
              [Required]
              public string Name { get; set; }
      
              public override ValidationResultCollection Validate() {
                  base.Validate();
                  if( Name.Contains( "test" ) )
                      throw new Warning( "名称不能包含test" );
                  return ValidationResultCollection.Success;
              }
          }
      

      不过重写 Validate 验证方式也存在一些问题.

      • Validate 方法逐渐变得臃肿,代码稳定性在降低.

      • 代码的清晰度很低,重要的验证条件属于业务规则,却被一堆杂乱的 if else 判断淹没了.

    • 验证规则

      验证规则 Util.Validation.IValidationRule 代表一个验证条件,接口定义如下.

        /// 
        /// 验证规则
        /// 
        public interface IValidationRule {
            /// 
            /// 验证
            /// 
            ValidationResult Validate();
        }
      

      可以为较复杂和重要的验证条件创建验证规则对象,把复杂的验证逻辑封装起来,并从领域对象中分离出来.

      • 创建验证规则对象

        约定: 验证规则对象需要取一个符合业务验证规则的名称, 并以 ValidationRule 结尾,文件放到 ValidationRules 目录中.

        ValidationRule 结尾可能导致名称过长.

        这里演示就随便起一个 SampleValidationRule.

        验证规则依赖一些对象才能进行验证,如何才能获取依赖?

        通过验证规则对象的构造方法传入需要的依赖对象.

        验证规则不通过Ioc容器管理,在需要的地方通过 new 创建验证规则实例.

        SampleValidationRule 示例构造方法只接收一个参数,但可以根据需要接收更多依赖项.

        实现验证规则的 Validate 方法.

        如果验证成功返回 ValidationResult.Success.

        如果验证失败返回验证结果对象 ValidationResult, 并设置验证失败消息.

        public class SampleValidationRule : IValidationRule {
            private readonly TestEntity _entity;
        
            public SampleValidationRule( TestEntity entity ) {
                _entity = entity;
            }
        
            public ValidationResult Validate() {
                if( _entity.Name.Contains( "test" ) )
                    return new ValidationResult( "名称不能包含test" );
                return ValidationResult.Success;
            }
        }
        
      • 将验证规则添加到领域对象

        领域对象基类定义了 AddValidationRule 方法,用于添加验证规则对象.

        从领域对象外部调用 AddValidationRule 传入验证规则.

            var entity = new TestEntity();
            entity.AddValidationRule( new SampleValidationRule( entity ) );
        

        可以通过工厂方法封装验证规则.

        public class TestEntity : AggregateRoot {
            public TestEntity() : this( Guid.Empty ) {
            }
            public TestEntity( Guid id ) : base( id ) {
            }
        
            [Required]
            public string Name { get; set; }
        
            public static TestEntity Create() {
                var entity = new TestEntity();
                entity.AddValidationRule( new SampleValidationRule( entity ) );
                return entity;
            }
        }
        
        var entity = TestEntity.Create();
        entity.Validate();
        

        对于比较固定且只依赖领域对象本身的验证规则,可以在构造方法添加.

        public class TestEntity : AggregateRoot {
            public TestEntity() : this( Guid.Empty ) {
            }
        
            public TestEntity( Guid id ) : base( id ) {
                AddValidationRule( new SampleValidationRule( this ) );
            }
        
            [Required]
            public string Name { get; set; }
        }
        
      • 设置验证处理器

        验证规则仅返回验证结果,验证失败如何处理由验证处理器决定.

        /// 
        /// 验证处理器
        /// 
        public interface IValidationHandler {
            /// 
            /// 处理验证错误
            /// 
            /// 验证结果集合
            void Handle( ValidationResultCollection results );
        }
        

        领域对象默认的验证处理器在验证失败时抛出 Warning 异常.

        你可以设置自己的验证处理器来替换默认的.

        下面定义的 NothingHandler 在验证失败时什么也不做.

        /// 
        /// 验证失败,不做任何处理
        /// 
        public class NothingHandler : IValidationHandler {
            /// 
            /// 处理验证错误
            /// 
            /// 验证结果集合
            public void Handle( ValidationResultCollection results ) {
            }
        }
        

        调用 SetValidationHandler 方法设置验证处理器.

        var entity = new TestEntity();
        entity.AddValidationRule( new SampleValidationRule( entity ) );
        entity.SetValidationHandler( new NothingHandler() );
        

    验证拦截器

    Util应用框架定义了几个用于验证的参数拦截器.

    • NotNullAttribute

      • 验证是否为 null,如果为 null 抛出 ArgumentNullException 异常.

      • 使用范例:

        public interface ITestService : ISingletonDependency {
            void Test( [NotNull] string value );
        }
      
    • NotEmptyAttribute

      • 使用 string.IsNullOrWhiteSpace 验证是否为空字符串,如果为空则抛出 ArgumentNullException 异常.

      • 使用范例:

        public interface ITestService : ISingletonDependency {
            void Test( [NotEmpty] string value );
        }
      
    • ValidAttribute

      • 如果对象实现了 IValidation 验证接口,则自动调用对象的 Validate 方法进行验证.

      • 使用范例:

        验证单个对象.

        public interface ITestService : ISingletonDependency {
            void Test( [Valid] CustomerDto dto );
        }
      

      验证对象集合.

        public interface ITestService : ISingletonDependency {
            void Test( [Valid] List dto );
        }
      

    源码解析

    DataAnnotationValidation 数据注解验证操作

    可以调用 DataAnnotationValidationValidate 方法验证数据注解.

    /// 
    /// 数据注解验证操作
    /// 
    public static class DataAnnotationValidation {
        /// 
        /// 验证
        /// 
        /// 验证目标
        public static ValidationResultCollection Validate( object target ) {
            if( target == null )
                throw new ArgumentNullException( nameof( target ) );
            var result = new ValidationResultCollection();
            var validationResults = new List();
            var context = new ValidationContext( target, null, null );
            var isValid = Validator.TryValidateObject( target, context, validationResults, true );
            if ( !isValid )
                result.AddList( validationResults );
            return result;
        }
    }
    

    ValidationResultCollection 验证结果集合

    ValidationResultCollection 用于收集验证结果消息.

    /// 
    /// 验证结果集合
    /// 
    public class ValidationResultCollection : List {
    
        /// 
        /// 初始化验证结果集合
        /// 
        public ValidationResultCollection() : this( "" ) {
        }
    
        /// 
        /// 初始化验证结果集合
        /// 
        /// 验证结果
        public ValidationResultCollection( string result ) {
            if( string.IsNullOrWhiteSpace( result ) )
                return;
            Add( new ValidationResult( result ) );
        }
    
        /// 
        /// 成功验证结果集合
        /// 
        public static readonly ValidationResultCollection Success = new();
    
        /// 
        /// 是否有效
        /// 
        public bool IsValid => Count == 0;
    
        /// 
        /// 添加验证结果集合
        /// 
        /// 验证结果集合
        public void AddList( IEnumerable results ) {
            if( results == null )
                return;
            foreach( var result in results )
                Add( result );
        }
    
        /// 
        /// 输出验证消息
        /// 
        public override string ToString() {
            if( IsValid )
                return string.Empty;
            return this.First().ErrorMessage;
        }
    }
    

    ThrowHandler 验证处理器

    ThrowHandler 是默认的验证处理器,在验证失败时抛出 Warning 异常.

    /// 
    /// 验证失败,抛出异常
    /// 
    public class ThrowHandler : IValidationHandler{
        /// 
        /// 处理验证错误
        /// 
        /// 验证结果集合
        public void Handle( ValidationResultCollection results ) {
            if ( results.IsValid )
                return;
            throw new Warning( results.First().ErrorMessage );
        }
    }
    

    ValidAttribute 验证拦截器

    ValidAttribute 是一个 Aop 参数拦截器,可以对实现了 IValidation 接口的单个对象或对象集合进行验证.

    /// 
    /// 验证拦截器
    /// 
    public class ValidAttribute : ParameterInterceptorBase {
        /// 
        /// 执行
        /// 
        public override async Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
            Validate( context.Parameter );
            await next( context );
        }
    
        /// 
        /// 验证
        /// 
        private void Validate( Parameter parameter ) {
            if ( Reflection.IsGenericCollection( parameter.RawType ) ) {
                ValidateCollection( parameter );
                return;
            }
            IValidation validation = parameter.Value as IValidation;
            validation?.Validate();
        }
    
        /// 
        /// 验证集合
        /// 
        private void ValidateCollection( Parameter parameter ) {
            if ( !( parameter.Value is IEnumerable validations ) )
                return;
            foreach ( var validation in validations )
                validation.Validate();
        }
    }
    
  • 相关阅读:
    Win11的22H2依然没有WSA(Windows Subsystem for Android)?
    Linux桌面环境中应用程序无法启动图形交互界面
    TypeScript编译选项
    Chrome访问剪切板实现右键复制粘贴
    2023,简历石沉大海?软件测试岗位真的已经饱和了....
    Oracle常用运维SQL-SQL执行性能及锁表等查询分析
    测试从业1到3年经验,常见软件测试工程师面试题总结
    springboot docker
    SpringData框架集成操作ES增删改查
    App Store上线规范及流程
  • 原文地址:https://www.cnblogs.com/xiadao521/p/17814219.html