一、簡要說明
ABP vNext 針對介面引數的校驗作業,分別由過濾器和攔截器兩步完成,過濾器內部使用的 ASP.NET Core MVC 所提供的 IModelStateValidator 進行處理,而攔截器使用的是 ABP vNext 自己提供的一套 IObjectValidator 進行校驗作業,
關于引數驗證相關的代碼,分布在以下三個專案當中:
- Volo.Abp.AspNetCore.Mvc
- Volo.Abp.Validation
- Volo.Abp.FluentValidation
通過 MVC 的過濾器和 ABP vNext 提供的攔截器,我們能夠快速地對介面的引數、物件的屬性進行統一的驗證處理,而不會將這些代碼擴散到業務層當中,
文章資訊:
基于的 ABP vNext 版本:1.0.0
創作日期:2019 年 10 月 22 日晚
更新日期:暫無
二、原始碼分析
2.1 模型驗證過濾器
模型驗證過濾器是直接使用的 MVC 那一套模型驗證機制,基于資料注解的方式進行校驗,資料注解也就是存放在 System.ComponentModel.DataAnnotations 命名空間下面的一堆特性定義,例如我們經常在 DTO 上面使用的 [Required] 、[StringLength] 特性等,如果想知道更多的資料注解用法,可以前往 MSDN 進行學習,
2.1.1 過濾器的注入
模型驗證過濾器 (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 框架進行使用,
2.1.2 過濾器的驗證流程
我們的驗證過濾器通過上述步驟,已經被注入到 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 }));
}
}
}
}
2.1.3 結果的包裝
當過濾器拋出了 AbpValidationException 例外之后,ABP vNext 會在例外過濾器 (AbpExceptionFilter) 內部捕獲這個特定例外 (取決于例外繼承的 IHasValidationErrors 介面),并對其進行特殊的包裝,
[Serializable]
public class AbpValidationException : AbpException,
IHasLogLevel,
// 注意這個介面,
IHasValidationErrors,
IExceptionWithSelfLogging
{
// ...
}


2.1.4 資料注解的驗證
這一節相當于是一個擴展知識,幫助我們了解資料注解的作業機制,以及 ModelStateDictionary 是怎么被填充的,

擴展閱讀:
-
ASP.NET Core 模型驗證詳解
-
.NET Core 開發日志 -- Model Binding
2.2 物件驗證攔截器
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) 的定義到配置類中,方便后續進行使用,
2.2.1 攔截器的注入
攔截器的注入行為很簡單,主要注冊的型別實作了 IValidationEnabled 介面,就會為其注入攔截器,
public static class ValidationInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (typeof(IValidationEnabled).IsAssignableFrom(context.ImplementationType))
{
context.Interceptors.TryAdd<ValidationInterceptor>();
}
}
}
2.2.2 攔截器的行為
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
)
);
}
2.2.3 “真正”的引數驗證器
我們看到,即便是在 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 套了三層,最終只是為了方便我們開發人員重寫各個階段的實作,也就更加地靈活可控,
2.2.4 默認的資料注解驗證
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 層了,
2.3 流暢驗證庫
回想上一節說的驗證輔助類,還有一個基于 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 相關文章?點擊我 即可跳轉到總目錄,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/103594.html
標籤:.NET Core
