[Abp 源码分析]DTO 自动验证
點擊上方藍字關注我們
0.簡介
在平時開發 API 接口的時候需要對前端傳入的參數進行校驗之后才能進入業務邏輯進行處理,否則一旦前端傳入一些非法/無效數據到 API 當中,輕則導致程序報錯,重則導致整個業務流程出現問題。
用過傳統 ASP.NET MVC 數據注解的同學應該知道,我們可以通過在 Model 上面指定各種數據特性,然后在前端調用 API 的時候就會根據這些注解來校驗 Model 內部的字段是否合法。
1.啟動流程
Abp 針對于數據校驗分為兩個地方進行,第一個是 MVC 的過濾器,也是我們最常使用的。第二個則是借助于 Castle 的攔截器實現的 DTO 數據校驗功能,前者只能用于控制器方法,而后者則支持普通方法。
1.1 過濾器注入
在注入 Abp 的時候,通過 AddAbp() 方法內部的?ConfigureAspNetCore()?配置了諸多過濾器。
private static void ConfigureAspNetCore(IServiceCollection services, IIocResolver iocResolver) {// ... 其他代碼//Configure MVCservices.Configure<MvcOptions>(mvcOptions =>{mvcOptions.AddAbp(services);});// ... 其他代碼 }過濾器注入方法:
internal static class AbpMvcOptionsExtensions {public static void AddAbp(this MvcOptions options, IServiceCollection services){// ... 其他代碼AddFilters(options);// ... 其他代碼}// ... 其他代碼private static void AddFilters(MvcOptions options){// ... 其他過濾器注入// 注入參數驗證過濾器options.Filters.AddService(typeof(AbpValidationActionFilter));// ... 其他過濾器注入}// ... 其他代碼 }1.2 攔截器注入
Abp 針對于驗證攔截器的注冊始于?AbpBootstrapper?類,該基類在之前曾經多次出現過,也就是在用戶調用?IServiceCollection.AddAbp<TStartupModule>()?方法的時候會初始化該類的一個實例對象。在該類的構造函數當中,會調用一個?AddInterceptorRegistrars()?方法用于添加各種攔截器的注冊類實例。代碼如下:
public class AbpBootstrapper : IDisposable {private AbpBootstrapper([NotNull] Type startupModule, [CanBeNull] Action<AbpBootstrapperOptions> optionsAction = null){// ... 其他代碼if (!options.DisableAllInterceptors){AddInterceptorRegistrars();}}// ... 其他代碼// 添加各種攔截器private void AddInterceptorRegistrars(){ValidationInterceptorRegistrar.Initialize(IocManager);AuditingInterceptorRegistrar.Initialize(IocManager);EntityHistoryInterceptorRegistrar.Initialize(IocManager);UnitOfWorkRegistrar.Initialize(IocManager);AuthorizationInterceptorRegistrar.Initialize(IocManager);}// ... 其他代碼\ }來到?ValidationInterceptorRegistrar?類型定義當中可以看到,其內部就是通過 Castle 的 IocContainer 來針對每次注入的應用服務應用上參數驗證攔截器。
internal static class ValidationInterceptorRegistrar {public static void Initialize(IIocManager iocManager){iocManager.IocContainer.Kernel.ComponentRegistered += Kernel_ComponentRegistered;}private static void Kernel_ComponentRegistered(string key, IHandler handler){// 判斷是否實現了 IApplicationService 接口,如果實現了,則為該對象添加攔截器if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation)){handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(ValidationInterceptor)));}} }2.代碼分析
從 Abp 庫代碼當中我們可以知道其攔截器與過濾器是在何時被注入的,下面我們就來具體分析一下他們的處理邏輯。
2.1 過濾器代碼分析
Abp 在框架初始化的時候就將?AbpValidationActionFilter?添加到 MVC 的配置當中,其自定義實現的攔截器實現了?IAsyncActionFilter?接口,也就是說當每次接口被調用的時候都會進入該攔截器的內部。
public class AbpValidationActionFilter : IAsyncActionFilter, ITransientDependency {// Ioc 解析器,用于解析各種注入的組件private readonly IIocResolver _iocResolver;// Abp 針對與 ASP.NET Core 的配置項,主要作用是判斷用戶是否需要檢測控制器方法private readonly IAbpAspNetCoreConfiguration _configuration;public AbpValidationActionFilter(IIocResolver iocResolver, IAbpAspNetCoreConfiguration configuration){_iocResolver = iocResolver;_configuration = configuration;}public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){// ... 處理邏輯} }在內部首先是結合配置項判斷用戶是否禁用了 MVC Controller 的參數驗證功能,禁用了則不進行任何操作。
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {// 判斷是否禁用了控制器檢測if (!_configuration.IsValidationEnabledForControllers || !context.ActionDescriptor.IsControllerAction()){await next();return;}// 針對應用服務增加一個驗證完成標識using (AbpCrossCuttingConcerns.Applying(context.Controller, AbpCrossCuttingConcerns.Validation)){// 解析出方法驗證器,傳入請求上下文,并且調用這些驗證器具體的驗證方法using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>()){validator.Object.Initialize(context);validator.Object.Validate();}await next();} }其實我們這里看到有一個?AbpCrossCuttingConcerns.Applying()?方法,那么該方法的作用是什么呢?
在這里我先大體講述一下該方法的作用,該方法主要是向應用服務對象 (也就是繼承了?ApplicationService?類的對象) 內部的 AppliedCrossCuttingConcerns 屬性增加一個常量值,在這里也就是?AbpCrossCuttingConcerns.Validation?的值,也就是一個字符串。
那么其作用是什么呢,就是防止重復驗證。從啟動流程一節我們就已經知道 Abp 框架在啟動的時候除了注入過濾器之外,還會注入攔截器進行接口參數驗證,當過濾器驗證過之后,其實沒必要再使用攔截器進行二次驗證。
所以在攔截器的?Intercept()?方法內部會有這樣一句代碼:
public void Intercept(IInvocation invocation) {// 判斷是否擁有處理過的標識if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){invocation.Proceed();return;}// ... 其他代碼 }解釋完?AbpCrossCuttingConcerns.Applying()?之后,我們繼續往下看代碼。
// 解析出方法驗證器,傳入請求上下文,并且調用這些驗證器具體的驗證方法 using (var validator = _iocResolver.ResolveAsDisposable<MvcActionInvocationValidator>()) {validator.Object.Initialize(context);validator.Object.Validate(); }await next();這里就比較簡單了,過濾器通過?IocResolver?解析出來了一個?MvcActionInvocationValidator?對象,使用該對象來校驗具體的參數內容。
2.2 攔截器代碼分析
看完過濾器代碼之后,其實攔截器代碼更加簡單。整體邏輯上面與過濾器差不多,只不過針對于攔截器,它是通過一個?MethodInvocationValidator?對象來校驗傳入的參數內容。
public class ValidationInterceptor : IInterceptor {// Ioc 解析器,用于解析各種注入的組件private readonly IIocResolver _iocResolver;public ValidationInterceptor(IIocResolver iocResolver){_iocResolver = iocResolver;}public void Intercept(IInvocation invocation){// 判斷過濾器是否已經處理過if (AbpCrossCuttingConcerns.IsApplied(invocation.InvocationTarget, AbpCrossCuttingConcerns.Validation)){// 處理過則直接進入具體方法內部,執行業務邏輯invocation.Proceed();return;}// 解析出方法驗證器,傳入請求上下文,并且調用這些驗證器具體的驗證方法using (var validator = _iocResolver.ResolveAsDisposable<MethodInvocationValidator>()){validator.Object.Initialize(invocation.MethodInvocationTarget, invocation.Arguments);validator.Object.Validate();}invocation.Proceed();} }可以看到兩個過濾器與攔截器業務邏輯相似,但都是通過驗證器來進行處理的,那么驗證器又是個什么鬼東西呢?
2.3 參數驗證器
驗證器即是用來具體執行驗證邏輯的工具,從上述代碼里面我們可以看到過濾器和攔截器都是通過解析出?MethodInvocationValidator/MvcActionInvocationValidator?之后調用其驗證方法進行驗證的。
首先我們來看一下 MVC 的驗證器是如何進行處理的,看方法類型的定義,可以看到其繼承了一個基類,叫?ActionInvocationValidatorBase,而這個基類呢,又繼承自?MethodInvocationValidator。
public class MvcActionInvocationValidator : ActionInvocationValidatorBase {// ... 其他代碼 } public abstract class ActionInvocationValidatorBase : MethodInvocationValidator {// ... 其他代碼 }所以我們分析代碼的順序調整一下,先看一下?MethodInvocationValidator?的內部是如何做處理的吧,這個類型內部還是比較簡單的,可能除了有一個遞歸有點繞之外。
其主要功能就是拿著傳遞進來的參數值,通過在 Abp 框架啟動的時候注入的具體驗證器(用戶自定義驗證器)來遞歸校驗每個參數的值。
/// <summary> /// 本類用于需要參數驗證的方法. /// </summary> public class MethodInvocationValidator : ITransientDependency {// 最大迭代驗證次數private const int MaxRecursiveParameterValidationDepth = 8;// 待驗證的方法信息protected MethodInfo Method { get; private set; }// 傳入的參數值protected object[] ParameterValues { get; private set; }// 方法參數信息protected ParameterInfo[] Parameters { get; private set; }protected List<ValidationResult> ValidationErrors { get; }protected List<IShouldNormalize> ObjectsToBeNormalized { get; }private readonly IValidationConfiguration _configuration;private readonly IIocResolver _iocResolver;public MethodInvocationValidator(IValidationConfiguration configuration, IIocResolver iocResolver){_configuration = configuration;_iocResolver = iocResolver;ValidationErrors = new List<ValidationResult>();ObjectsToBeNormalized = new List<IShouldNormalize>();}// 初始化攔截器參數public virtual void Initialize(MethodInfo method, object[] parameterValues){Check.NotNull(method, nameof(method));Check.NotNull(parameterValues, nameof(parameterValues));Method = method;ParameterValues = parameterValues;Parameters = method.GetParameters();}// 開始驗證參數的有效性public void Validate(){// 檢測是否初始化,沒有初始化則拋出系統級異常CheckInitialized();// 檢測方法是否有參數if (Parameters.IsNullOrEmpty()){return;}// 檢測方法是否為公開方法if (!Method.IsPublic){return;}// 如果沒有開啟方法參數檢測,則直接返回if (IsValidationDisabled()){return; }// 如果方法所定義的參數數量與傳入的參數值數量匹配不上,則拋出系統級異常if (Parameters.Length != ParameterValues.Length){throw new Exception("Method parameter count does not match with argument count!");}// 遍歷方法的參數列表,使用傳入的參數值進行校驗for (var i = 0; i < Parameters.Length; i++){ValidateMethodParameter(Parameters[i], ParameterValues[i]);}// 如果校驗的錯誤結果集合有任意一條數據,則拋出用戶異常,返回給前端展示if (ValidationErrors.Any()){ThrowValidationError();}foreach (var objectToBeNormalized in ObjectsToBeNormalized){objectToBeNormalized.Normalize();}}// ... 忽略的代碼// 校驗調用方法時傳遞的參數與參數值protected virtual void ValidateMethodParameter(ParameterInfo parameterInfo, object parameterValue){// 如果參數值為空的情況下,做一系列特殊判斷if (parameterValue == null){if (!parameterInfo.IsOptional && !parameterInfo.IsOut && !TypeHelper.IsPrimitiveExtendedIncludingNullable(parameterInfo.ParameterType, includeEnums: true)){ValidationErrors.Add(new ValidationResult(parameterInfo.Name + " is null!", new[] { parameterInfo.Name }));}return;}// 遞歸校驗參數ValidateObjectRecursively(parameterValue, 1);}protected virtual void ValidateObjectRecursively(object validatingObject, int currentDepth){// 驗證層級是否超過了最大層級(8)if (currentDepth > MaxRecursiveParameterValidationDepth){return;}// 值是否為空,為空則不繼續進行校驗if (validatingObject == null){return;}// 判斷其類型是否是用戶配置的忽略類型,忽略則不進行校驗if (_configuration.IgnoredTypes.Any(t => t.IsInstanceOfType(validatingObject))){return;}// 判斷參數類型是否為基本類型if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObject.GetType())){return;}SetValidationErrors(validatingObject);// 判定參數類型是否實現了 IEnumerabe 接口,如果實現了,則遞歸遍歷校驗其內部的元素if (IsEnumerable(validatingObject)){foreach (var item in (IEnumerable) validatingObject){ValidateObjectRecursively(item, currentDepth + 1);}}// 如果實現了標準化接口,則進行標準化操作if (validatingObject is IShouldNormalize){ObjectsToBeNormalized.Add(validatingObject as IShouldNormalize);}// 是否還需要繼續遞歸校驗if (ShouldMakeDeepValidation(validatingObject)){var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();foreach (var property in properties){// 如果有禁止校驗的特性則忽略if (property.Attributes.OfType<DisableValidationAttribute>().Any()){continue;}ValidateObjectRecursively(property.GetValue(validatingObject), currentDepth + 1);}}}// ... 其他代碼protected virtual bool ShouldValidateUsingValidator(object validatingObject, Type validatorType){return true;}// 是否進行深度驗證protected virtual bool ShouldMakeDeepValidation(object validatingObject){// 不需要遞歸集合對象if (validatingObject is IEnumerable){return false;}var validatingObjectType = validatingObject.GetType();// 不需要遞歸基礎類型的對象if (TypeHelper.IsPrimitiveExtendedIncludingNullable(validatingObjectType)){return false;}return true;}// ... 其他代碼 }有朋友可能會奇怪,在方法內部不是通過?IEnumerable?判斷之后來進行遞歸校驗么,為什么在最后面還有一個深度驗證呢?
這是因為當前對象除了是一個集合的情況之外,還有可能其內部某個對象是另外一個用戶所自定義的復雜對象,這個時候就必須要通過深度驗證來校驗各個參數的值。不過這個遞歸也是有限度的,通過?MaxRecursiveParameterValidationDepth?來控制這個迭代層數為 8 層。如果不加以限制的話,那么很有可能出現循環引用而產生死循環的情況,或者是層級過深導致接口相應緩慢。
那么在這里執行具體校驗操作的則是那些實現了?IMethodParameterValidator?接口的對象,這些對象在 Abp 核心模塊(AbpKernelModule)的預加載的時候被添加到了?Configuration.Validation.Validators?屬性當中。
當然用戶也可以在自己的模塊預加載方法當中增加自己的參數驗證器,只要實現該接口即可。
public sealed class AbpKernelModule : AbpModule {public override void PreInitialize(){// ... 其他代碼// 增加需要忽略的類型AddIgnoredTypes();// 增加參數校驗器AddMethodParameterValidators();}private void AddMethodParameterValidators(){Configuration.Validation.Validators.Add<DataAnnotationsValidator>();Configuration.Validation.Validators.Add<ValidatableObjectValidator>();Configuration.Validation.Validators.Add<CustomValidator>();}// Abp 默認需要忽略的對象private void AddIgnoredTypes(){var commonIgnoredTypes = new[]{typeof(Stream),typeof(Expression)};foreach (var ignoredType in commonIgnoredTypes){Configuration.Auditing.IgnoredTypes.AddIfNotContains(ignoredType);Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}var validationIgnoredTypes = new[] { typeof(Type) };foreach (var ignoredType in validationIgnoredTypes){Configuration.Validation.IgnoredTypes.AddIfNotContains(ignoredType);}} }之后呢,回到之前的校驗方法,可以看到在?SetValidationErrors(object validatingObject)?方法里面遍歷了之前被注入的驗證器集合,然后調用其?Validate()?方法來進行具體的參數校驗。
protected virtual void SetValidationErrors(object validatingObject) {foreach (var validatorType in _configuration.Validators){if (ShouldValidateUsingValidator(validatingObject, validatorType)){using (var validator = _iocResolver.ResolveAsDisposable<IMethodParameterValidator>(validatorType)){var validationResults = validator.Object.Validate(validatingObject);ValidationErrors.AddRange(validationResults);}}} }2.4 具體的參數驗證器
這里以 Abp 默認實現的?DataAnnotationValidator?類型為例,可以看看他是怎么來根據參數的數據注解來驗證參數是否正確的。
public class DataAnnotationsValidator : IMethodParameterValidator {public virtual IReadOnlyList<ValidationResult> Validate(object validatingObject){return GetDataAnnotationAttributeErrors(validatingObject);}protected virtual List<ValidationResult> GetDataAnnotationAttributeErrors(object validatingObject){var validationErrors = new List<ValidationResult>();var properties = TypeDescriptor.GetProperties(validatingObject).Cast<PropertyDescriptor>();// 獲得參數值的所有屬性,如果傳入的是一個 DTO 對象的話,他內部肯定會有很多屬性的foreach (var property in properties){var validationAttributes = property.Attributes.OfType<ValidationAttribute>().ToArray();// 沒有數據注解特性,跳過當前屬性處理if (validationAttributes.IsNullOrEmpty()){continue;}// 創建一個錯誤信息上下文,用戶數據注解工具進行校驗var validationContext = new ValidationContext(validatingObject){DisplayName = property.DisplayName,MemberName = property.Name};// 根據特性來校驗參數結果foreach (var attribute in validationAttributes){var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);if (result != null){validationErrors.Add(result);}}}return validationErrors;} }3. 后記
最近工作較忙,可能更新速度不會像原來那么快,不過我盡可能在國慶結束后完成剩余文章,謝謝大家的支持。
作者:myzony
出處:https://www.cnblogs.com/myzony/p/9716742.html
公眾號“碼俠江湖”所發表內容注明來源的,版權歸原出處所有(無法查證版權的或者未注明出處的均來自網絡,系轉載,轉載的目的在于傳遞更多信息,版權屬于原作者。如有侵權,請聯系,筆者會第一時間刪除處理!
掃描二維碼
獲取更多精彩
碼俠江湖
喜歡就點個在看再走吧
總結
以上是生活随笔為你收集整理的[Abp 源码分析]DTO 自动验证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 规则引擎RulesEngine
- 下一篇: [Abp 源码分析]多语言(本地化)处理