一、簡要說明
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
是怎么被填充的。
擴展閱讀:
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 相關文章?點擊我 即可跳轉到總目錄。