Guide to implement cross-cutting concerns in C#

Cross cutting concerns (wiki) are common architectural elements such as logging, instrumentation, caching and transactions. In this article, we will explain how we integrate these into our projects. The functional classes in our project may use logging or caching within their method. Or they may not know that these other classes exist. We will cover both scenarios.

Decorator pattern

Decorator pattern is an elegant solution to implement cross-cutting concerns.

Consider the IDemo interface. We will add instrumentation concern to it.

interface IDemo
{
    void DoSomething();
}

class Demo : IDemo
{
    public void DoSomething()
    {
        Console.WriteLine("I am doing something");
    }
}

Add Instrumentation as a decorator. The decorator implements the same IDemo interface. It also has an IDemo interface as a private field. This interface has the actual implementation of IDemo. Whereas our decorator performs only instrumentation.

class InstrumentationDemo : IDemo
{
    private IDemo _demo;
    public InstrumentationDemo(IDemo demo)
    {
        _demo = demo;
    }

    public void DoSomething()
    {
        var watch = new Stopwatch();
        watch.Start();
        _demo.DoSomething();
        Console.WriteLine(watch.Elapsed.TotalMilliseconds.ToString());
    }
}

Wrap InstrumentationDemo over the Demo object as follows.

var demo = new Demo();
var instrumentationDemo = new InstrumentationDemo(demo);
instrumentationDemo.DoSomething();

There is a problem with using the decorator pattern  as shown above. For each interface, we write a decorator. This makes the codebase quite large. One way to solve the problem is shown below. Our decorator class implements multiple interfaces.

IDemo2:

interface IDemo2
{
    void DoWork();
}

class Demo2 : IDemo2
{
    public void DoWork()
    {
        Console.WriteLine("I am doing work");
    }
}

Decorator:

class InstrumentationDecorator : IDemo, IDemo2
{
    private IDemo _demo;
    private IDemo2 _demo2;
    public InstrumentationDecorator(IDemo demo)
    {
        _demo = demo;
    }

    public InstrumentationDecorator(IDemo2 demo2)
    {
        _demo2 = demo2;
    }

    public void DoSomething()
    {
        var watch = new Stopwatch();
        watch.Start();
        _demo.DoSomething();
        Console.WriteLine(watch.Elapsed.TotalMilliseconds.ToString());
    }

    public void DoWork()
    {
        var watch = new Stopwatch();
        watch.Start();
        _demo2.DoWork();
        Console.WriteLine(watch.Elapsed.TotalMilliseconds.ToString());
    }
}

There is still a problem. The decorator class becomes large with repetitive code.

Decorator as DynamicObject

Avoid writing repetitive code by emitting IL code to generate decorators. If the dependency injection framework has support for dynamic types, the following decorator should work for all interfaces.

public class InstrumentationDecorator<T> : DynamicObject
{
    private T _inner;

    public InstrumentationDecorator(T inner)
    {
        _inner = inner;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, 
                                        object[] args, 
                                        out object result)
    {
        result = null;
        string methodName = binder.Name;
        var argNames = binder.CallInfo.ArgumentNames;
        var methods = typeof(T).GetMethods();
        foreach (var method in methods)
        {
            if (method.Name.Equals(methodName))
            {
                var methodArgNames = method.GetParameters()
                                        .Select(pi => pi.Name)
                                        .ToArray();

                // Assume that there are no optional parameters!
                if (argNames.Count == methodArgNames.Length)
                {
                    bool skipMethod = false;
                    foreach (string argName in argNames)
                    {
                        if (!methodArgNames.Contains(argName))
                        {
                            skipMethod = true;
                            break;
                        }
                    }
                    if (skipMethod)
                        continue;

                    // Assume that the arguments are in the same order
                    Func<object> action = () =>
                    {
                        return method.Invoke(_inner, args);
                    };
                    result = DecoratedMethod(action);
                    return true;
                }
            }
        }

        return false;
    }

    private object DecoratedMethod(Func<object> action)
    {
        var watch = new Stopwatch();
        watch.Start();
        var result = action();
        watch.Stop();
        Console.WriteLine(watch.Elapsed.TotalMilliseconds.ToString());
        return result;
    }
}

This is how to use the decorator. Note the use of dynamic keyword.

var demo = new Demo();
dynamic instrumentationDemo = new InstrumentationDecorator<IDemo>(demo);
instrumentationDemo.DoSomething();

Caching

The dynamic decorator pattern explained above decorates every method in the interface. For some concerns like Caching, we should not decorate all methods. For example, consider the following interface:

interface IData<T,U>
{
    void Create(T obj);
    void Update(U id, T obj);
    void Delete(U id);
    IEnumerable<T> Get();
    T Get(U id);
}

For the above interface, cache the object retrieved from the Get method. Decorate only the Get method. Not Create, Update or Delete method. Aspect oriented programming or AOP has a better solution compared to the dynamic Decorator approach.

An example Caching decorator is shown below.

class CachingDecorator<T> : DynamicObject
{
    private object DecoratedMethod(Func<object> innerInvoke)
    {
        var result = CacheHelper.Get(key);
        if(result==null) 
        {
            result = innerInvoke();
            CacheHelper.Put(key, result);
        }
        return result;
    }
}

Not all interfaces require caching support. Only a few methods within each interface require caching support. We will look at a basic AOP framework for handling the caching problem.

Simple AOP framework

At the heart of AOP is the interceptor object. Intercept every method call that requires AOP support. AOP has some terminology:

  • PointCut – Method to intercept.
  • Advice – Type that implements the concern.
  • JoinPoint – Information about the interception.

Consider an interface, IService.

interface IService
{
    // Method whose return should be cached
    string GetData();
    void DoWork();
}

class Service : IService
{
    public string GetData()
    {
        Console.WriteLine("I am getting data from the actual method");
        return "Hello world";
    }

    public void DoWork()
    {
        Console.WriteLine("I am doing work");
    }
}

PointCut has the MethodInfo of the GetData method.

class PointCut : IEqualityComparer<PointCut>
{
    public PointCut() {}

    public PointCut(MethodInfo cut)
    {
        Cut = cut;
    }

    public MethodInfo Cut
    {
        get;
        set;
    }

    public bool Equals(PointCut x, PointCut y)
    {
        return x.Cut.ToString().Equals(y.Cut.ToString());
    }

    public int GetHashCode(PointCut obj)
    {
        return obj.Cut.ToString().GetHashCode();
    }

    public static IEqualityComparer<PointCut> EqualityComparer
    {
        get
        {
            var comparer = new PointCut();
            return comparer;
        }
    } 
}

PointCut implements the IEqualityComparer interface for storing it in a dictionary.

IAdvice interface implements the concern. It uses a JoinPoint object.

interface IAdvice
{
    object DoAdvice(JoinPoint joinPoint);
}

class JoinPoint
{
    public JoinPoint(object inner, MethodInfo invocation, object[] arguments)
    {
        InnerObject = inner;
        Invocation = invocation;
        Arguments = arguments;
    }

    public object InnerObject { get; set; }
    public MethodInfo Invocation { get; set; }
    public object[] Arguments { get; set; }
}

Any AOP framework has two additional classes:

  • Registry – Dictionary which has the mapping between PointCut and Advice
  • Interceptor – Dynamic object which intercepts the interface method call.

AOP Registry:

class AOPRegistry
{
    private Dictionary<PointCut, IAdvice> _registry = new Dictionary<PointCut, IAdvice>(PointCut.EqualityComparer);

    public void AddAspect(PointCut pointCut, IAdvice advice)
    {
        _registry.Add(pointCut, advice);
    }

    public IAdvice GetAdvice(PointCut pointCut)
    {
        if (_registry.ContainsKey(pointCut))
            return _registry[pointCut];
        return null;
    }
}

AOP Interceptor:

class AOPInterceptor<T> : DynamicObject
{
    private T _inner;
    private Dictionary<string, MethodInfo> _map = new Dictionary<string, MethodInfo>();
    private AOPRegistry _registry;

    public AOPInterceptor(AOPRegistry registry, T inner)
    {
        _registry = registry;
        _inner = inner;
        foreach (var methodInfo in typeof(T).GetMethods())
        {
            _map.Add(methodInfo.Name, methodInfo);
        }
    }
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        // Simplified retrieval of member info.
        var methodInfo = _map[binder.Name];
        if (methodInfo != null)
        {
            var pointCut = new PointCut(methodInfo);
            var advice = _registry.GetAdvice(pointCut);
            if (advice != null)
            {
                var joinPoint = new JoinPoint(_inner, methodInfo, args);
                result = advice.DoAdvice(joinPoint);
            }
            else
            {
                result = methodInfo.Invoke(_inner, args);
            }

            return true;
        }

        result = null;
        return false;
    }
}

AOP Interceptor checks whether the registry has an advice for the method call. If there is an advice, it calls the advice method.

To see how all of this works, we implement a CachingAdvice which intercepts the call to GetData method.

class CachingAdvice : IAdvice
{
    private ICache _cache;
    public CachingAdvice(ICache cache)
    {
        _cache = cache;
    }
    public object DoAdvice(JoinPoint joinPoint)
    {
        string key = joinPoint.InnerObject.GetType().Name;
        var result = _cache.Get(key);
        if (result == null)
        {
            result = joinPoint.Invocation.Invoke(joinPoint.InnerObject, joinPoint.Arguments);
            _cache.Put(key, result);
        }

        return result;
    }
}

Finally, here is the plumbing code that initializes the AOP framework and does the method call.

var methodInfo = typeof(IService).GetMethod("GetData");
var pointCut = new PointCut(methodInfo);
var cachingAdvice = new CachingAdvice(new Cache());
var registry = new AOPRegistry();
registry.AddAspect(pointCut, cachingAdvice);

var service = new Service();
dynamic interceptor = new AOPInterceptor<IService>(registry, service);
interceptor.DoWork();
string text = interceptor.GetData();

The above AOP framework is simplistic. If the number of PointCuts for a concern (advice) is quite small, then the AOP framework is useful. This is the case for caching.

Sometimes, the class should know about the concern. Use Dependency injection. Logging uses this approach.

Logging

Most methods log traces at the beginning of the method and at the end of the method. If an exception occurs, then the method logs the exception at the error level. If the method succeeds, then the method logs an informational message. The following code shows a typical method with logging code.

public void DoWork()
{
    logger.trace("Work started");
    try
    {
        repository.DoWork();
        logger.logMessage("Work succeeded");
    }
    catch(Exception e)
    {
        logger.logException(e);
        throw;
    }
    logger.trace("Work ended");
}

With PostSharp, an aspect oriented framework, the above method reduces to a single line of code.

[LoggingAspect]
public void DoWork()
{
    repository.DoWork();
}

Our dynamic decorator is also quite useful.

class LoggerDecorator : DynamicObject
{
    // standard dynamic stuff here.
    
    private void DecoratedMethod(Func<object> action)
    {
        logger.trace("processing started");
        try
        {
            action();
            logger.logMessage("processing is successful");
        }
        catch(Exception e)
        {
            logger.logException(e);
            throw;
        }
        logger.trace("processing ended");
    }
}

But sometimes, we log or trace messages even within methods. Not necessarily at the beginning and the end of methods. Consider the following method:

private object GetResults()
{
    var result1 = repository1.DoWork();
    var result2 = repository2.DoWork();
    
    logger.trace("Going to merge results1 and results2");
    return results1.Merge(results2);
}

We inject a logger using Dependency injection.

class StdService
{
    public ILogger Logger { get; set; }
    
    private void DoWork()
    {
        // Do some work
        Logger.Trace("Some message");
        // Do some more work
    }
}

Summary

There are three approaches to implementing cross-cutting concerns:

  1. Decorator pattern.
  2. Aspect oriented programming and Interceptor pattern.
  3. Dependency injection pattern.

To implement cross-cutting concerns, use a combination of all three approaches. The selected Dependency injection framework should have good support for dynamic types and interception.

Related Posts

Leave a Reply

Your email address will not be published.