Util應(yīng)用框架基礎(chǔ)(四) - 驗(yàn)證
本節(jié)介紹Util應(yīng)用框架如何進(jìn)行驗(yàn)證.
概述
驗(yàn)證是業(yè)務(wù)健壯性的基礎(chǔ).
.Net 提供了一套稱為 DataAnnotations 數(shù)據(jù)注解的方法,可以對屬性進(jìn)行一些基本驗(yàn)證,比如必填項(xiàng)驗(yàn)證,長度驗(yàn)證等.
Util應(yīng)用框架使用標(biāo)準(zhǔn)的數(shù)據(jù)注解作為基礎(chǔ)驗(yàn)證,并對自定義驗(yàn)證進(jìn)行擴(kuò)展.
基礎(chǔ)用法
引用Nuget包
Nuget包名: Util.Validation.
通常不需要手工引用它.
數(shù)據(jù)注解
數(shù)據(jù)注解是一種.Net 特性 Attribute,可以在屬性上應(yīng)用它們.
常用數(shù)據(jù)注解
下面列出一些常用數(shù)據(jù)注解,如果還不能滿足需求,可以創(chuàng)建自定義的數(shù)據(jù)注解.
-
RequiredAttribute 必填項(xiàng)驗(yàn)證
[Required] 驗(yàn)證屬性不能是空值.
范例:
public class Test { [Required] public string Name { get; set; } }[Required] 支持一些參數(shù),可以設(shè)置驗(yàn)證失敗的提示消息.
public class Test { [Required(ErrorMessage = "名稱不能為空")] public string Name { get; set; } } -
StringLengthAttribute 字符串長度驗(yàn)證
[StringLength] 可以對字符串長度進(jìn)行驗(yàn)證.
下面的例子驗(yàn)證 Name 屬性的字符串最大長度為 5.
public class Test { [StringLength(5)] public string Name { get; set; } }還可以同時(shí)設(shè)置最小長度.
下面驗(yàn)證 Name 屬性字符串最小長度為1,最大長度為 5.
public class Test { [StringLength(5,MinimumLength = 1)] public string Name { get; set; } } -
MaxLengthAttribute 字符串最大長度驗(yàn)證
[MaxLength] 也可以用來驗(yàn)證字符串最大長度.
驗(yàn)證 Name 屬性的字符串最大長度為 5.
public class Test { [MaxLength(5)] public string Name { get; set; } } -
MinLengthAttribute 字符串最小長度驗(yàn)證
[MinLength] 也可以用來驗(yàn)證字符串最小長度.
驗(yàn)證 Name 屬性的字符串最小長度為 1.
public class Test { [MinLength(1)] public string Name { get; set; } } -
RangeAttribute 數(shù)值范圍驗(yàn)證
[Range] 用于驗(yàn)證數(shù)值范圍.
下面驗(yàn)證 Money 屬性的值必須在 1 到 5 之間的范圍.
public class Test { [Range( 1, 5 )] public int Money { get; set; } } -
EmailAddressAttribute 電子郵件驗(yàn)證
[EmailAddress] 用于驗(yàn)證電子郵件的格式.
public class Test { [EmailAddress] public int Email { get; set; } } -
PhoneAttribute 手機(jī)號驗(yàn)證
[Phone] 用于驗(yàn)證手機(jī)號的格式.
public class Test { [Phone] public int Tel { get; set; } } -
IdCardAttribute 身份證驗(yàn)證
[IdCard] 用于驗(yàn)證身份證的格式.
它是一個(gè)Util應(yīng)用框架自定義的數(shù)據(jù)注解.
public class Test { [IdCard] public int IdCard { get; set; } } -
UrlAttribute Url驗(yàn)證
[Url] 用于驗(yàn)證網(wǎng)址格式.
public class Test { [Url] public int Url { get; set; } } -
RegularExpressionAttribute 正則表達(dá)式驗(yàn)證
[RegularExpression] 可以使用正則表達(dá)式進(jìn)行驗(yàn)證.
由于正則表達(dá)式比較復(fù)雜,對于經(jīng)常使用的場景,應(yīng)封裝成自定義數(shù)據(jù)注解.
下面使用正則表達(dá)式驗(yàn)證身份證,可以封裝到 [IdCard] 數(shù)據(jù)注解,從而避免正則表達(dá)式的復(fù)雜性.
public class Test { [RegularExpression( @"(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)" )] public string IdCard { get; set; } }
驗(yàn)證數(shù)據(jù)注解
雖然在對象屬性上添加了數(shù)據(jù)注解,但它們并不會(huì)自動(dòng)觸發(fā)驗(yàn)證.
你可以使用 Asp.Net Core 提供的方法驗(yàn)證對象上的數(shù)據(jù)注解.
Util 提供了一個(gè)輔助方法 Util.Validation.DataAnnotationValidation.Validate 用來驗(yàn)證數(shù)據(jù)注解.
DataAnnotationValidation.Validate 方法接收一個(gè)對象參數(shù),只需將要驗(yàn)證的對象實(shí)例傳入即可.
返回類型為驗(yàn)證結(jié)果集合,包含所有驗(yàn)證失敗的消息.
public class Test {
[Required]
public string Name { get; set; }
public ValidationResultCollection Validate() {
return DataAnnotationValidation.Validate( this );
}
}
大部分情況下,你并不需要調(diào)用 DataAnnotationValidation.Validate 方法驗(yàn)證數(shù)據(jù)注解.
實(shí)體,值對象,DTO等對象已經(jīng)內(nèi)置了 Validate 方法,它們會(huì)自動(dòng)驗(yàn)證數(shù)據(jù)注解.
Util Angular UI 數(shù)據(jù)注解驗(yàn)證支持
Util Angular UI支持 Razor TagHelper服務(wù)端標(biāo)簽語法.
可以在表單組件使用 Lambda表達(dá)式綁定 DTO 對象屬性.
TestDto參數(shù)對象 Name 屬性使用 [Required] 設(shè)置必填項(xiàng)驗(yàn)證.
public class TestDto : DtoBase {
[Required]
[Display(Name = "name")]
public string Name { get; set; }
}
Razor 頁面聲明 TestDto 模型, 定義輸入框 util-input,使用 for 屬性綁定到 TestDto 參數(shù)對象的 Name 屬性.
@page
@model TestDto
<util-form>
<util-input id="input_Name" for="Name" />
</util-form>
Razor頁面最終會(huì)生成html,表單標(biāo)簽 nz-form-label 添加了 nzRequired 必填項(xiàng)屬性, 輸入框 input 添加了 required 必填項(xiàng)屬性.
<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數(shù)據(jù)注解轉(zhuǎn)換成標(biāo)簽的驗(yàn)證屬性,可以讓 Web Api 和 UI 的驗(yàn)證同步.
自定義驗(yàn)證
數(shù)據(jù)注解可以解決一些常見的驗(yàn)證場景.
但業(yè)務(wù)上可能需要編寫自定義代碼以更靈活的方式驗(yàn)證.
Util應(yīng)用框架定義了一個(gè)驗(yàn)證接口 Util.Validation.IValidation.
IValidation 接口定義了 Validate 方法,執(zhí)行該方法返回驗(yàn)證結(jié)果集合.
/// <summary>
/// 驗(yàn)證操作
/// </summary>
public interface IValidation {
/// <summary>
/// 驗(yàn)證
/// </summary>
ValidationResultCollection Validate();
}
實(shí)體,值對象,DTO等對象類型實(shí)現(xiàn)了 IValidation 接口,意味著這些對象可以通過標(biāo)準(zhǔn)的 Validate 方法進(jìn)行驗(yàn)證.
var entity = new TestEntity();
entity.Validate();
不論對象內(nèi)部多么復(fù)雜,要驗(yàn)證它只需調(diào)用 Validate 方法即可.
驗(yàn)證邏輯被完全封裝到對象內(nèi)部.
DTO自定義驗(yàn)證
DTO參數(shù)對象 Validate 方法默認(rèn)僅驗(yàn)證數(shù)據(jù)注解,如果有錯(cuò)誤將拋出 Warning 異常.
Warning 異常代表業(yè)務(wù)錯(cuò)誤,它的錯(cuò)誤消息會(huì)返回給客戶端.
Validate 是一個(gè)虛方法,可以進(jìn)行重寫.
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 方法.
首先調(diào)用 base.Validate(); ,保證數(shù)據(jù)注解得到驗(yàn)證.
如果數(shù)據(jù)注解驗(yàn)證通過, 判斷 Name 屬性是否包含 test 字符串,如果包含則拋出 Warning 異常.
由于DTO參數(shù)僅用來傳遞數(shù)據(jù),不應(yīng)包含復(fù)雜的驗(yàn)證邏輯,通過重寫 Validate 方法添加簡單自定義驗(yàn)證邏輯應(yīng)能滿足需求.
另外, DTO參數(shù)驗(yàn)證失敗,可直接拋出 Warning 異常,讓全局異常處理器進(jìn)行處理.
領(lǐng)域?qū)ο笞远x驗(yàn)證
領(lǐng)域?qū)ο蟀瑢?shí)體和值對象等.
對于較復(fù)雜的業(yè)務(wù)場景,與DTO不同的是,領(lǐng)域?qū)ο罂捎糜跇I(yè)務(wù)處理,而不是傳遞數(shù)據(jù).
需要為領(lǐng)域?qū)ο筇峁└嗟尿?yàn)證支持.
領(lǐng)域?qū)ο笥卸喾N方式進(jìn)行自定義驗(yàn)證.
-
重寫 Validate 方法
領(lǐng)域?qū)ο笞詈唵蔚淖远x驗(yàn)證方式是重寫 Validate 方法,并提供額外的驗(yàn)證邏輯.
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 驗(yàn)證方式也存在一些問題.
-
Validate 方法逐漸變得臃腫,代碼穩(wěn)定性在降低.
-
代碼的清晰度很低,重要的驗(yàn)證條件屬于業(yè)務(wù)規(guī)則,卻被一堆雜亂的 if else 判斷淹沒了.
-
-
驗(yàn)證規(guī)則
驗(yàn)證規(guī)則 Util.Validation.IValidationRule 代表一個(gè)驗(yàn)證條件,接口定義如下.
/// <summary> /// 驗(yàn)證規(guī)則 /// </summary> public interface IValidationRule { /// <summary> /// 驗(yàn)證 /// </summary> ValidationResult Validate(); }可以為較復(fù)雜和重要的驗(yàn)證條件創(chuàng)建驗(yàn)證規(guī)則對象,把復(fù)雜的驗(yàn)證邏輯封裝起來,并從領(lǐng)域?qū)ο笾蟹蛛x出來.
-
創(chuàng)建驗(yàn)證規(guī)則對象
約定: 驗(yàn)證規(guī)則對象需要取一個(gè)符合業(yè)務(wù)驗(yàn)證規(guī)則的名稱, 并以 ValidationRule 結(jié)尾,文件放到 ValidationRules 目錄中.
ValidationRule 結(jié)尾可能導(dǎo)致名稱過長.
這里演示就隨便起一個(gè) SampleValidationRule.
驗(yàn)證規(guī)則依賴一些對象才能進(jìn)行驗(yàn)證,如何才能獲取依賴?
通過驗(yàn)證規(guī)則對象的構(gòu)造方法傳入需要的依賴對象.
驗(yàn)證規(guī)則不通過Ioc容器管理,在需要的地方通過 new 創(chuàng)建驗(yàn)證規(guī)則實(shí)例.
SampleValidationRule 示例構(gòu)造方法只接收一個(gè)參數(shù),但可以根據(jù)需要接收更多依賴項(xiàng).
實(shí)現(xiàn)驗(yàn)證規(guī)則的 Validate 方法.
如果驗(yàn)證成功返回 ValidationResult.Success.
如果驗(yàn)證失敗返回驗(yàn)證結(jié)果對象 ValidationResult, 并設(shè)置驗(yàn)證失敗消息.
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; } } -
將驗(yàn)證規(guī)則添加到領(lǐng)域?qū)ο?/p>
領(lǐng)域?qū)ο蠡惗x了 AddValidationRule 方法,用于添加驗(yàn)證規(guī)則對象.
從領(lǐng)域?qū)ο笸獠空{(diào)用 AddValidationRule 傳入驗(yàn)證規(guī)則.
var entity = new TestEntity(); entity.AddValidationRule( new SampleValidationRule( entity ) );可以通過工廠方法封裝驗(yàn)證規(guī)則.
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();對于比較固定且只依賴領(lǐng)域?qū)ο蟊旧淼尿?yàn)證規(guī)則,可以在構(gòu)造方法添加.
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; } } -
設(shè)置驗(yàn)證處理器
驗(yàn)證規(guī)則僅返回驗(yàn)證結(jié)果,驗(yàn)證失敗如何處理由驗(yàn)證處理器決定.
/// <summary> /// 驗(yàn)證處理器 /// </summary> public interface IValidationHandler { /// <summary> /// 處理驗(yàn)證錯(cuò)誤 /// </summary> /// <param name="results">驗(yàn)證結(jié)果集合</param> void Handle( ValidationResultCollection results ); }領(lǐng)域?qū)ο竽J(rèn)的驗(yàn)證處理器在驗(yàn)證失敗時(shí)拋出 Warning 異常.
你可以設(shè)置自己的驗(yàn)證處理器來替換默認(rèn)的.
下面定義的 NothingHandler 在驗(yàn)證失敗時(shí)什么也不做.
/// <summary> /// 驗(yàn)證失敗,不做任何處理 /// </summary> public class NothingHandler : IValidationHandler { /// <summary> /// 處理驗(yàn)證錯(cuò)誤 /// </summary> /// <param name="results">驗(yàn)證結(jié)果集合</param> public void Handle( ValidationResultCollection results ) { } }調(diào)用 SetValidationHandler 方法設(shè)置驗(yàn)證處理器.
var entity = new TestEntity(); entity.AddValidationRule( new SampleValidationRule( entity ) ); entity.SetValidationHandler( new NothingHandler() );
-
驗(yàn)證攔截器
Util應(yīng)用框架定義了幾個(gè)用于驗(yàn)證的參數(shù)攔截器.
-
NotNullAttribute
-
驗(yàn)證是否為 null,如果為 null 拋出 ArgumentNullException 異常.
-
使用范例:
public interface ITestService : ISingletonDependency { void Test( [NotNull] string value ); } -
-
NotEmptyAttribute
-
使用 string.IsNullOrWhiteSpace 驗(yàn)證是否為空字符串,如果為空則拋出 ArgumentNullException 異常.
-
使用范例:
public interface ITestService : ISingletonDependency { void Test( [NotEmpty] string value ); } -
-
ValidAttribute
-
如果對象實(shí)現(xiàn)了 IValidation 驗(yàn)證接口,則自動(dòng)調(diào)用對象的 Validate 方法進(jìn)行驗(yàn)證.
-
使用范例:
驗(yàn)證單個(gè)對象.
public interface ITestService : ISingletonDependency { void Test( [Valid] CustomerDto dto ); }驗(yàn)證對象集合.
public interface ITestService : ISingletonDependency { void Test( [Valid] List<CustomerDto> dto ); } -
源碼解析
DataAnnotationValidation 數(shù)據(jù)注解驗(yàn)證操作
可以調(diào)用 DataAnnotationValidation 的 Validate 方法驗(yàn)證數(shù)據(jù)注解.
/// <summary>
/// 數(shù)據(jù)注解驗(yàn)證操作
/// </summary>
public static class DataAnnotationValidation {
/// <summary>
/// 驗(yàn)證
/// </summary>
/// <param name="target">驗(yàn)證目標(biāo)</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 驗(yàn)證結(jié)果集合
ValidationResultCollection 用于收集驗(yàn)證結(jié)果消息.
/// <summary>
/// 驗(yàn)證結(jié)果集合
/// </summary>
public class ValidationResultCollection : List<ValidationResult> {
/// <summary>
/// 初始化驗(yàn)證結(jié)果集合
/// </summary>
public ValidationResultCollection() : this( "" ) {
}
/// <summary>
/// 初始化驗(yàn)證結(jié)果集合
/// </summary>
/// <param name="result">驗(yàn)證結(jié)果</param>
public ValidationResultCollection( string result ) {
if( string.IsNullOrWhiteSpace( result ) )
return;
Add( new ValidationResult( result ) );
}
/// <summary>
/// 成功驗(yàn)證結(jié)果集合
/// </summary>
public static readonly ValidationResultCollection Success = new();
/// <summary>
/// 是否有效
/// </summary>
public bool IsValid => Count == 0;
/// <summary>
/// 添加驗(yàn)證結(jié)果集合
/// </summary>
/// <param name="results">驗(yàn)證結(jié)果集合</param>
public void AddList( IEnumerable<ValidationResult> results ) {
if( results == null )
return;
foreach( var result in results )
Add( result );
}
/// <summary>
/// 輸出驗(yàn)證消息
/// </summary>
public override string ToString() {
if( IsValid )
return string.Empty;
return this.First().ErrorMessage;
}
}
ThrowHandler 驗(yàn)證處理器
ThrowHandler 是默認(rèn)的驗(yàn)證處理器,在驗(yàn)證失敗時(shí)拋出 Warning 異常.
/// <summary>
/// 驗(yàn)證失敗,拋出異常
/// </summary>
public class ThrowHandler : IValidationHandler{
/// <summary>
/// 處理驗(yàn)證錯(cuò)誤
/// </summary>
/// <param name="results">驗(yàn)證結(jié)果集合</param>
public void Handle( ValidationResultCollection results ) {
if ( results.IsValid )
return;
throw new Warning( results.First().ErrorMessage );
}
}
ValidAttribute 驗(yàn)證攔截器
ValidAttribute 是一個(gè) Aop 參數(shù)攔截器,可以對實(shí)現(xiàn)了 IValidation 接口的單個(gè)對象或?qū)ο蠹线M(jìn)行驗(yàn)證.
/// <summary>
/// 驗(yàn)證攔截器
/// </summary>
public class ValidAttribute : ParameterInterceptorBase {
/// <summary>
/// 執(zhí)行
/// </summary>
public override async Task Invoke( ParameterAspectContext context, ParameterAspectDelegate next ) {
Validate( context.Parameter );
await next( context );
}
/// <summary>
/// 驗(yàn)證
/// </summary>
private void Validate( Parameter parameter ) {
if ( Reflection.IsGenericCollection( parameter.RawType ) ) {
ValidateCollection( parameter );
return;
}
IValidation validation = parameter.Value as IValidation;
validation?.Validate();
}
/// <summary>
/// 驗(yàn)證集合
/// </summary>
private void ValidateCollection( Parameter parameter ) {
if ( !( parameter.Value is IEnumerable<IValidation> validations ) )
return;
foreach ( var validation in validations )
validation.Validate();
}
}

浙公網(wǎng)安備 33010602011771號