之前在[料理佳餚] 使用 Decorator Pattern 分離參數檢查與資料處理這篇文章有提到我想要用 AOP 的方式來把參數的條件檢查分離出來,當時還沒有什麼好的做法,但是在遇到 FluentValidation 之後有了新的想法,只要搭配 Autofac 及 Autofac.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 >