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.

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.

Wrap InstrumentationDemo over the Demo object as follows.

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:

Decorator:

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.

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

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:

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.

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.

PointCut has the MethodInfo of the GetData method.

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

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

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:

AOP Interceptor:

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.

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

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.

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

Our dynamic decorator is also quite useful.

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

We inject a logger using Dependency injection.

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.

Guide to implement cross-cutting concerns in C#
Tagged on: