本節介紹Util應用框架如何進行驗證.
驗證是業務健壯性的基礎.
.Net 提供了一套稱為 DataAnnotations 資料註解的方法,可以對屬性進行一些基本驗證,比如必填項驗證,長度驗證等.
Util應用框架使用標準的資料註解作為基礎驗證,並對自定義驗證進行擴充套件.
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支援 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引數物件 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 的 Validate 方法驗證資料註解.
/// <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 用於收集驗證結果訊息.
/// <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 是預設的驗證處理器,在驗證失敗時丟擲 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 是一個 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();
}
}