ABP vNext 針對接口參數的校驗工做,分別由過濾器和攔截器兩步完成。過濾器內部使用的 ASP.NET Core MVC 所提供的 IModelStateValidator
進行處理,而攔截器使用的是 ABP vNext 本身提供的一套 IObjectValidator
進行校驗工做。html
關於參數驗證相關的代碼,分佈在如下三個項目當中:api
經過 MVC 的過濾器和 ABP vNext 提供的攔截器,咱們可以快速地對接口的參數、對象的屬性進行統一的驗證處理,而不會將這些代碼擴散到業務層當中。mvc
文章信息:框架
基於的 ABP vNext 版本:1.0.0async
創做日期:2019 年 10 月 22 日晚ide
更新日期:暫無源碼分析
模型驗證過濾器是直接使用的 MVC 那一套模型驗證機制,基於數據註解的方式進行校驗。數據註解也就是存放在 System.ComponentModel.DataAnnotations
命名空間下面的一堆特性定義,例如咱們常常在 DTO 上面使用的 [Required]
、[StringLength]
特性等,若是想知道更多的數據註解用法,能夠前往 MSDN 進行學習。單元測試
模型驗證過濾器 (AbpValidationActionFilter
) 的定義存放在 Volo.Abp.AspNetCore.Mvc 項目內部,它是在模塊的 ConfigureService()
方法中被注入到 IoC 容器的。學習
AbpAspNetCoreMvcModule
裏面的相關代碼:測試
namespace Volo.Abp.AspNetCore.Mvc { [DependsOn( typeof(AbpAspNetCoreModule), typeof(AbpLocalizationModule), typeof(AbpApiVersioningAbstractionsModule), typeof(AbpAspNetCoreMvcContractsModule), typeof(AbpUiModule) )] public class AbpAspNetCoreMvcModule : AbpModule { // public override void ConfigureServices(ServiceConfigurationContext context) { // ... Configure<MvcOptions>(mvcOptions => { mvcOptions.AddAbp(context.Services); }); } // ... } }
上述代碼是調用對 MvcOptions
編寫的 AddAbp(this MvcOptions, IServiceCollection)
擴展方法,傳入了咱們的 IoC 註冊容器(IServiceCollection
)。
AbpMvcOptionsExtensions
裏面的相關代碼:
internal static class AbpMvcOptionsExtensions { public static void AddAbp(this MvcOptions options, IServiceCollection services) { AddConventions(options, services); // 註冊過濾器。 AddFilters(options); AddModelBinders(options); AddMetadataProviders(options, services); } // ... private static void AddFilters(MvcOptions options) { options.Filters.AddService(typeof(AbpAuditActionFilter)); options.Filters.AddService(typeof(AbpFeatureActionFilter)); // 咱們的參數驗證過濾器。 options.Filters.AddService(typeof(AbpValidationActionFilter)); options.Filters.AddService(typeof(AbpUowActionFilter)); options.Filters.AddService(typeof(AbpExceptionFilter)); } // ... }
到這一步,咱們的 AbpValidationActionFilter
會被添加到 IoC 容器當中,以供 ASP.NET Core Mvc 框架進行使用。
咱們的驗證過濾器經過上述步驟,已經被注入到 IoC 容器當中了,之後咱們每次的接口調用都會進入 AbpValidationActionFilter
的 OnActionExecutionAsync()
方法內部。在這個過濾器的內部實現代碼中,咱們看到 ABP 爲咱們注入了一個 IModelStateValidator
對象。
public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency { private readonly IModelStateValidator _validator; public AbpValidationActionFilter(IModelStateValidator validator) { _validator = validator; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { //TODO: Configuration to disable validation for controllers..? //TODO: 是否應該增長一個配置項,以便開發人員禁用驗證功能 ? // 判斷當前請求是不是一個控制器行爲,是則返回 true。 // 第二個條件會判斷當前的接口返回值是 IActionResult、JsonResult、ObjectResult、NoContentResult 的一種,是則返回 true。 // 這裏則會忽略不是控制器的方法,控制器類型不是上述類型任意一種也會被忽略。 if (!context.ActionDescriptor.IsControllerAction() || !context.ActionDescriptor.HasObjectResult()) { await next(); return; } // 調用驗證器進行驗證操做。 _validator.Validate(context.ModelState); await next(); } }
過濾器的行爲很簡單,判斷當前的 API 請求是否符合條件,不符合則不進行參數驗證,不然調用 IModelStateValidator
的 Validate
方法,將模型狀態傳遞給它進行處理。
這個接口從名字上看,應該是模型狀態驗證器。由於咱們接口上面的參數,在 ASP.NET Core MVC 的使用當中,會進行模型綁定,即創建對象到 Http 請求參數的映射。
public interface IModelStateValidator { void Validate(ModelStateDictionary modelState); void AddErrors(IAbpValidationResult validationResult, ModelStateDictionary modelState); }
ABP vNext 的默認實現是 ModelStateValidator
,它的內部實現也很簡單。就是遍歷 ModelStateDictionary
對象的錯誤信息,將其添加到一個 AbpValidationResult
對象內部的 List
集合。這樣作的目的,是方便後面 ABP vNext 進行錯誤拋出。
public class ModelStateValidator : IModelStateValidator, ITransientDependency { public virtual void Validate(ModelStateDictionary modelState) { var validationResult = new AbpValidationResult(); AddErrors(validationResult, modelState); if (validationResult.Errors.Any()) { throw new AbpValidationException( "ModelState is not valid! See ValidationErrors for details.", validationResult.Errors ); } } public virtual void AddErrors(IAbpValidationResult validationResult, ModelStateDictionary modelState) { if (modelState.IsValid) { return; } foreach (var state in modelState) { foreach (var error in state.Value.Errors) { validationResult.Errors.Add(new ValidationResult(error.ErrorMessage, new[] { state.Key })); } } } }
當過濾器拋出了 AbpValidationException
異常以後,ABP vNext 會在異常過濾器 (AbpExceptionFilter
) 內部捕獲這個特定異常 (取決於異常繼承的 IHasValidationErrors
接口),並對其進行特殊的包裝。
[Serializable] public class AbpValidationException : AbpException, IHasLogLevel, // 注意這個接口。 IHasValidationErrors, IExceptionWithSelfLogging { // ... }
這一節至關因而一個擴展知識,幫助咱們瞭解數據註解的工做機制,以及 ModelStateDictionary
是怎麼被填充的。
擴展閱讀:
ABP vNext 除了使用 ASP.NET Core MVC 提供的模型驗證功能,本身也提供了一個單獨的驗證模塊。咱們先來看看模塊類型內部所執行的操做:
public class AbpValidationModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { // 添加攔截器註冊類。 context.Services.OnRegistred(ValidationInterceptorRegistrar.RegisterIfNeeded); // 添加對象驗證攔截器的輔助對象。 AutoAddObjectValidationContributors(context.Services); } private static void AutoAddObjectValidationContributors(IServiceCollection services) { var contributorTypes = new List<Type>(); // 在類型註冊的時候,若是類型實現了 IObjectValidationContributor 接口,則認定是驗證器的輔助類。 services.OnRegistred(context => { if (typeof(IObjectValidationContributor).IsAssignableFrom(context.ImplementationType)) { contributorTypes.Add(context.ImplementationType); } }); // 最後向 Options 類型添加輔助類的類型定義。 services.Configure<AbpValidationOptions>(options => { options.ObjectValidationContributors.AddIfNotContains(contributorTypes); }); } }
模塊在啓動時進行了兩個操做,第一是爲框架註冊對象驗證攔截器,第二則是添加 輔助類型(IObjectValidationContributor
) 的定義到配置類中,方便後續進行使用。
攔截器的注入行爲很簡單,主要註冊的類型實現了 IValidationEnabled
接口,就會爲其注入攔截器。
public static class ValidationInterceptorRegistrar { public static void RegisterIfNeeded(IOnServiceRegistredContext context) { if (typeof(IValidationEnabled).IsAssignableFrom(context.ImplementationType)) { context.Interceptors.TryAdd<ValidationInterceptor>(); } } }
public class ValidationInterceptor : AbpInterceptor, ITransientDependency { private readonly IMethodInvocationValidator _methodInvocationValidator; public ValidationInterceptor(IMethodInvocationValidator methodInvocationValidator) { _methodInvocationValidator = methodInvocationValidator; } public override void Intercept(IAbpMethodInvocation invocation) { Validate(invocation); invocation.Proceed(); } public override async Task InterceptAsync(IAbpMethodInvocation invocation) { Validate(invocation); await invocation.ProceedAsync(); } protected virtual void Validate(IAbpMethodInvocation invocation) { _methodInvocationValidator.Validate( new MethodInvocationValidationContext( invocation.TargetObject, invocation.Method, invocation.Arguments ) ); } }
攔截器內部只會調用 IMethodInvocationValidator
對象提供的 Validate()
方法,在調用時會將方法的參數,方法類型等數據封裝到 MethodInvocationValidationContext
。
這個上下文類型,自己就繼承了前面提到的 AbpValidationResult
類型,在其內部增長了存儲參數信息的屬性。
public class MethodInvocationValidationContext : AbpValidationResult { public object TargetObject { get; } // 方法的元數據信息。 public MethodInfo Method { get; } // 方法的具體參數值。 public object[] ParameterValues { get; } // 方法的參數信息。 public ParameterInfo[] Parameters { get; } public MethodInvocationValidationContext(object targetObject, MethodInfo method, object[] parameterValues) { TargetObject = targetObject; Method = method; ParameterValues = parameterValues; Parameters = method.GetParameters(); } }
接下來咱們看一下真正的 對象驗證器 ,也就是 IMethodInvocationValidator
的默認實現 MethodInvocationValidator
當中具體的操做。
// ... public virtual void Validate(MethodInvocationValidationContext context) { // ... AddMethodParameterValidationErrors(context); if (context.Errors.Any()) { ThrowValidationError(context); } } // ... protected virtual void AddMethodParameterValidationErrors(MethodInvocationValidationContext context) { // 循環調用 IObjectValidator 的 GetErrors 方法,捕獲參數的具體錯誤。 for (var i = 0; i < context.Parameters.Length; i++) { AddMethodParameterValidationErrors(context, context.Parameters[i], context.ParameterValues[i]); } } protected virtual void AddMethodParameterValidationErrors(IAbpValidationResult context, ParameterInfo parameterInfo, object parameterValue) { var allowNulls = parameterInfo.IsOptional || parameterInfo.IsOut || TypeHelper.IsPrimitiveExtended(parameterInfo.ParameterType, includeEnums: true); // 添加錯誤信息到 Errors 裏面,方便後面拋出。 context.Errors.AddRange( _objectValidator.GetErrors( parameterValue, parameterInfo.Name, allowNulls ) ); }
咱們看到,即使是在 IMethodInvocationValidator
內部,也沒有真正地進行參數驗證工做,而是調用了 IObjectValidator
進行對象驗證處理,其接口定義以下:
public interface IObjectValidator { void Validate( object validatingObject, string name = null, bool allowNull = false ); List<ValidationResult> GetErrors( object validatingObject, // 待驗證的值。 string name = null, // 參數的名字。 bool allowNull = false // 是否容許可空。 ); }
它的默認實現代碼以下:
public class ObjectValidator : IObjectValidator, ITransientDependency { protected IHybridServiceScopeFactory ServiceScopeFactory { get; } protected AbpValidationOptions Options { get; } public ObjectValidator(IOptions<AbpValidationOptions> options, IHybridServiceScopeFactory serviceScopeFactory) { ServiceScopeFactory = serviceScopeFactory; Options = options.Value; } public virtual void Validate(object validatingObject, string name = null, bool allowNull = false) { var errors = GetErrors(validatingObject, name, allowNull); if (errors.Any()) { throw new AbpValidationException( "Object state is not valid! See ValidationErrors for details.", errors ); } } public virtual List<ValidationResult> GetErrors(object validatingObject, string name = null, bool allowNull = false) { // 若是待驗證的值爲空。 if (validatingObject == null) { // 若是參數自己是容許可空的,那麼直接返回。 if (allowNull) { return new List<ValidationResult>(); //TODO: Returning an array would be more performent } else { // 不然在錯誤信息裏面加入不能爲空的錯誤。 return new List<ValidationResult> { name == null ? new ValidationResult("Given object is null!") : new ValidationResult(name + " is null!", new[] {name}) }; } } // 構造一個新的上下文,將其分派給輔助類進行驗證。 var context = new ObjectValidationContext(validatingObject); using (var scope = ServiceScopeFactory.CreateScope()) { // 遍歷以前模塊啓動的輔助類型。 foreach (var contributorType in Options.ObjectValidationContributors) { // 經過 IoC 建立實例。 var contributor = (IObjectValidationContributor) scope.ServiceProvider.GetRequiredService(contributorType); // 調用輔助類型進行具體認證。 contributor.AddErrors(context); } } return context.Errors; } }
因此咱們的對象驗證,尚未真正的進行驗證處理,全部的驗證操做都是由各個 驗證輔助類型 處理的。而這些輔助類型有兩種,第一是基於數據註解 的 驗證輔助類型,第二種則是基於 FluentValidation 庫編寫的一種驗證輔助類。
雖然 ABP vNext 套了三層,最終只是爲了方便咱們開發人員重寫各個階段的實現,也就更加地靈活可控。
ABP vNext 爲了下降咱們的學習成本,自己也是支持 ASP.NET Core MVC 那一套數據註解校驗。你能夠在某個非控制器類型的參數上,使用 [Required]
等數據註解特性。
它的默認實現我就再也不多加贅述,基本就是經過反射獲得參數對象上面的全部 ValidationAttribute
特性,顯式地調用 GetValidationResult()
方法,獲取到具體的錯誤信息,而後添加到上下文結果當中。
foreach (var attribute in validationAttributes) { var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext); if (result != null) { errors.Add(result); } }
另外注意,這個遞歸驗證的深度是 8 級,在輔助類型的 MaxRecursiveParameterValidationDepth
常量中進行了定義。也就是說,你這個對象圖的邏輯層級不能超過 8 級。
public class A1 { [Required] public string Name { get; set;} public B2 B2 { get; set;} } public class B2 { [StringLength(8)] public string Name { get; set;} }
若是你方法參數是 A1
類型的話,那麼這就有 2 層了。
回想上一節說的驗證輔助類,還有一個基於 FluentValidation 庫的類型,這裏對於該庫的使用方法參考單元測試便可。我這裏只講解一下,這個輔助類型是如何進行驗證的。
public class FluentObjectValidationContributor : IObjectValidationContributor, ITransientDependency { private readonly IServiceProvider _serviceProvider; public FluentObjectValidationContributor( IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void AddErrors(ObjectValidationContext context) { // 構造泛型類型,若是你對 Person 寫了個驗證器,那麼驗證器類型就是 IValidator<Person>。 var serviceType = typeof(IValidator<>).MakeGenericType(context.ValidatingObject.GetType()); // 經過 IoC 得到一個實例。 var validator = _serviceProvider.GetService(serviceType) as IValidator; if (validator == null) { return; } // 調用驗證器的方法進行驗證。 var result = validator.Validate(context.ValidatingObject); if (!result.IsValid) { // 得到錯誤數據,將 FluentValidation 的錯誤轉換爲標準的錯誤信息。 context.Errors.AddRange( result.Errors.Select( error => new ValidationResult(error.ErrorMessage) ) ); } } }
單元測試當中的基本用法:
public class MyMethodInputValidator : AbstractValidator<MyMethodInput> { public MyMethodInputValidator() { RuleFor(x => x.MyStringValue).Equal("aaa"); RuleFor(x => x.MyMethodInput2.MyStringValue2).Equal("bbb"); RuleFor(customer => customer.MyMethodInput3).SetValidator(new MyMethodInput3Validator()); } }
總的來講 ABP vNext 爲咱們提供了多種參數驗證方法,通常來講使用 MVC 過濾器配合數據註解就夠了。若是你確實有一些特殊的需求,那也可使用本身的方式對參數進行驗證,只須要實現 IObjectValidationContributor
接口就行。
須要看其餘的 ABP vNext 相關文章?點擊我 便可跳轉到總目錄。