Inspecting aspects and interception in .NET, part 1
Recently I was tasked with adding instrumentation to a project: measuring how often methods were being called and how long each execution took. I had a couple of options.
- Timing it inside of the methods;
- Use an adapter;
- Use an aspect.
The adapter solution and why not
Inserting timing code directly into the methods would violate separation of concerns, so it was never an option. The adapter solution consists of defining an adapter class which wraps calls with timing code. Let’s say we have a Foo
class which implements IFoo
:
interface IFoo
{
void Bar();
}
class Foo: IFoo
{
public void Bar()
{
// do important things here
}
}
Our adapter, FooTimingAdapter
, would then look something like this:
class FooTimingAdapter: IFoo
{
private readonly IFoo _internalFoo;
public FooTimingAdapter(IFoo internalFoo)
{
_internalFoo = internalFoo;
}
public void Bar()
{
var stopwatch = Stopwatch.StartNew();
_internalFoo.Bar();
Debug.WriteLine($"IFoo.Bar took {stopwatch.Elapsed}");
}
}
This is nice; it separates the concern of instrumentation from the actual implementation of the class. The problem, of course, is that when you have a lot of classes to instrument, the collection of adapter classes becomes a burden to maintain.
When you change IFoo
, you not only have to change Foo
, but also FooTimingAdapter
. Doing TDD, as you should, this means you also have to write new, menial, tests for your adapter. This is starting to sound worse by the minute.
Aspects to the rescue
Instrumentation is a clear example of a cross-cutting concern: it is relevant to the program, but it is a different concern than, say, database access. It’s also generally highly reusable, which makes it a fine candidate for an aspect.
The difficulty with aspects is how to integrate them into your code; calling them directly kind of defeats the point. You want to use metadata like a marker interface or an attribute to indicate where an aspect should be invoked. .NET leaves you with a few options.
- Instructing an Intermediate Language (or IL) weaver like Fody where and how to insert calls to your aspect (which involves writing code that emits IL);
- Using an AOP framework like PostSharp, which uses IL-weaving under the hood but provides abstractions and interception points so you don’t have to write IL;
- Using a proxy generation framework like Castle DynamicProxy to intercept calls.
Interception
Interception is all done at run-time. A proxy object is created which routes calls through an interceptor. Unlike the other methods, it’s only possible to intercept interfaces or class methods that are abstract
or virtual
. If you only want to intercept some calls, you’ll have to do filtering yourself, at run-time.
Alternatives to Interception
IL-emitting code quickly becomes complicated and very hard to understand and maintain, and your aspects’ code becomes invisible when debugging your code. Performance is top notch, however, because the code executed is identical to just inserting the code yourself. You can apply aspects to all of your code, including private
and internal
methods. Only applying your aspect to some of your code is done compile-time, so it’s cost-free at run-time.
Tools like PostSharp tend to have a large impact on your build process and they often require (expensive) licenses for each developer that compiles your project. In a team of developers, that’s probably everybody. Performance is excellent and it is fairly easy to reason about what your code is doing when debugging. You can easily target individual methods, classes or entire namespaces and assemblies if you want, and this is again done compile-time.
Let’s do interception
Performance-wise and feature-wise, this is easily the worst option. Because it’s highly legible (relatively, of course) and has virtually no impact on your build process—very important when you’re doing TDD—I still consider it to be the best option.
Let’s take a look at a very naive interceptor to time a single method, using Castle DynamicProxy.
class TimingInterceptor: IInterceptor
{
public void Intercept(IInvocation invocation)
{
var methodToInstrument =
typeof(IFoo).GetMethod(nameof(IFoo.Bar));
// is this the method we want to instrument?
if (invocation.MethodInvocationTarget != methodToInstrument)
{
// no, so just call the original method
invocation.Proceed();
return;
}
// yes, time it!
var stopwatch = Stopwatch.StartNew();
// call the original method
invocation.Proceed();
Debug.WriteLine($"IFoo.Bar took {stopwatch.Elapsed}");
}
}
This is naive, for a couple of reasons. First of all, it only supports instrumenting a single method. We probably want an attribute to indicate which methods to instrument. Second of all, it does not handle failure of the intercepted method. It might not be important for this purpose, but exception handling is something you have to explicitly do in an interceptor.
Last of all, it uses reflection each time a method is called. A cache would reduce the impact of that, but there is an unavoidable cost from the interception framework which has to create an IInvocation
for each call, and it might do some reflection to do that each time a method is called.
Benchmarking time
What’s the overhead of all that reflection and whatever else the interception framework is doing? We could reason about it, but measuring is better. Benchmarks often yield surprising results (and it’s a lot of fun), so next time we’ll dive into benchmarking interception, and looking at how DI frameworks cope with this.