[Abp vNext 源碼分析] - 9. 接口參數的驗證

1、簡要說明

ABP vNext 針對接口參數的校驗工做,分別由過濾器和攔截器兩步完成。過濾器內部使用的 ASP.NET Core MVC 所提供的 IModelStateValidator 進行處理,而攔截器使用的是 ABP vNext 本身提供的一套 IObjectValidator 進行校驗工做。html

關於參數驗證相關的代碼,分佈在如下三個項目當中:api

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

經過 MVC 的過濾器和 ABP vNext 提供的攔截器,咱們可以快速地對接口的參數、對象的屬性進行統一的驗證處理,而不會將這些代碼擴散到業務層當中。mvc

文章信息:框架

基於的 ABP vNext 版本:1.0.0async

創做日期:2019 年 10 月 22 日晚ide

更新日期:暫無源碼分析

2、源碼分析

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
{
    // ...
}

2.1.4 數據註解的驗證

這一節至關因而一個擴展知識,幫助咱們瞭解數據註解的工做機制,以及 ModelStateDictionary 是怎麼被填充的。

擴展閱讀:

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());
    }
}

3、總結

總的來講 ABP vNext 爲咱們提供了多種參數驗證方法,通常來講使用 MVC 過濾器配合數據註解就夠了。若是你確實有一些特殊的需求,那也可使用本身的方式對參數進行驗證,只須要實現 IObjectValidationContributor 接口就行。

須要看其餘的 ABP vNext 相關文章?點擊我 便可跳轉到總目錄。

相關文章
相關標籤/搜索