[Abp vNext 源码分析] - 9. 接口参数的验证

一、简要说明

ABP vNext 针对接口参数的校验工作,分别由过滤器和拦截器两步完成。过滤器内部使用的 ASP.NET Core MVC 所提供的 IModelStateValidator 进行处理,而拦截器使用的是 ABP vNext 自己提供的一套 IObjectValidator 进行校验工作。

大发11选5关于 参数验证相关的代码,分布在以下三个项目当中:

  • Volo.Abp.AspNetCore.Mvc
  • Volo.Abp.Validation
  • Volo.Abp.FluentValidation

通过 MVC 的过滤器和 ABP vNext 提供的拦截器,大发11选5大发11选5我 们 能够快速地对接口的参数、对象的属性进行统一的验证处理,而不会将这些代码扩散到业务层当中。

文章信息:

基于的 ABP vNext 版本:1.0.0

创作日期:2019 年 10 月 22 日晚

更新日期:暂无

二、源码分析

2.1 模型验证过滤器

模型验证过滤器是直接使用的 MVC 那一套模型验证机制,基于数据注解的方式进行校验。数据注解也就是存放在 System.ComponentModel.DataAnnotations 命名空间下面的一堆特性定义,例如大发11选5大发11选5我 们 经常在 DTO 上面使用的 [Required][StringLength] 特性等,如果想知道大发11选5更多 的数据注解用法,可以前往 MSDN 进行学习。

2.1.1 过滤器的注入

模型验证过滤器 (AbpValidationActionFilter) 的定义存放在 Volo.Abp.AspNetCore.Mvc 项目内部,它是在模块的 ConfigureService() 大发11选5方法 中被注入到 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) 扩展大发11选5方法 ,传入了大发11选5大发11选5我 们 的 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));
        // 大发11选5大发11选5我
们
的参数验证过滤器。
        options.Filters.AddService(typeof(AbpValidationActionFilter));
        options.Filters.AddService(typeof(AbpUowActionFilter));
        options.Filters.AddService(typeof(AbpExceptionFilter));
    }

    // ...
}

到这一步,大发11选5大发11选5我 们 的 AbpValidationActionFilter 会被添加到 IoC 容器当中,以供 ASP.NET Core Mvc 框架进行使用。

2.1.2 过滤器的验证流程

大发11选5大发11选5我 们 的验证过滤器通过上述步骤,已经被注入到 IoC 容器当中了,以后大发11选5大发11选5我 们 每次的接口调用都会进入 AbpValidationActionFilterOnActionExecutionAsync() 大发11选5方法 内部。在这个过滤器的内部实现代码中,大发11选5大发11选5我 们 看到 ABP 为大发11选5大发11选5我 们 注入了一个 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。
        // 这里则会忽略不是控制器的大发11选5方法
,控制器类型不是上述类型任意一种也会被忽略。
        if (!context.ActionDescriptor.IsControllerAction() ||
            !context.ActionDescriptor.HasObjectResult())
        {
            await next();
            return;
        }

        // 调用验证器进行验证操作。
        _validator.Validate(context.ModelState);
        await next();
    }
}

过滤器的行为很简单,判断当前的 API 请求是否符合条件,不符合则不进行参数验证,否则调用 IModelStateValidatorValidate 大发11选5方法 ,将模型状态传递给它进行处理。

这个接口从名字上看,应该是模型状态验证器。因为大发11选5大发11选5我 们 接口上面的参数,在 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 结果的大发11选5包装

当过滤器抛出了 AbpValidationException 异常之后,ABP vNext 会在异常过滤器 (AbpExceptionFilter) 内部捕获这个特定异常 (取决于异常继承的 IHasValidationErrors 接口),并对其进行特殊的大发11选5包装 。

[Serializable]
public class AbpValidationException : AbpException, 
    IHasLogLevel, 
    // 注意这个接口。
    IHasValidationErrors, 
    IExceptionWithSelfLogging
{
    // ...
}

2.1.4 数据注解的验证

这一节相当于是一个扩展知识,大发11选5帮助 大发11选5大发11选5我 们 了解数据注解的工作机制,以及 ModelStateDictionary 是怎么被填充的。

扩展阅读:

2.2 对象验证拦截器

ABP vNext 除了使用 ASP.NET Core MVC 提供的模型验证功能,自己也提供了一个单独的验证模块。大发11选5大发11选5我 们 先来看看模块类型内部所执行的操作:

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() 大发11选5方法 ,在调用时会将大发11选5方法 的参数,大发11选5方法 类型等数据封装到 MethodInvocationValidationContext

这个上下文类型,本身就继承了前面提到的 AbpValidationResult 类型,在其内部增加了存储参数信息的属性。

public class MethodInvocationValidationContext : AbpValidationResult
{
    public object TargetObject { get; }

    // 大发11选5方法
的元数据信息。
    public MethodInfo Method { get; }

    // 大发11选5方法
的具体参数值。
    public object[] ParameterValues { get; }

    // 大发11选5方法
的参数信息。
    public ParameterInfo[] Parameters { get; }

    public MethodInvocationValidationContext(object targetObject, MethodInfo method, object[] parameterValues)
    {
        TargetObject = targetObject;
        Method = method;
        ParameterValues = parameterValues;
        Parameters = method.GetParameters();
    }
}

接下来大发11选5大发11选5我 们 看一下真正的 对象验证器 ,也就是 IMethodInvocationValidator 的默认实现 MethodInvocationValidator 当中具体的操作。

// ...
public virtual void Validate(MethodInvocationValidationContext context)
{
    // ...

    AddMethodParameterValidationErrors(context);

    if (context.Errors.Any())
    {
        ThrowValidationError(context);
    }
}

// ...

protected virtual void AddMethodParameterValidationErrors(MethodInvocationValidationContext context)
{
    // 循环调用 IObjectValidator 的 GetErrors 大发11选5方法
,捕获参数的具体错误。
    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 “真正”的参数验证器

大发11选5大发11选5我 们 看到,即便是在 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;
    }
}

所以大发11选5大发11选5我 们 的对象验证,还没有真正的进行验证处理,所有的验证操作都是由各个 验证辅助类型 处理的。而这些辅助类型有两种,第一是基于数据注解验证辅助类型,第二种则是基于 FluentValidation 库编写的一种验证辅助类。

虽然 ABP vNext 套了三层,最终只是为了方便大发11选5大发11选5我 们 开发人员重写各个阶段的实现,也就更加地灵活可控。

2.2.4 默认的数据注解验证

ABP vNext 为了降低大发11选5大发11选5我 们 的学习成本,本身也是支持 ASP.NET Core MVC 那一套数据注解校验。大发11选5你 可以在某个非控制器类型的参数上,使用 [Required] 等数据注解特性。

它的默认实现大发11选5我 就不再多加赘述,基本就是通过反射得到参数对象上面的所有 ValidationAttribute 特性,显式地调用 GetValidationResult() 大发11选5方法 ,获取到具体的错误信息,然后添加到上下文结果当中。

foreach (var attribute in validationAttributes)
{
    var result = attribute.GetValidationResult(property.GetValue(validatingObject), validationContext);
    if (result != null)
    {
        errors.Add(result);
    }
}

另外注意,这个递归验证的深度是 8 级,在辅助类型的 MaxRecursiveParameterValidationDepth 常量中进行了定义。也就是说,大发11选5你 这个对象图的逻辑层级不能超过 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;}
}

如果大发11选5你 大发11选5方法 参数是 A1 类型的话,那么这就有 2 层了。

2.3 流畅验证库

回想上一节说的验证辅助类,还有一个基于 FluentValidation 库的类型,这里对于该库的使用大发11选5方法 参考单元测试即可。大发11选5我 这里只讲解一下,这个辅助类型是如何进行验证的。

public class FluentObjectValidationContributor : IObjectValidationContributor, ITransientDependency
{
    private readonly IServiceProvider _serviceProvider;

    public FluentObjectValidationContributor(
        IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void AddErrors(ObjectValidationContext context)
    {
        // 构造泛型类型,如果大发11选5你
对 Person 写了个验证器,那么验证器类型就是 IValidator<Person>。
        var serviceType = typeof(IValidator<>).MakeGenericType(context.ValidatingObject.GetType());
        // 通过 IoC 获得一个实例。
        var validator = _serviceProvider.GetService(serviceType) as IValidator;
        if (validator == null)
        {
            return;
        }

        // 调用验证器的大发11选5方法
进行验证。
        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 为大发11选5大发11选5我 们 提供了多种参数验证大发11选5方法 ,一般来说使用 MVC 过滤器配合数据注解就够了。如果大发11选5你 确实有一些特殊的需求,那也可以使用自己的方式对参数进行验证,只需要实现 IObjectValidationContributor 接口就行。

需要看其他的 ABP vNext 相关文章?点击大发11选5我 即可跳转到总目录。

posted @ 2019-10-23 09:45 myzony 阅读(...) 评论(...) 编辑 收藏
Flag Counter