📜 ⬆️ ⬇️

Reasonable AOP for IOC container fans

I really don't like boilerplate. Such code is boring to write, sadly accompany and modify. I don’t like it at all when that bolierplate is mixed with the business logic of the application. The problem was described very well by krestjaninoff 5 years ago . If you are not familiar with the AOP paradigm, read the material on the link, it reveals the topic .

As at the time of reading this article, even now neither PostSharp nor Spring suits me. But in the meantime, other tools appeared in .NET that allow you to pull out the “left” code from business logic, decorate it with separate reusable modules and describe it declaratively, without slipping into rewriting the resulting IL and other sodomy.

It's about the project Castle.DynamicProxy and its application in the development of enterprise applications. I will borrow an example from krestjaninoff , because I see the 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 method received:

For the sake of fairness, it is worth noting that the authorization and access rights check, caching could already be provided by ASP.NET using the [Authorize] and [OutputCache] attributes , however, by condition, this is a “spherical web service in vacuum” (also written in Java ), therefore, the requirements for it are unknown, as, however, it is not known whether ASP.NET, WCF or a corporate framework is used.

Task



In the AOP world there is a special term for the problem we are solving: cross-cutting concerns . Base concerns are highlighted - the main functionality of the system, for example, business logic and cross-cutting concerns - minor functionality (logging, access control, error handling, etc.), which is nevertheless necessary everywhere in the application code.

Most often I meet and perfectly illustrates the situation of cross-cutting concern of this type:
 dbContext.InTransaction(x => { //... }, onFailure: e => {success: false, message: e.Message}); 

Absolutely everything is ugly in it, ranging from increasing code nesting, ending with shifting the functions of the system designer to the application programmer: there is no guarantee that transactions will be invoked wherever needed, it is unclear how to manage the isolation level of transactions and nested transactions and this code will be copied one 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 define what we are missing. This approach is used in popular insulation frameworks: Moq and Rhino Mocks . We have two options available :
  1. creating a proxy for the interface link (in this case the composition will be used)
  2. creating a proxy for the class (a successor will be created)

The main difference for us will be that in order to modify the methods of the class, they must be declared accessible ( public or protected ) and virtual. The mechanism is similar to Lazy Loading in Nhibernate or EF . To enrich the functionality in Castle.DynamicProxy, Interceptors are used . For example, to ensure that all application services are transacted, 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<Foo>() .As<IFoo>() .InterceptedBy(typeof(TransactionScoper)); var container = builder.Build(); var willBeIntercepted = container.Resolve<IFoo>(); 

Similarly, error handling can be added.
  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 means of OOP is the ability to add supporting functionality to any types without the need to create heirs. The approach also solves the problem of multiple inheritance. We can easily add more than one interceptor for each type:
 var fooInterfaceProxyWith2Interceptors = generator.CreateInterfaceProxyWithTarget(Foo, CallLogger, ErrorHandler); 

Another strength of this approach is the separation of end-to-end functionality from the business logic layer and the best separation of the 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 not, then attributes can be used to obtain information at runtime (although this approach may lead to some problems):
  public abstract class AttributeBased<T> : IInterceptor where T:Attribute { public void Intercept(IInvocation invocation) { var attrs = invocation.Method .GetCustomAttributes(typeof(T), true) .Cast<T>() .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:
  1. Not intuitiveness
  2. Intersection with the infrastructure code of other frameworks
  3. Dependence on the IOC container
  4. Performance


Not intuitiveness


The easiest way to deal with this code structuring is for people familiar with functional programming concepts. With a dirty number of reservations, the approach can be called reminiscent of a “ composition ”. Crookedly designed interceptors can cause a fair amount of not obvious bugs and performance problems.

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 cycling. The approach is more suitable for commands for which it is important to abstract from the final execution infrastructure. In addition, the approach works in the context of partial application, not “all or nothing”. Nobody forces us to re-implement AOP-style authorization checks, if this is not required.

Dependence on the IOC container


For the service layer, minus is practically absent if you practice IOC / DI. In 99% of cases, services will be obtained using an IOC container. Entity and Dto are usually created explicitly using the new operator or the mapper. I think that this is the right state of affairs and I do not see the use of interceptors at the level of creating Entity or Dto. I have seen several examples of using interceptors to fill service fields in Entity , but over time this approach has always been discarded. It is much better that the object itself cares about the safety of its invariant .

Performance


I cited the previous three points for accuracy rather than pragmatic considerations. I rather attribute them to the limits of applicability of the approach, and not to the real problems. I was not so sure about the performance, so I decided to make a series of benchmarks using BenchmarkDotNet . With the fantasy, I didn’t have much, so the time of getting 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 samples are available on github . Obviously, you have to pay for magic with reflection and dynamic compilation:
  1. Object creation time: ~ 2,000 ns. It doesn't matter if the services are created once, and for the “live” dependencies, such as the context of the database, another object is responsible.
  2. Runtime operations: also approximately ~ 1,000 extra nanoseconds inside Castle.DynamicProxy use Reflection with all the ensuing consequences.

In absolute values, this is quite a lot, but if the code runs for longer than 50 ns, for example, it writes to the database or a request over the network, 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  
MethodMedianStddev
CreateInstance0.0000 ns0.0000 ns
CreateClassProxy1,972.0032 ns8.5611 ns
CreateClassProxyWithTarget2,246.4208 ns5.3436 ns
CreateInterfaceProxyWithTarget2,063.6905 ns41.9450 ns
CreateInterfaceProxyWithoutTarget2,105.9238 ns4.9295 ns
Foo_GetRandomNumber11.0409 ns0.1306 ns
Foo_InterfaceProxyGetRandomNumber51.6061 ns0.2764 ns
FooClassProxy_GetRandomNumber9.0125 ns0.1766 ns
BarClassProxy_GetRandomNumber44.8110 ns0.4770 ns
FooInterfaceProxyWithCallLoggerInterceptor_GetRandomNumber1,756.8129 ns75.4694 ns
BarClassProxyWithCallLoggerInterceptor_GetRandomNumber1,714.5871 ns25.2403 ns
FooInterfaceProxyWith2Interceptors_GetRandomNumber2,636.1626 ns20.0195 ns
BarClassProxyWith2Interceptors_GetRandomNumber2,603.6707 ns4.6360 ns
Bus_GetRandomNumber100,471,410.5375 ns113,713.1684 ns
BusInterfaceProxyWith2Interceptors_GetRandomNumber100,539,356.0575 ns89,725.5474 ns
CallLogger_Intercept3,841.4488 ns26.3829 ns
WriteLine859.0076 ns34.1630 ns
I think if you replace Reflection with LambdaExpression, you can ensure that there will be no difference in performance, but you need to rewrite DynamicProxy, add support to popular containers (now interceptors are supported exactly from Autofac and Castle.Windsor boxes , I don’t know about the others) . I doubt it will happen soon.

Therefore, if on average your operations are performed for at least 100 ms and the three previous minuses do not frighten you, the “container AOP” in C # is already production-ready.

Source: https://habr.com/ru/post/305360/


All Articles