[料理佳餚] FluentValidation + Autofac.Extras.DynamicProxy2 實現參數條件檢查的 AOP 攔截器

  • 1003
  • 0
  • C#
  • 2016-07-05

之前在[料理佳餚] 使用 Decorator Pattern 分離參數檢查與資料處理這篇文章有提到我想要用 AOP 的方式來把參數的條件檢查分離出來,當時還沒有什麼好的做法,但是在遇到 FluentValidation 之後有了新的想法,只要搭配 AutofacAutofac.Extras.DynamicProxy2 就可以實現參數條件檢查的 AOP 攔截器。

首先先參考[料理佳餚] 讓 FluentValidation 把參數的檢查條件口語化這篇文章把驗證器準備好。

建立驗證器工廠

FluentValidation 提供了一個 IValidatorFactory 介面,我們建立一個 ValidatorFactory 類別來實作 IValidatorFactory,其中驗證器對應的部分我們依靠 DI Framework 來幫我們做,因此在之後依賴關係註冊的時候也一起把驗證器跟 IValidator<T> 的關係註冊進去。

class ValidatorFactory : IValidatorFactory
{
    public IValidator GetValidator(Type type)
    {
        Type validatorType = typeof(IValidator<>).MakeGenericType(type);

        var serviceTypes =
            AutoConfig.Container.ComponentRegistry.Registrations
                .SelectMany(r => r.Services.OfType<TypedService>())
                .Select(s => s.ServiceType);

        if (serviceTypes.Contains(validatorType))
        {
            return AutoConfig.Container.Resolve(validatorType) as IValidator;
        }
        else
        {
            throw new NullReferenceException();
        }
    }

    public IValidator<T> GetValidator<T>()
    {
        return (IValidator<T>)this.GetValidator(typeof(T));
    }
}

建立驗證攔截器

接著我們來建立攔截器,實作 IInterceptor 介面,並且宣告一個靜態唯讀的 ValidationFactory 來幫我們生產驗證器,當驗證通過時,就執行 invocation.Proceed() 做原來目標方法該做的事情,如果驗證不通過就回傳驗證錯誤訊息。

  • ValidateArguments(object[] arguments) 用來封裝參數的驗證方式,透過 ValidatorFactory.GetValidator() 驗證器工廠去取得每個參數所對應到的驗證器。
  • ServiceResult 是我自行設計的一個抽象類別,所有目標方法的回傳值都是繼承自這個抽象類別。
class ValidationInterceptor : IInterceptor
{
    private static readonly IValidatorFactory ValidatorFactory = new ValidatorFactory();

    public void Intercept(IInvocation invocation)
    {
        var validationResult = ValidateArguments(invocation.Arguments);

        if (validationResult.IsValid)
        {
            invocation.Proceed();
        }
        else
        {
            ServiceResult serviceResult = Activator.CreateInstance(invocation.Method.ReturnType) as ServiceResult;
            serviceResult.IsSuccess = false;
            serviceResult.Message = string.Join("\r\n", validationResult.Errors.Select(err => err.ErrorMessage));

            invocation.ReturnValue = serviceResult;
        }
    }

    private ValidationResult ValidateArguments(object[] arguments)
    {
        var validationResult = new ValidationResult();

        foreach (var arg in arguments)
        {
            var validator = ValidatorFactory.GetValidator(arg.GetType());

            validationResult =
                validator == null
                    ? new ValidationResult()
                    : validator.Validate(arg);

            if (validationResult.IsValid == false)
            {
                return validationResult;
            }
        }

        return validationResult;
    }
}

DI 註冊

最後 AOP 攔截器需要仰賴 DI 來建立被依賴介面的代理,以便可以在注入對象的方法前後插入攔截方法,所以我們把需要註冊的依賴關係設定好。

class AutoConfig
{
    public static IContainer Container { get; private set; }

    public static void Config()
    {
        var builder = new ContainerBuilder();

        builder
            .RegisterType<OrderValidator>()
            .As<IValidator<Order>>();

        builder
            .RegisterType<ValidationInterceptor>();

        builder
            .RegisterType<OrderService>()
            .As<IOrderService>()
            .EnableInterfaceInterceptors();

        Container = builder.Build();
    }
}

在有驗證需求的類別中掛上驗證攔截器

我們在有需要驗證參數的類別中,把驗證攔截器掛上去,基本上就大功告成了。

我們用 DI Framework 做了攔截器,因此在操作服務的時候就需要使用 DI Container 來解析並產生服務的實體。

解決美中不足

攔截器只能掛在類別上,無法掛在方法上,所以類別中有某些方法不需要做參數驗證的,我們不做些什麼的話還是會讓這些方法執行參數驗證,因此我自己建立了一個 IgnoreInterceptionAttribute 準備要用來解決這個問題。

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class IgnoreInterceptionAttribute : Attribute
{
    public IgnoreInterceptionAttribute(Type interceptorServiceType)
    {
        this.InterceptorServiceType = interceptorServiceType;
    }

    public Type InterceptorServiceType { get; }
}

接著我們要在 ValidationInterceptor 裡面加上這個方法。

private bool IsIgnoreValidation(MethodInfo methodInvocationTarget)
{
    return
        methodInvocationTarget
            .CustomAttributes
            .Where(attr => attr.AttributeType.Equals(typeof(IgnoreInterceptionAttribute)))
            .SelectMany(attr => attr.ConstructorArguments.Select(arg => arg.Value))
            .Any(type => type.Equals(this.GetType()));
}

再來我們改寫 Intercept(IInvocation invocation) 方法中的一小段。

最後把 IgnoreInterceptionAttribute 掛到不需要做參數驗證的方法上就行了,這樣不需要做參數驗證的方法,就不會去執行參數驗證的邏輯。

心得

做這樣的分離不是為了當下找自己的麻煩而是為了往後的維護,人的記憶力是有限的,無論是接手維護的人還是程式的作者本人,日子一久,看到程式碼的當下都需要重新做一番思考,有效地分離程式職責可以幫助在重新思考程式邏輯時,清楚地知道當下該段程式碼所要聚焦的職責是什麼,以利做更精準的判斷。

參考資料

 < Source Code >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學