一、简要说明# 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 里面的相关代码: Copy 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.AddAbp(context.Services); }); } // ... } } 上述代码是调用对 MvcOptions 编写的 AddAbp(this MvcOptions, IServiceCollection) 扩展方法,传入了我们的 IoC 注册容器(IServiceCollection)。 AbpMvcOptionsExtensions 里面的相关代码: Copy 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 对象。 Copy 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 请求参数的映射。 Copy public interface IModelStateValidator { void Validate(ModelStateDictionary modelState); void AddErrors(IAbpValidationResult validationResult, ModelStateDictionary modelState); } ABP vNext 的默认实现是 ModelStateValidator ,它的内部实现也很简单。就是遍历 ModelStateDictionary 对象的错误信息,将其添加到一个 AbpValidationResult 对象内部的 List 集合。这样做的目的,是方便后面 ABP vNext 进行错误抛出。 Copy 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 接口),并对其进行特殊的包装。 Copy [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 提供的模型验证功能,自己也提供了一个单独的验证模块。我们先来看看模块类型内部所执行的操作: Copy 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(); // 在类型注册的时候,如果类型实现了 IObjectValidationContributor 接口,则认定是验证器的辅助类。 services.OnRegistred(context => { if (typeof(IObjectValidationContributor).IsAssignableFrom(context.ImplementationType)) { contributorTypes.Add(context.ImplementationType); } }); // 最后向 Options 类型添加辅助类的类型定义。 services.Configure(options => { options.ObjectValidationContributors.AddIfNotContains(contributorTypes); }); } } 模块在启动时进行了两个操作,第一是为框架注册对象验证拦截器,第二则是添加 辅助类型(IObjectValidationContributor) 的定义到配置类中,方便后续进行使用。 2.2.1 拦截器的注入# 拦截器的注入行为很简单,主要注册的类型实现了 IValidationEnabled 接口,就会为其注入拦截器。 Copy public static class ValidationInterceptorRegistrar { public static void RegisterIfNeeded(IOnServiceRegistredContext context) { if (typeof(IValidationEnabled).IsAssignableFrom(context.ImplementationType)) { context.Interceptors.TryAdd(); } } } 2.2.2 拦截器的行为# Copy 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 类型,在其内部增加了存储参数信息的属性。 Copy 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 当中具体的操作。 Copy // ... 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 进行对象验证处理,其接口定义如下: Copy public interface IObjectValidator { void Validate( object validatingObject, string name = null, bool allowNull = false ); List GetErrors( object validatingObject, // 待验证的值。 string name = null, // 参数的名字。 bool allowNull = false // 是否允许可空。 ); } 它的默认实现代码如下: Copy public class ObjectValidator : IObjectValidator, ITransientDependency { protected IHybridServiceScopeFactory ServiceScopeFactory { get; } protected AbpValidationOptions Options { get; } public ObjectValidator(IOptions 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 GetErrors(object validatingObject, string name = null, bool allowNull = false) { // 如果待验证的值为空。 if (validatingObject == null) { // 如果参数本身是允许可空的,那么直接返回。 if (allowNull) { return new List(); //TODO: Returning an array would be more performent } else { // 否则在错误信息里面加入不能为空的错误。 return new List { 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() 方法,获取到具体的错误信息,然后添加到上下文结果当中。 Copy foreach (var attribute in validationAttributes) { var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext); if (result != null) { errors.Add(result); } } 另外注意,这个递归验证的深度是 8 级,在辅助类型的 MaxRecursiveParameterValidationDepth 常量中进行了定义。也就是说,你这个对象图的逻辑层级不能超过 8 级。 Copy public class A1 { [Required] public string