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 :
    1. creation of a proxy via an interface link (in this case, composition will be used)
    2. 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:
    1. Not intuitive
    2. Intersection with the infrastructure code of other frameworks
    3. IOC container dependency
    4. 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:
    1. 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
    2. 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 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_GetRandomNumber 11.0409 ns0.1306 ns
    Foo_InterfaceProxyGetRandomNumber 51.6061 ns0.2764 ns
    FooClassProxy_GetRandomNumber 9.0125 ns0.1766 ns
    BarClassProxy_GetRandomNumber 44.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
    Writeline 859.0076 ns34.1630 ns
    I think if you replace Reflection with cached LambdaExpression, you can achieve that there will be no difference in performance at all, but for this you need to rewrite DynamicProxy, add support to popular containers (now interceptors are precisely supported from the Autofac and Castle.Windsor box , I don’t know about the others) . I doubt that this will happen in the near future.

    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.

    Also popular now: