天天看點

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

一、簡要說明

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 容器當中了,以後我們每次的接口調用都會進入

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
{
    // ...
}
           
[Abp vNext 源碼分析] - 9. 接口參數的驗證
[Abp vNext 源碼分析] - 9. 接口參數的驗證

2.1.4 資料注解的驗證

這一節相當于是一個擴充知識,幫助我們了解資料注解的工作機制,以及

ModelStateDictionary

是怎麼被填充的。

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

擴充閱讀:

  • 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 源碼分析] - 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 相關文章?點選我 即可跳轉到總目錄。