Principles of Aspect Oriented Programming in C#
01 June 2015
Aspect Oriented Programming (AOP) has been around for a long time. It is a powerful concept that allows for the separation of “crosscutting” concerns that has always been widely misunderstood by developers and managers alike in my experience. This confusion has largely been due to the mismatch between AOP theory, terminology, and implementation. Recently, I have seen a renewed interest in it, and I hope this article can help to demystify some of the confusions.
Most developers have heard of the separation of concerns (e.g. separating business logic from view logic), but what are “crosscutting” concerns? Crosscutting concerns are universal concerns that remain the same across methods, classes, and applications. These concerns can possibly even span companies and industries. These concerns usually have patterns and practice standards written for them such as tracing, auditing, error handling, caching, and localization.
Consider the following (rather useless) method. The catch block in this method is capturing the value of the parameter “depth.” This information is not available in a standard stack-trace even though it can be extremely valuable when trying to diagnose a problem that occurred in production. This is why many mid-size and large organizations often require standard catch blocks on public methods of all classes of all applications across the organization. This is what makes error handling the definitive example of a crosscutting concern.
Imagine being required to implement the error handling above on every single method you write. Not only does it make it much harder read the code, but it leads to other inherent problems. What happens when the standards change, and you have already implemented the error handling on hundreds or thousands of methods? What happens when a developer blindly copies-and-pastes the catch block and forgets to add required information? What happens when you add a parameter to the method, but forget to add it to the logging? What happens when your catch block doesn’t properly check for null on a parameter, and it throws an exception that has nothing to do with the initial exception? This is what makes AOP so convenient and powerful. It can separate this crosscutting concern and isolate it in a single place.
Depending on the implementation of the framework that you choose, the terminology will vary widely. For example, Spring.Net uses traditional terms, while Castle Windsor takes a different approach and uses different terms. Whichever framework you prefer, it is still important to understand the traditional terms.
- Advice - Additional code that you want to apply to an existing object. This code should focus on crosscutting concerns (described above).
- Joinpoint - The point in the execution of the code where the advice should be applied/executed.
- Pointcut - A pointcut is a set of joinpoints, but the term is sometimes used instead of “joinpoint.”
- Aspect - The combination of the pointcut and the advice.
How it works
There are two types of AOP frameworks for .NET. These include frameworks that wrap your objects in a proxy such as Castle Windsor and Spring.Net, and IL rewriter such as PostSharp that actually inject code directly into your compile assemblies. This article focuses on proxy-style AOP frameworks even though some of the concepts are transferable.
In its rawest form (but not its typical implementation), a class is instantiated and passed to a proxy generator where it is wrapped in a proxy and is bundled with aspects.
Once a consumer has a proxy, all calls to the target class happen through the proxy. When calls are made, the proxy routes all of the calls through each of the aspects before they are passed to the target instance method.
Where to use it
AOP frameworks are often bundled with Dependency Injection (DI) frameworks. Although AOP and DI principles are completely separate concepts, there is a reason they are usually bundled together. They work extremely well in concert. This is because they operate on the same types of objects and DI frameworks can be an excellent place to wrap objects into proxies because DI frameworks are centralized and designed for various levels of configuration.
Not all objects are appropriate for AOP. The ones that are appropriate implement interfaces to abstract away their functionality. These are often objects that fall into design patterns such as repositories, controllers, builders, commands, and adaptors.
Where not to use it
Possibly excluding frameworks that work by rewriting compiled assemblies, AOP does not work well with Entities, Models, DTOs, and other Data Structures in C#. This is because of two reasons. The first is that there needs to be an abstraction that the framework can override. Secondly, proxies tend to be a problem when it comes to both object relational mappers (ORMs) and with serialization.
Data objects and abstractions
Forgetting about AOP for the moment, using interfaces on your entities, models, and DTOs is something you simply do not want to consider. Data structures expose data, but have no meaningful functions. Therefore, they have nothing to abstract away. From a design standpoint, this means there is no good reason to add a model specific interface to each of your data structures. It adds double the work, and complicates your design. This also exponentially complicates child data structure objects.
Not only is the use of interfaces generally a horrible idea on data structures, it also can lead to AOP confusion. Since developers think of these structures as classes and not interfaces, inevitably there will be times when a new property is added to the class, but not the interface. When this happens, the aspects will fire on all of the properties that are defined on the interface. But, the aspects will not fire on the properties that failed to be added to the interface.
Data objects, proxies, and serialization
Dealing with proxies is normally not a problem when not dealing with data objects. After all, they are intended for classes that have already been abstracted away. However, with DTOs and during serialization this can lead to exceptions and/or unexpected behavior. Also, objects that have been deserialized will not have the aspects they had before being serialized.
One of the biggest confusions with AOP is the idea of “decorating” your classes with aspects. This confusion comes from many examples of “decorating” classes with attributes. The term decorate in this case actually refers to the Decorator Pattern and not the act of adding non-functional attributes as decoration to the class definition. In other words, the decorator is the proxy/wrapper.
Adding an attribute to a class doesn’t actually do anything on its own. When using Castle Windsor, adding an InterceptorAttribute to your class doesn’t automatically add an aspect to an instance of your class when using the “new” keyword. The attribute tells Castle Windsor’s DI framework that you would like to add an aspect (i.e. Interceptor). If you are using another DI framework such as TinyIoC, it will be ignored and your attribute will be ignored.
Although AOP is incredibly powerful and useful, it is not without its problems. However, with proper administration, these too can often be mitigated. They include the following:
Aspects not firing
One of the most confusing things for a developer with AOP is when aspects are not firing, but “should be.” This usually is a symptom of misunderstanding the meaning of “decorate” as described above.
Proxies can generally also be added directly around concrete classes that have no interface implementations. Usually, frameworks will fire aspects on virtual methods, but non-virtual methods will not fire methods. As a best practice, I would recommend not doing this.
Confusion while debugging
When debugging, instead of stepping into a method, you will instead step into the advice first. This can lead to confusion and frustration. To mitigate this, it may be sensible to configure your framework to only inject aspects in release mode.
Complicated stack traces
With AOP, stack traces will include all of the aspect code that was called with or without your knowledge. This can lead to extremely long stack traces. While an annoyance, this should not be an issue. If it does become an issue, there are a few things that you can do. First, consider examining your concept of “exceptional.” You may be missing opportunities to recover from situations rather than throwing an exception. Second, review problem stack traces for areas that can be simplified. Ask why there are so many layers. Ask whether or not a call/transaction is trying to do too much. Ask whether or not there have been too many aspects added to the system.
Bugs in the AOP code will have widespread effects
If an aspect wraps nearly all of the logic in an application and that aspect has a bug, it should be no surprise that this bug will have widespread effects. It is therefore highly recommended that AOP code be handled very professionally. It should be well unit tested and well understood before changes are made to it.
Performance issues in AOP code will have widespread effects
Again, it should be no surprise that code that wraps all of your application code at multiple levels will cause amplified performance issues if it is poorly written. Therefore, use aspects sparsely and effectively as possibly. Avoid adding heavy business logic into an aspect.
Fear of the unknown
Hopefully, this article can help to remove some of the fear of AOP that exists. But, as you can see, there are plenty of easily avoidable traps to fall into.
Aspect Oriented Programming is a powerful concept that can be used to meet the demands of your company (i.e. crosscutting concerns) while still allowing you to make your code readable and maintainable. While using AOP in in conjunction with a DI framework is not a necessity, it helps avoid many of the potential problems that will cause aspects not to fire. Always avoid using it with data structures.
is licensed under a Creative Commons Attribution 4.0 International License