為了幫助大家更深刻地認識Dora.Interception,並更好地將它應用到你的項目中,我們通過如下幾個簡單的實例來演示幾個常見的AOP應用在Dora.Interception下的實現。對於下麵演示的實例,它們僅僅是具有指導性質的應用,所以我會儘可能地簡化,如果大家需要將相應的應用場景移植到具體的... ...
為了幫助大家更深刻地認識Dora.Interception,並更好地將它應用到你的項目中,我們通過如下幾個簡單的實例來演示幾個常見的AOP應用在Dora.Interception下的實現。對於下麵演示的實例,它們僅僅是具有指導性質的應用,所以我會儘可能地簡化,如果大家需要將相應的應用場景移植到具體的項目開發中,需要做更多的優化。源代碼從這裡下載。
目錄
一、對輸入參數的格式化
二、對參數的自動化驗證
三、對方法的返回值進行自動緩存
一、對輸入參數的格式化
我們有一些方法對輸入參數在格式上由一些要求,但是我們有不希望對原始傳入的參數做過多的限制,那麼我們可以通過AOP的方式對輸入參數進行格式化。以如下這段代碼為例,Demo的Invoke方法有一個字元串類型的參數input,我們希望該值總是以大寫的形式存儲下來,但是有希望原始的輸入不區分大小寫,於是我們按照如下的方式在參數上標註一個UpperCaseAttribute。這種類型的格式轉換是通過我們自定義的一個名為ArgumentConversionInterceptor的Interceptor實現的,標準在方法上的ConvertArgumentsAttribute就是它對應的InterceptorAttribute。在Main方法中,我們按照DI的形式創建Demo對象(實際上是Demo代理對象),並調用其Invoke方法,那麼以小寫格式傳入的參數將自動轉換成大寫形式。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var demo = new ServiceCollection() 6 .AddSingleton<Demo, Demo>() 7 .BuildInterceptableServiceProvider() 8 .GetRequiredService<Demo>(); 9 Debug.Assert(demo.Invoke("foobar") == "FOOBAR"); 10 } 11 } 12 public class Demo 13 { 14 [ConvertArguments] 15 public virtual string Invoke([UpperCase]string input) 16 { 17 return input; 18 } 19 }
接下來我們就利用Dora.Intercecption來實現這個應用場景。對應標註在參數input上的UpperCaseAttribute用於註冊一個對應的ArgumentConvertor,因為它的本質工作是進行參數的轉換,抽象的ArgumentConvertor通過如下這個介面來表示。IArgumentConvertor具有一個唯一的方式Convert來完成針對參數的轉化,該方法的輸入是一個ArgumentConveresionContext對象,通過這個上下文對象我們可以獲取代表當前參數的ParameterInfo對象和參數值。
1 public interface IArgumentConvertor 2 { 3 object Convert(ArgumentConveresionContext context); 4 } 5 6 public class ArgumentConveresionContext 7 { 8 public ParameterInfo ParameterInfo { get; } 9 public object Value { get; } 10 11 public ArgumentConveresionContext(ParameterInfo parameterInfo, object valule) 12 { 13 this.ParameterInfo = parameterInfo; 14 this.Value = valule; 15 } 16 }
就像Dora.Interception將Interceptor和Interceptor的提供刻意分開一樣,我們同樣將提供ArgumentConvertor的ArgumentConvertorProvider通過如下這個介面來表示。
1 public interface IArgumentConvertorProvider 2 { 3 IArgumentConvertor GetArgumentConvertor(); 4 }
簡單起見,我們讓UpperCaseAttribute同時實現IArgumentConvertor和IArgumentConvertorProvider介面。在實現的Convert方法中,我們將輸入的參數轉換成大寫形式,至於實現的另一個方法GetArgumentConvertor,只需要返回它自己就可以了。
1 [AttributeUsage(AttributeTargets.Parameter)] 2 public class UpperCaseAttribute : Attribute, IArgumentConvertor, IArgumentConvertorProvider 3 { 4 public object Convert(ArgumentConveresionContext context) 5 { 6 if (context.ParameterInfo.ParameterType == typeof(string)) 7 { 8 return context.Value?.ToString()?.ToUpper(); 9 } 10 return context.Value; 11 } 12 13 public IArgumentConvertor GetArgumentConvertor() 14 { 15 return this; 16 } 17 }
我們最後來看看真正完成參數轉換的Interceptor是如何實現的。如下麵的代碼所示,在ArgumentConversionInterceptor的InvokeAsync方法中,我們通過標識方法調用上下文的InvocationContext對象的TargetMethod屬性得到表示目標方法的MethodInfo對象,然後解析出標準在參數上的所有ArgumentConverterProvider。然後通過InvocationContext的Arguments屬性得到對應的參數值,並將參數值和對應的MethodInfo對象創建出ArgumentConveresionContext對象,後者最後傳入由ArgumentConverterProvider提供的ArgumentConvertor作相應的參數。被轉換後的參數被重新寫入由InvocationContext的Arguments屬性表示的參數列表中即可。
1 public class ArgumentConversionInterceptor 2 { 3 private InterceptDelegate _next; 4 5 public ArgumentConversionInterceptor(InterceptDelegate next) 6 { 7 _next = next; 8 } 9 10 public Task InvokeAsync(InvocationContext invocationContext) 11 { 12 var parameters = invocationContext.TargetMethod.GetParameters(); 13 for (int index = 0; index < invocationContext.Arguments.Length; index++) 14 { 15 var parameter = parameters[index]; 16 var converterProviders = parameter.GetCustomAttributes(false).OfType<IArgumentConvertorProvider>().ToArray(); 17 if (converterProviders.Length > 0) 18 { 19 var convertors = converterProviders.Select(it => it.GetArgumentConvertor()).ToArray(); 20 var value = invocationContext.Arguments[0]; 21 foreach (var convertor in convertors) 22 { 23 var context = new ArgumentConveresionContext(parameter, value); 24 value = convertor.Convert(context); 25 } 26 invocationContext.Arguments[index] = value; 27 } 28 } 29 return _next(invocationContext); 30 } 31 } 32 33 public class ConvertArgumentsAttribute : InterceptorAttribute 34 { 35 public override void Use(IInterceptorChainBuilder builder) 36 { 37 builder.Use<ArgumentConversionInterceptor>(this.Order); 38 } 39 }
二、對參數的自動化驗證
將相應的驗證規則應用到方法的參數上,進而實現自動化參數驗證是AOP的一個更加常見的應用場景。一如下的代碼片段為例,還是Demo的Invoke方法,我們在input參數上應用一個MaxLengthAttribute特性,這是微軟自身提供的一個用於限制字元串長度的ValidationAttribute。在這個例子中,我們將字元串長度限製為5個字元以下,並提供了一個驗證錯誤消息。針對對參數實施驗證的是標準在方法上的ValidateArgumentsAttribute提供的Interceptor。在Main方法中,我們按照DI的方式得到Demo對應的代理對象,並調用其Invoke方法。由於傳入的字元串(“Foobar”)的長度為6,所以驗證會失敗,後果就是會拋出一個ValidationException類型的異常,後者被進一步封裝成AggregateException異常。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var demo = new ServiceCollection() 6 .AddSingleton<Demo, Demo>() 7 .BuildInterceptableServiceProvider() 8 .GetRequiredService<Demo>(); 9 try 10 { 11 demo.Invoke("Foobar"); 12 Debug.Fail("期望的驗證異常沒有拋出"); 13 } 14 catch (AggregateException ex) 15 { 16 ValidationException validationException = (ValidationException)ex.InnerException; 17 Debug.Assert("字元串長度不能超過5" == validationException.Message); 18 } 19 } 20 } 21 public class Demo 22 { 23 [ValidateArguments] 24 public virtual string Invoke( 25 [MaxLength(5, ErrorMessage ="字元串長度不能超過5")] 26 string input) 27 { 28 return input; 29 } 30 }
那麼我們看看ValidateArgumentsAttribute和由它提供的Interceptor具有怎樣的實現。從下麵給出的代碼可以看出ValidationInterceptor的實現與上面這個ArgumentConversionInterceptor具有類似的實現,邏輯非常簡單,我就不作解釋的。在這裡我順便說說另一個問題:有一些框架會將Interceptor直接應用到參數上(比如WCF可以定義ParameterInspector來對參數進行檢驗),我覺得從設計上講是不妥的,因為AOP的本質是針對方法的攔截,所以Interceptor最終都只應該與方法進行映射,針對參數驗證、轉化以及其他基於參數的處理都應該是具體某個Interceptor自身的行為。換句話說,應用在參數上的規則是為具體某種類型的Interceptor服務的,這些規則應該由對應的Interceptor來解析,但是Interceptor自身不應該映射到參數上。
1 public class ValidationInterceptor 2 { 3 private InterceptDelegate _next; 4 5 public ValidationInterceptor(InterceptDelegate next) 6 { 7 _next = next; 8 } 9 10 public Task InvokeAsync(InvocationContext invocationContext) 11 { 12 var parameters = invocationContext.TargetMethod.GetParameters(); 13 for (int index = 0; index < invocationContext.Arguments.Length; index++) 14 { 15 var parameter = parameters[index]; 16 var attributes = parameter.GetCustomAttributes(false).OfType<ValidationAttribute>(); 17 foreach (var attribute in attributes) 18 { 19 var value = invocationContext.Arguments[index]; 20 var context = new ValidationContext(value); 21 attribute.Validate(value, context); 22 } 23 } 24 return _next(invocationContext); 25 } 26 } 27 28 public class ValidateArgumentsAttribute : InterceptorAttribute 29 { 30 public override void Use(IInterceptorChainBuilder builder) 31 { 32 builder.Use<ValidationInterceptor>(this.Order); 33 } 34 }
三、對方法的返回值進行自動緩存
有時候我們會定義這樣一些方法:方法自身會進行一些相對耗時的操作並返回最終的處理結果,並且方法的輸入決定方法的輸出。對於這些方法,為了避免耗時方法的頻繁執行,我們可以採用AOP的方式對方法的返回值進行自動緩存,我們照例先來看看最終的效果。如下麵的代碼片段所示,Demo類型具有一個GetCurrentTime返回當前時間,它具有一個參數用來指定返回時間的Kind(Local、UTC或者Unspecified)。該方法上標註了一個CaheReturnValueAttribute提供一個Interceptor來緩存方法的返回值。緩存是針對輸入參數進行的,也就是說,如果輸入參數一致,得到的執行結果就是相同的,Main方法的調試斷言證實了這一點。
class Program { static void Main(string[] args) { var demo = new ServiceCollection() .AddMemoryCache() .AddSingleton<Demo, Demo>() .BuildInterceptableServiceProvider() .GetRequiredService<Demo>(); var time1 = demo.GetCurrentTime(DateTimeKind.Local); Thread.Sleep(1000); Debug.Assert(time1 == demo.GetCurrentTime(DateTimeKind.Local)); var time2 = demo.GetCurrentTime(DateTimeKind.Utc); Debug.Assert(time1 != time2); Thread.Sleep(1000); Debug.Assert(time2 == demo.GetCurrentTime(DateTimeKind.Utc)); var time3 = demo.GetCurrentTime(DateTimeKind.Unspecified); Debug.Assert(time3 != time1); Debug.Assert(time3 != time2); Thread.Sleep(1000); Debug.Assert(time3 == demo.GetCurrentTime(DateTimeKind.Unspecified)); Console.Read(); } } public class Demo { [CacheReturnValue] public virtual DateTime GetCurrentTime(DateTimeKind dateTimeKind) { switch (dateTimeKind) { case DateTimeKind.Local: return DateTime.Now.ToLocalTime(); case DateTimeKind.Utc: return DateTime.UtcNow; default: return DateTime.Now; } } }
現在我們實現緩存的CacheInterceptor是如何定義的,不過在這之前我們先來看看作為緩存的Key的定義。緩存的Key是具有如下定義的CacheKey,它由兩部分組成,表示方法的MethodBase和調用方法傳入的參數。
public struct Cachekey { public MethodBase Method { get; } public object[] InputArguments { get; } public Cachekey(MethodBase method, object[] arguments) { this.Method = method; this.InputArguments = arguments; } public override bool Equals(object obj) { if (!(obj is Cachekey)) { return false; } Cachekey another = (Cachekey)obj; if (!this.Method.Equals(another.Method)) { return false; } for (int index = 0; index < this.InputArguments.Length; index++) { var argument1 = this.InputArguments[index]; var argument2 = another.InputArguments[index]; if (argument1 == null && argument2 == null) { continue; } if (argument1 == null || argument2 == null) { return false; } if (!argument2.Equals(argument2)) { return false; } } return true; } public override int GetHashCode() { int hashCode = this.Method.GetHashCode(); foreach (var argument in this.InputArguments) { hashCode = hashCode ^ argument.GetHashCode(); } return hashCode; } }
如下所示的是CacheInterceptor的定義,可以看出實現的邏輯非常簡單。CacheInterceptor採用以方法註入形式提供的IMemoryCache 來對方法調用的返回值進行緩存。在InvokeAsync方法中,我們根據當前執行上下文提供的代表當前方法的MethodBase和輸入參數創建作為緩存Key的CacheKey對象。如果根據這個Key能夠從緩存中提取相應的返回值,那麼它會直接將此值保存到執行上下文中,並且終止當前方法的調用。反之,如果返回值尚未被緩存,它會繼續後續的調用,併在調用結束之後將返回值存入緩存,以便後續調用時使用。
public class CacheInterceptor { private readonly InterceptDelegate _next; public CacheInterceptor(InterceptDelegate next) { _next = next; } public async Task InvokeAsync(InvocationContext context, IMemoryCache cache) { var key = new Cachekey(context.Method, context.Arguments); if (cache.TryGetValue(key, out object value)) { context.ReturnValue = value; } else { await _next(context); cache.Set(key, context.ReturnValue); } } }
我們標註在GetCurrent方法上的CacheReturnValueAttribute定義如下,它只需要在重寫的Use方法中按照標準的方式註冊上面這個CacheInterceptor即可。順便再說一下,將Interceptor和註冊它的Attribute進行分離還具有一個好處:我可以為Attribute指定一個不同的名稱,比如這個CacheReturnValueAttribute。
[AttributeUsage(AttributeTargets.Method)] public class CacheReturnValueAttribute : InterceptorAttribute { public override void Use(IInterceptorChainBuilder builder) { builder.Use<CacheInterceptor>(this.Order); } }
Dora.Interception, 為.NET Core度身打造的AOP框架 [1]:全新的版本
Dora.Interception, 為.NET Core度身打造的AOP框架 [2]:不一樣的Interceptor定義方式
Dora.Interception, 為.NET Core度身打造的AOP框架 [3]:Interceptor的註冊
Dora.Interception, 為.NET Core度身打造的AOP框架 [4]:演示幾個典型應用