一、简要说明
ABP vNext 针对接口参数的校验工作,分别由过滤器和拦截器两步完成。过滤器内部使用的 ASP.NET Core MVC 所提供的 IModelStateValidator
进行处理,而拦截器使用的是 ABP vNext 自己提供的一套 IObjectValidator
进行校验工作。
关于参数验证相关的代码,分布在以下三个项目当中:
- Volo.Abp.AspNetCore.Mvc
- Volo.Abp.Validation
- Volo.Abp.FluentValidation
通过 MVC 的过滤器和 ABP vNext 提供的拦截器,我们能够快速地对接口的参数、对象的属性进行统一的验证处理,而不会将这些代码扩散到业务层当中。
二、源码分析
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
接口就行。