Util應用框架基礎(四)

2023-11-07 09:02:04

本節介紹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();
    }
}