[Abp vNext 原始碼分析] - 9. 介面引數的驗證
一、簡要說明
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
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 相關文章?點選我 即可跳轉到總目錄。