1. 程式人生 > >[Abp vNext 原始碼分析] - 9. 介面引數的驗證

[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 容器當中了,以後我們每次的介面呼叫都會進入 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 是怎麼被填充的。

擴充套件閱讀:

  • 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 套了三層,最終只是為了方便我們開發人員重寫各個階段的實現,也就更加地靈活可控。

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