Castle Core is a library that provides some utilities but I’ll just talk about using one of them, the DynamicProxy. If the post on BenchmarkDotNet was in the race for golden shovel award, a post on Castle DynamicProxy is a sure winner, but I feel like it :)
Castle DynamicProxy is a a “lightweight runtime proxy generator”, that enables you to do a kind of aspect oriented programming, allowing for some code to be executed before or after a method is invoked on a proxied interface. It’s useful for some cases, and I’ll talk about two of such cases: caching and timing operation execution times. We could also do this using for example decorators, implementing the same interface of the target class. The problem with the decorator approach is that we need to implement all the operations one by one, but with the DynamicProxy approach we can apply the same logic to all operations as long as it’s sufficiently generic.
The sample code for this post is on GitHub. I also included some benchmarks with these samples, using the BenchmarkDotNet library that I talked about in a previous post.
Timing operations execution time
I’m starting with the simplest one (at least in my barebones sample implementation). I created a class TimingInterceptor
that implements IInterceptor
. This is the interface that should be implemented to be able to perform actions before and/or after (or even instead of) a method is invoked. It has only the Intercept
method that gets and IInvocation
instance as argument, which contains all the information about the invoked method.
The implementation of the interceptor in this case is fairly simple.
|
|
Ok, now to be honest, this is without considering async methods. Considering them we get something a bit more complex, you can check the complete implementation here.
Now to use the interceptor we must create a proxy and provide it with an interceptor instance.
|
|
Then to use it you just need to invoke the methods on the proxiedService
instance instead of the service
instance.
Caching
I’m not going into much detail about this one, because it has more logic related with caching than with the interception capabilities we’re talking about in this post. If you feel like it, please check out the full code on GitHub and hit me with feedback, as this turned out a real cannon to kill a fly :)
I’m not copying the whole code to the article, it has a bunch of components, so I’ll just paste a simplified version (no async support) of the main class CacheInterceptor
(in this case SimplifiedCacheInterceptor
) so you can get a general idea of the implementation.
|
|
If you take a look at the main method Intercept
, you’ll see the logic is fairly simple. I did however extract some logic to other classes, like the cache key generation (allowing for different strategies) and the operation’s cache configuration fetching (allowing the configuration to be stored in different locations, in my implementation, I used a custom attribute).
For the creation of the cache key I implemented two strategies: ConfigurationBasedCacheKeyCreationStrategy
and ReflectionBasedCacheKeyCreationStrategy
. The former is provided with the logic to create the keys on the constructor, whilst the latter uses information about the invocation to create the key. I’ll show you this second one to exemplify some of the information we have access when intercepting invocations, useful for cases like this.
|
|
As you can see I’m using a good amount of info from the invocation, like the generic arguments, the MethodInfo
for the target method, the concrete arguments and the type of the target class (the one that is being proxied to).
Performance
What about performance? Well a performance penalty can be expected, but it mostly comes down to the code we gotta create to make the thing generic, rather than caused by the DynamicProxy (although there is a more noticeable impact when we first instantiate the proxy).
Using BenchmarkDotNet I created some tests. You can see the results below.
TimingInterceptor
// * Summary *
Host Process Environment Information:
BenchmarkDotNet.Core=v0.9.9.0
OS=Windows
Processor=?, ProcessorCount=8
Frequency=2740595 ticks, Resolution=364.8843 ns, Timer=TSC
CLR=CORE, Arch=64-bit ? [RyuJIT]
GC=Concurrent Workstation
dotnet cli version: 1.0.0-preview2-003133
Type=TimingBenchmark Mode=Throughput
Method | Median | StdDev |
---------------------------- |-------------- |----------- |
DynamicProxy | 211.0648 ns | 1.0241 ns |
DynamicProxyAsync | 316.2422 ns | 4.2481 ns |
DynamicProxyWithResultAsync | 2,057.3374 ns | 28.1047 ns |
Decorator | 50.2034 ns | 0.6157 ns |
DecoratorAsync | 116.6465 ns | 1.4087 ns |
DecoratorWithResultAsync | 133.0129 ns | 1.9464 ns |
CacheInterceptor
// * Summary *
Host Process Environment Information:
BenchmarkDotNet.Core=v0.9.9.0
OS=Windows
Processor=?, ProcessorCount=8
Frequency=2740595 ticks, Resolution=364.8843 ns, Timer=TSC
CLR=CORE, Arch=64-bit ? [RyuJIT]
GC=Concurrent Workstation
dotnet cli version: 1.0.0-preview2-003133
Type=CacheBenchmark Mode=Throughput
Method | Median | StdDev |
----------------------------- |--------------- |-------------- |
ProxyWithGeneratedKeys | 42,423.5197 ns | 2,722.0279 ns |
ProxyWithConfiguredKeys | 26,318.9704 ns | 270.8096 ns |
ProxyWithGeneratedKeysAsync | 48,604.7269 ns | 787.1521 ns |
ProxyWithConfiguredKeysAsync | 32,578.6152 ns | 1,675.7112 ns |
Decorator | 407.3324 ns | 16.3274 ns |
DecoratorAsync | 524.9182 ns | 5.6404 ns |
The best benchmark result to get insight of the impact of using the DynamicProxy is the first one on the TimingInterceptor
results. It’s the one that has less logic on the interceptor. The others, as we should expect, as complexity rises so does the execution time. The best example of this is the CacheInterceptor
when it needs to create auto-magically the cache key, resorting to reflection multiple times.
At the end of the day it comes down to whether the speed decrease is acceptable or not for the specific context. In general I don’t think it’s slow (we’re on the microseconds order of magnitude), but it’s a fact, mainly in the case of the CacheInterceptor
, that it is noticeably slower than using a more straightforward approach.
Wrapping up
So we can see that in terms of code that can be reused, we’ve got a win. It does, depending on what we want to do with the proxy, come with a penalty in terms of performance. It will depend on the type of system we’re building if the impact is acceptable or not.
Any suggestions and/or improvements, don’t hesitate, shout about it!
Cyaz