为了帮助大家更深刻地认识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]:演示几个典型应用