欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-11-01 10:54:34
一、简要说明 ABP vNext 针对接口参数的校验工作,分别由过滤器和拦截器两步完成。过滤器内部使用的 ASP.NET Core MVC 所提供的 进行处理,而拦截器使用的是 ABP vNext 自己提供的一套 进行校验工作。 关于参数验证相关的代码,分布在以下三个项目当中: Volo.Abp.A ......

一、简要说明

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 容器当中了,以后我们每次的接口调用都会进入 abpvalidationactionfilteronactionexecutionasync() 方法内部。在这个过滤器的内部实现代码中,我们看到 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 请求是否符合条件,不符合则不进行参数验证,否则调用 imodelstatevalidatorvalidate 方法,将模型状态传递给它进行处理。

这个接口从名字上看,应该是模型状态验证器。因为我们接口上面的参数,在 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
{
    // ...
}

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

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

2.1.4 数据注解的验证

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

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

扩展阅读:

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 源码分析] - 9. 接口参数的验证

虽然 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 相关文章? 即可跳转到总目录。