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 必填項屬性.

<form nz-form>
    <nz-form-item>
        <nz-form-label [nzRequired]="true">name</nz-form-label>
        <nz-form-control [nzErrorTip]="vt_input_Name">
            <input #input_Name="" #v_input_Name="xValidationExtend" name="name" nz-input="" x-validation-extend="" [(ngModel)]="model.name" [required]="true" />
            <ng-template #vt_input_Name="">{{v_input_Name.getErrorMessage()}}</ng-template>
        </nz-form-control>
    </nz-form-item>
</form>

通過將DTO數據註解轉換成標籤的驗證屬性,可以讓 Web Api 和 UI 的驗證同步.

自定義驗證

數據註解可以解決一些常見的驗證場景.

但業務上可能需要編寫自定義代碼以更靈活的方式驗證.

Util應用框架定義了一個驗證接口 Util.Validation.IValidation.

IValidation 接口定義了 Validate 方法,執行該方法返回驗證結果集合.

/// <summary>
/// 驗證操作
/// </summary>
public interface IValidation {
    /// <summary>
    /// 驗證
    /// </summary>
    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<TestEntity> {
            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 代表一個驗證條件,接口定義如下.

      /// <summary>
      /// 驗證規則
      /// </summary>
      public interface IValidationRule {
          /// <summary>
          /// 驗證
          /// </summary>
          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<TestEntity> {
          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<TestEntity> {
          public TestEntity() : this( Guid.Empty ) {
          }
      
          public TestEntity( Guid id ) : base( id ) {
              AddValidationRule( new SampleValidationRule( this ) );
          }
      
          [Required]
          public string Name { get; set; }
      }
      
    • 設置驗證處理器

      驗證規則僅返回驗證結果,驗證失敗如何處理由驗證處理器決定.

      /// <summary>
      /// 驗證處理器
      /// </summary>
      public interface IValidationHandler {
          /// <summary>
          /// 處理驗證錯誤
          /// </summary>
          /// <param name="results">驗證結果集合</param>
          void Handle( ValidationResultCollection results );
      }
      

      領域對象默認的驗證處理器在驗證失敗時拋出 Warning 異常.

      你可以設置自己的驗證處理器來替換默認的.

      下面定義的 NothingHandler 在驗證失敗時什麼也不做.

      /// <summary>
      /// 驗證失敗,不做任何處理
      /// </summary>
      public class NothingHandler : IValidationHandler {
          /// <summary>
          /// 處理驗證錯誤
          /// </summary>
          /// <param name="results">驗證結果集合</param>
          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<CustomerDto> dto );
      }
    

源碼解析

DataAnnotationValidation 數據註解驗證操作

可以調用 DataAnnotationValidationValidate 方法驗證數據註解.

/// <summary>
/// 數據註解驗證操作
/// </summary>
public static class DataAnnotationValidation {
    /// <summary>
    /// 驗證
    /// </summary>
    /// <param name="target">驗證目標</param>
    public static ValidationResultCollection Validate( object target ) {
        if( target == null )
            throw new ArgumentNullException( nameof( target ) );
        var result = new ValidationResultCollection();
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext( target, null, null );
        var isValid = Validator.TryValidateObject( target, context, validationResults, true );
        if ( !isValid )
            result.AddList( validationResults );
        return result;
    }
}

ValidationResultCollection 驗證結果集合

ValidationResultCollection 用於收集驗證結果消息.

/// <summary>
/// 驗證結果集合
/// </summary>
public class ValidationResultCollection : List<ValidationResult> {

    /// <summary>
    /// 初始化驗證結果集合
    /// </summary>
    public ValidationResultCollection() : this( "" ) {
    }

    /// <summary>
    /// 初始化驗證結果集合
    /// </summary>
    /// <param name="result">驗證結果</param>
    public ValidationResultCollection( string result ) {
        if( string.IsNullOrWhiteSpace( result ) )
            return;
        Add( new ValidationResult( result ) );
    }

    /// <summary>
    /// 成功驗證結果集合
    /// </summary>
    public static readonly ValidationResultCollection Success = new();

    /// <summary>
    /// 是否有效
    /// </summary>
    public bool IsValid => Count == 0;

    /// <summary>
    /// 添加驗證結果集合
    /// </summary>
    /// <param name="results">驗證結果集合</param>
    public void AddList( IEnumerable<ValidationResult> results ) {
        if( results == null )
            return;
        foreach( var result in results )
            Add( result );
    }

    /// <summary>
    /// 輸出驗證消息
    /// </summary>
    public override string ToString() {
        if( IsValid )
            return string.Empty;
        return this.First().ErrorMessage;
    }
}

ThrowHandler 驗證處理器

ThrowHandler 是默認的驗證處理器,在驗證失敗時拋出 Warning 異常.

/// <summary>
/// 驗證失敗,拋出異常
/// </summary>
public class ThrowHandler : IValidationHandler{
    /// <summary>
    /// 處理驗證錯誤
    /// </summary>
    /// <param name="results">驗證結果集合</param>
    public void Handle( ValidationResultCollection results ) {
        if ( results.IsValid )
            return;
        throw new Warning( results.First().ErrorMessage );
    }
}

ValidAttribute 驗證攔截器

ValidAttribute 是一個 Aop 參數攔截器,可以對實現了 IValidation 接口的單個對象或對象集合進行驗證.

/// <summary>
/// 驗證攔截器
/// </summary>
public class ValidAttribute : ParameterInterceptorBase {
    /// <summary>
    /// 執行
    /// </summary>
    public override async Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
        Validate( context.Parameter );
        await next( context );
    }

    /// <summary>
    /// 驗證
    /// </summary>
    private void Validate( Parameter parameter ) {
        if ( Reflection.IsGenericCollection( parameter.RawType ) ) {
            ValidateCollection( parameter );
            return;
        }
        IValidation validation = parameter.Value as IValidation;
        validation?.Validate();
    }

    /// <summary>
    /// 驗證集合
    /// </summary>
    private void ValidateCollection( Parameter parameter ) {
        if ( !( parameter.Value is IEnumerable<IValidation> validations ) )
            return;
        foreach ( var validation in validations )
            validation.Validate();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章