Reasonable AOP for fans of IOC containers
I really dislike the boilerplate. Such code is boring to write, depressingly maintain and modify. I don’t like it at all when the same bolierplate is mixed with the business logic of the application. The problem described very well krestjaninoff even 5 years ago . If you are not familiar with the AOP paradigm, read the material here, it covers the topic . Both at the time of reading this article, neither PostSharp nor Spring are happy with me now . But over the years, other tools have appeared in .NET that make it possible to extract the “left” code from business logic, make it separate reusable modules and describe it declaratively, without slipping into rewriting the resulting IL and other sodomy.
It will be about the projectCastle.DynamicProxy and its application in enterprise application development. I will borrow an example from krestjaninoff , because I see a similar code with enviable regularity, and it gives me a lot of trouble.
public BookDTO getBook(Integer bookId) throws ServiceException, AuthException {
if (!SecurityContext.getUser().hasRight("GetBook"))
throw new AuthException("Permission Denied");
LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;
String cacheKey = "getBook:" + bookId;
try {
if (cache.contains(cacheKey)) {
book = (BookDTO) cache.get(cacheKey);
} else {
book = bookDAO.readBook(bookId);
cache.put(cacheKey, book);
}
} catch(SQLException e) {
throw new ServiceException(e);
}
LOG.debug("Book info is: " + book.toString());
return book;
}
So, in the example above, one “useful” operation is reading a book from the database by Id. In the load, the method received:
- authorization check
- caching
- exception handling
- logging
In fairness, it is worth noting that authorization and access rights verification, caching could already be provided by ASP.NET using the [Authorize] and [OutputCache] attributes , however, this condition is a “spherical web service in a vacuum” (also written in Java ), therefore, the requirements for it are unknown, as, however, it is not known whether ASP.NET, WCF or the corporate framework is used.
Task

- move the helper code to a suitable place
- make it (code) reusable for other services
In the world of AOP, there is a special term for the problem we are solving: cross-cutting concerns . Stand base Concerns - the basic functionality of the system, for example, business logic, and cross-cutting Concerns - a secondary functionality (logging, checking permissions, error handling, etc.) necessary nonetheless throughout the application code.
Most often I meet and perfectly illustrate the situation of cross-cutting concern of this kind:
dbContext.InTransaction(x => {
//...
}, onFailure: e => {success: false, message: e.Message});
Everything is ugly in it, from increasing code nesting to shifting the functions of a system designer to an application programmer: there is no guarantee that transactions will be called wherever needed, it is not clear how to manage the transaction isolation level and nested transactions and this code will be copied a hundred thousand times where necessary and not necessary.
Decision
Castle.DynamicProxy provides a simple API for creating proxy objects on the fly with the ability to override what we are missing. This approach is used in popular isolation frameworks: Moq and Rhino Mocks . Two options are available to us :- creation of a proxy via an interface link (in this case, composition will be used)
- creating a proxy for the class (an heir will be created)
The main difference for us will be that in order to modify class methods, they must be declared accessible ( public or protected ) and virtual. The mechanism is similar to Lazy Loading in Nhibernate or EF . Castle.DynamicProxy uses Interceptors to enrich functionality . For example, to ensure that all application services are transactional, you can write an Interceptor like this:
public class TransactionScoper : IInterceptor
{
public void Intercept(IInvocation invocation)
{
using (var tr = new TransactionScope())
{
invocation.Proceed();
tr.Complete();
}
}
}
And create a proxy:
var generator = new ProxyGenerator();
var foo = new Foo();
var fooInterfaceProxyWithCallLogerInterceptor
= generator.CreateInterfaceProxyWithTarget(foo, TransactionScoper);
Or using a container :
var builder = new ContainerBuilder();
builder.Register(c => new TransactionScoper());
builder.RegisterType()
.As()
.InterceptedBy(typeof(TransactionScoper));
var container = builder.Build();
var willBeIntercepted = container.Resolve();
Similarly, you can add error handling
public class ErrorHandler : IInterceptor
{
public readonly TextWriter Output;
public ErrorHandler(TextWriter output)
{
Output = output;
}
public void Intercept(IInvocation invocation)
{
try
{
Output.WriteLine($"Method {0} enters in try/catch block", invoca-tion.Method.Name);
invocation.Proceed();
Output.WriteLine("End of try/catch block");
}
catch (Exception ex)
{
Output.WriteLine("Exception: " + ex.Message);
throw new ValidationException("Sorry, Unhandaled exception occured", ex);
}
}
}
public class ValidationException : Exception
{
public ValidationException(string message, Exception innerException)
:base(message, innerException)
{ }
}
Or logging:
public class CallLogger : IInterceptor
{
public readonly TextWriter Output;
public CallLogger(TextWriter output)
{
Output = output;
}
public void Intercept(IInvocation invocation)
{
Output.WriteLine("Calling method {0} with parameters {1}.",
invocation.Method.Name,
string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));
invocation.Proceed();
Output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
}
}
Caching and many other operations. A distinctive feature of this approach from the implementation of the “decorator” pattern by OOP tools is the ability to add auxiliary functionality to any types without the need to create heirs. The approach also solves the problem of multiple inheritance. We can safely add more than one interceptor to each type:
var fooInterfaceProxyWith2Interceptors
= generator.CreateInterfaceProxyWithTarget(Foo, CallLogger, ErrorHandler);
Another strong point of this approach is the separation of end-to-end functionality from the layer of business logic and the best separation of infrastructure code from the application domain.
If during the registration process it is impossible to say exactly which services need to be proxied and which ones not, then you can use attributes to get information in runtime (although this approach can lead to some problems):
public abstract class AttributeBased : IInterceptor
where T:Attribute
{
public void Intercept(IInvocation invocation)
{
var attrs = invocation.Method
.GetCustomAttributes(typeof(T), true)
.Cast()
.ToArray();
if (!attrs.Any())
{
invocation.Proceed();
}
else
{
Intercept(invocation, attrs);
}
}
protected abstract void Intercept(IInvocation invocation, params T[] attr);
}
You can even use a ready-made solution .
Minuses
I see four objective disadvantages of this approach:
- Not intuitive
- Intersection with the infrastructure code of other frameworks
- IOC container dependency
- Performance
Not intuitive
The easiest way to deal with such structuring of code is for people familiar with the concepts of functional programming. With a dirty amount of reservations, the approach can be called reminiscent of “ composition ”. Crookedly designed interceptors can cause a fair amount of not obvious bugs and performance issues.
Intersection with the infrastructure code of other frameworks
As I said at the beginning, the Authorize and OutputCache attributes are already in ASP.NET. In a sense, we are engaged in bicycle building. The approach is more suitable for teams for which abstracting from the final execution infrastructure is important. In addition, the approach works in the context of partial application, rather than “all or nothing”. Nobody forces us to re-implement authorization checks in the AOP-style, if this is not required.
IOC container dependency
For the service layer, the minus is practically absent if you practice IOC / DI. In 99% of cases, services will be received using an IOC container. Entity and Dto are usually created explicitly using the new operator or the mapper. I think that this is the correct state of things and I don’t see the use of interceptors at the level of Entity or Dto creation. I saw several examples of using interceptors to fill service fields in Entity , but over time this approach has always been abandoned. It is much better that the object itself takes care of the safety of its invariant .
Performance
I cited the three preceding paragraphs for accuracy rather than for pragmatic reasons. I rather attribute them to the limits of applicability of the approach, rather than to real problems. As for performance, I was not so sure, so I decided to make a series of benchmarks using BenchmarkDotNet . I didn’t have much with fantasy, so the time it took to get a random number was measured:
public class Foo : IFoo
{
private static readonly Random Rnd = new Random();
public double GetRandomNumber() => Rnd.Next();
}
public class Foo : IFoo
{
private static readonly Random Rnd = new Random();
public double GetRandomNumber() => Rnd.Next();
}
Benchmark sources and code examples are available on github . Obviously, magic with reflection and dynamic compilation comes at a price:
- Site Creation Time: ~ 2,000 ns. It doesn’t matter if the services are created once, but for the life time of the “fading” dependencies, such as the database context, another object is responsible
- Execution time: approximately ~ 1,000 extra nanoseconds inside Castle.DynamicProxy Reflection is used, with all the ensuing consequences.
In absolute terms, this is quite a lot, however, if the code is executed for longer than 50 ns, for example, a record in the database or a query over the network occurs, the situation looks different:
public class Bus : Bar
{
public override double GetRandomNumber()
{
Thread.Sleep(100);
return base.GetRandomNumber();
}
}
Host Process Environment Information: BenchmarkDotNet = v0.9.8.0 OS = Microsoft Windows NT 6.2.9200.0 Processor = Intel (R) Core (TM) i7-4710HQ CPU 2.50GHz, ProcessorCount = 8 Frequency = 2435775 ticks, Resolution = 410.5470 ns, Timer = TSC CLR = MS.NET 4.0.30319.42000, Arch = 64-bit RELEASE [RyuJIT] GC = Concurrent Workstation JitModules = clrjit-v4.6.1080.0
Type = InterceptorBenchmarks Mode = Throughput GarbageCollection = Concurrent Workstation LaunchCount = 1 WarmupCount = 3 TargetCount = 3
| Method | Median | Stddev |
|---|---|---|
| Createinstance | 0.0000 ns | 0.0000 ns |
| CreateClassProxy | 1,972.0032 ns | 8.5611 ns |
| CreateClassProxyWithTarget | 2,246.4208 ns | 5.3436 ns |
| CreateInterfaceProxyWithTarget | 2,063.6905 ns | 41.9450 ns |
| CreateInterfaceProxyWithoutTarget | 2,105.9238 ns | 4.9295 ns |
| Foo_GetRandomNumber | 11.0409 ns | 0.1306 ns |
| Foo_InterfaceProxyGetRandomNumber | 51.6061 ns | 0.2764 ns |
| FooClassProxy_GetRandomNumber | 9.0125 ns | 0.1766 ns |
| BarClassProxy_GetRandomNumber | 44.8110 ns | 0.4770 ns |
| FooInterfaceProxyWithCallLoggerInterceptor_GetRandomNumber | 1,756.8129 ns | 75.4694 ns |
| BarClassProxyWithCallLoggerInterceptor_GetRandomNumber | 1,714.5871 ns | 25.2403 ns |
| FooInterfaceProxyWith2Interceptors_GetRandomNumber | 2,636.1626 ns | 20.0195 ns |
| BarClassProxyWith2Interceptors_GetRandomNumber | 2,603.6707 ns | 4.6360 ns |
| Bus_GetRandomNumber | 100,471,410.5375 ns | 113,713.1684 ns |
| BusInterfaceProxyWith2Interceptors_GetRandomNumber | 100,539,356.0575 ns | 89,725.5474 ns |
| CallLogger_Intercept | 3,841.4488 ns | 26.3829 ns |
| Writeline | 859.0076 ns | 34.1630 ns |
Therefore, if on average your operations are performed for at least 100 ms and the three previous minuses do not scare you, the “container AOP” in C # is already production-ready.