What is aspect-oriented programming?
Aspect-Oriented Programming (AOP) extends Object-Oriented Programming with concepts and tools that focus on separating the implementation of repetitive code patterns from the main business logic.
Without AOP, repetitive code patterns are implemented manually in source code, resulting in additional (and tedious) work, excessive code complexity, and more defects.
Telemetry data shows that AOP can decrease the number of lines of code by 15%. Not only does it reduce development costs and defects, but it also makes the code cleaner and easier to read, greatly simplifying maintenance.
Aspect-oriented programming relies on three concepts:
Aspects are a mechanism to encapsulate patterns of repetitive code. Aspects know how to mechanically modify your source code to add new behaviors.
Aspects are like custom attributes that alter the way your code executes, e.g., by adding caching, exception handling, or immutability. Aspects work like templates that you can apply to hand-written code. They must be applied to a declaration—typically a method, property, or class.
Aspects are made of several primitive transformations named advice. For instance, overriding a method, adding an interface to a class, or intercepting field accesses are different kinds of advice.
In addition to advising the code, good aspect frameworks enable you to analyze source code and report errors and warnings.
Several targeting mechanisms to apply aspects to source code, including:
- Custom attributes,
- Code queries executed at compile time,
- Inheritance from base types to derived types.
A composition mechanism ensuring that several aspects (e.g., logging and caching) can be safely and predictably added to the same declaration.
The process of applying one or many aspects to source code is often called weaving.
Is AOP still useful in C#?
AOP was formalized in the early 2000s as a way to improve the modularity of classic object-oriented languages at a time when Java and C# were still in their infancy. In the meantime, both languages and development practices have evolved. The generalization of the use of higher-order functions (delegates, anonymous methods, lambdas) and dependency injection has greatly improved modularity, covering some of the initial use cases of aspect-oriented programming. Therefore, some developers question the usefulness of AOP in modern applications.
The academic roots of aspect-oriented programming did not help. Esoteric jargon like advice, pointcut, and joinpoint make AOP sound overly complex. Fortunately, .NET implementations, following PostSharp's lead, never followed this theoretical framework. After all, we don't even position Metalama as an aspect-oriented framework!
But despite the progress of programming languages and software engineering practices, despite the catastrophic communication around AspectJ, we still think that the gap identified by the early AOP researchers has not been completely addressed by classic languages.
I'm talking about the abstraction gap between human reasoning and programming languages.
Software architects think in terms of patterns and may say, "Add logging to all public methods of public classes in namespace X
, and implement INotifyPropertyChanged
for all classes derived from BaseEntity
." However, the C# language does not have a way to do "for each declaration X
, add behavior Y
."
Therefore, to save developers from implementing these requirements by hand, languages like C# must be extended to support this kind of meta-programming feature.
Whether we call these meta-programming features AOP or just code generation is eventually just a secondary question.
What are the typical uses of AOP in modern .NET?
In .NET, examples of code patterns that can be handled by AOP are:
- logging (AOP's "Hello, World" example), profiling,
- security,
- caching,
- transaction handling,
- observability (
INotifyPropertyChanged
), - change tracking,
- design patterns such as Builder, Memento, or Factory,
- multi-threading,
- equality,
ToString
,- WPF dependency properties and commands,
- storage (including object-database mapping and serialization),
- pulling dependencies.
Example
Logging is the "hello, world" example of aspect-oriented programming. Here is a logging aspect implemented with Metalama:
public class LogAttribute : OverrideMethodAspect
{
Console.WriteLine( $"{meta.Target.Method}: starting.")
try
{
return meta.Proceed();
}
finally
{
Console.WriteLine( $"{meta.Target.Method}: completed.")
}
}
One way to add the aspect to a method is simply to add a custom attribute:
[Log]
public void Greet()
{
Console.WriteLine("Hello, world.");
}
If we want to target select targets using a code query instead of hand-picking one, we can use a fabric:
internal class Fabric : ProjectFabric
{
public override void AmendProject( IProjectAmender amender )
{
amender
.SelectMany( c => c.Types )
.SelectMany( t => t.Methods )
.AddAspectIfEligible<LogAttribute>();
}
}
Here is the transformed code, i.e., the one that gets executed:
public void Greet()
{
Console.WriteLine( $"{meta.Target.Method}: starting.")
try
{
Console.WriteLine("Hello, world.");
}
finally
{
Console.WriteLine( $"{meta.Target.Method}: completed.")
}
}
Benefits of aspect-oriented programming
Benefit 1. Boosting development efficiency
Utilizing aspects in development significantly frees up time for more meaningful tasks. By employing Metalama, you can shift 10-50% of implementation workloads (depending on the project type) from your team to the compiler. This is possible because patterns typically rely on implementation guidelines, often involving algorithmic processes that machines excel at handling. Telemetry data shows that, depending on the type of project, companies typically save between 10% and 50% of source lines of code using Metalama. For instance, Clever Age, an IT consulting company, achieved a 24% code reduction when building an app for the French space agency.
With Metalama, you create an aspect once and save time while reducing source code each time the aspect is applied. Consequently, your codebase could shrink by 10-50% while maintaining the same functionality and enhanced reliability.
This approach allows developers to concentrate on inventive and value-adding tasks, increasing job satisfaction by eliminating the need to write repetitive low-level code. Metalama enables architects and senior developers to address overarching architectural challenges and streamline implementation, acting as talent multipliers within the team. New or junior team members can quickly ramp up and contribute more effectively, as they can focus on specific tasks without being burdened by low-level technical details.
Benefit 2. Enhancing application reliability
Utilizing AOP enhances application reliability in several ways.
First, less boilerplate code leads to fewer bugs. As best-selling author Steve McConnell points out, in a typical business application, one defect exists for every 40 lines of code. By reducing the source code, you not only decrease development costs but also the number of defects. Moreover, patterns implemented with aspects require testing only once, not every time they are applied to business code.
Second, AOP allows you to code at a higher level of abstraction, with most low-level details reliably handled by the compiler. This results in simpler source code that is easier to reason about, thereby reducing the number of defects. AOP enables faster failure detection, often as soon as build time. Consider multithreading, for example. Instead of making field-level decisions with locks or events, you can make architectural decisions to apply the [Synchronized] threading model to the base class, with the aspect taking care of the rest. Thanks to PostSharp IL, ATS Global could write thread-safe, machine-verified code and avoid random bugs typically found in multithreaded applications.
Lastly, AOP makes it affordable to prepare your app for production. You can add comprehensive logging to your entire codebase in minutes and implement features such as caching or exception handling without cluttering your business logic.
AOP is the ideal tool to enhance the robustness of your applications.
Benefit 3. Taming code complexity
Utilizing aspects effectively reduces code complexity. By clearly separating technical details from business logic, code becomes cleaner, simpler, and easier to read. This clarity allows new team members to quickly understand the code, diagnose problems, fix bugs, and add new functionalities.
“Sure, we achieved some considerable savings in terms of LOC. But more importantly, the new code is much less complex and much easier to maintain. This is what really saves time and money in the long run.” Daniel Wolf, Project Manager, mobileX AG
Benefit 4. Streamlining maintenance
Ultimately, the primary advantage of using AOP is reducing maintenance costs and extending the codebase's lifespan before a complete rewrite becomes necessary. Considering that maintenance accounts for 55% to 95% of a software system's total cost [10], this is crucial. Lower complexity enables team members who join the project post-release to be productive. Since developers spend 70% of their time understanding code [11][12], maintaining low complexity significantly impacts the maintenance team's productivity.
Features checklist
Code transformation (advising)
The main role of an aspect is to transform code, so the most critical factor when choosing an AOP framework is the kind of code transformation it supports. The more transformations supported, the more complex patterns you can automate.
Here are the advice kinds supported by Metalama:
- Overriding existing type members (including non-virtual, non-interface members)
- Introducing new members to an existing type
- Introducing new types
- Implementing new interfaces in existing types
- Appending parameters to constructors and pulling (required for dependency injection)
Design-time support
Back in the day, you had to build your project to refresh errors and warnings. IDEs have come a long way and now give real-time feedback to developers while they are typing.
If your aspect is introducing new members, types, or interfaces, it's often useful to be able to reference them in source code. If the new member is generated as a post-compilation step, this is not possible. Only Roslyn-based frameworks can bring the modern development experience to aspect-oriented programming.
Error reporting
A secondary but important role of aspects is to report warnings and errors. For instance, you might want to report an error if the developer tries to use your caching aspect on a void
method.
As a bonus, look for the ability to suggest a code fix when you report a warning.
Targeting mechanisms
How will you choose on which types, methods, or properties your aspects will apply?
Metalama supports the following mechanisms:
- Custom attributes: You hand-pick the target declarations and add aspects as custom attributes.
- Inheritance: Aspects applied to a base type or member automatically also apply to derived types and overriding members.
- Programmatically: Aspects can be added using a compile-time C# API that lets you query the code model using a familiar LINQ-style API.
Other frameworks (including PostSharp) may also support adding aspects using a compile-time XML file, but we found this approach less practical than the purely programmatic one when designing Metalama.
Debugging experience
Can you still meaningfully debug your code after you enhance it with aspects? Can you debug the generated code, or just the source code?
With Metalama, the default debugging experience is to debug the source code, i.e., you will step into the source code but step over the generated code. However, by switching to the LamaDebug
configuration, you will be able to step into every line of code generated by the aspect.
Testing
How can you test that your aspects are working as expected? Do they consistently generate the code you expect? Do they report expected errors and warnings upon invalid code?
Metalama provides a test framework that allows you to test that the generated code is as expected.
IDE tooling
The aim of aspect-oriented programming is to get the boilerplate out of sight and keep the source code clean.
However, to understand the code, it's often useful to know that an aspect affects your class or method, even if you don't want to be always bothered by how it is affected, i.e., you don't want to always see the generated code.
Metalama integrates with CodeLens, so you always get a hint when an aspect is applied to your code. If you want to see exactly how your code is affected, you can always open the diff comparing your source code with the generated code.
Alternatives
Today's aspect-oriented frameworks all rely on one of the following approaches:
MSIL Rewriting is the process of modifying the binary assembly during the build process. An additional step is added to this process just after the C# compiler. This approach was pioneered by PostSharp when the C# compiler was a black box. It is now considered obsolete, but many tools still rely on it.
Roslyn-based frameworks interact directly with the compiler without requiring an additional process. They are based on official Roslyn extension points such as analyzers and code generators, plus unofficial extension points added by Metalama.Compiler, an open-source fork of Roslyn, allowing arbitrary code transformations. Metalama is the only framework in this category at the moment. Roslyn-based frameworks can give you real-time feedback as you are typing, while MSIL-based ones require you to rebuild.
Middleware-based frameworks generally rely on a dependency injection framework and generate dynamic proxies at runtime. They are limited to intercepting interface methods and adding new interfaces to types.
Here is an overview of the main aspect-oriented frameworks available for .NET in 2024. This list is followed by a table comparing their features.
PostSharp
PostSharp, first launched in 2008, was the first complete implementation of AOP concepts in .NET. It is based on MSIL rewriting. PostSharp became a source of inspiration for several MSIL-based AOP frameworks.
PostSharp includes a broad set of ready-made aspects. It has complete documentation.
It comes with a Visual Studio extension that provides visibility into the transformations performed by aspects.
Metalama
Metalama, built by the same team as PostSharp and first launched in 2023, is PostSharp's successor. Based on Roslyn, Metalama works both at design time (within the IDE) and at compile time. It is today's most complete implementation of aspect-oriented principles.
Metalama uses a C#-to-C# template language coined T#. T# is 100% C#-compatible, so you can get full IntelliSense support with any IDE.
Since Metalama generates C# and not MSIL, you can preview and even debug the code generated by your aspects.
Metalama shares the same Visual Studio extension as PostSharp.
AspectInjector
Like PostSharp, AspectInjector is based on MSIL rewriting.
While far from PostSharp in terms of features, AspectInjector supports most code overriding and introduction features expected from an AOP framework.
Rougamo
Rougamo is another compile-time AOP framework based on MSIL rewriting. Its code transformation abilities are limited. It implements an AspectJ-inspired pointcut mechanism to select code to be modified.
AspectCore
AspectCore is an aspect-oriented framework based on dynamic proxies. This approach operates at runtime by generating a proxy type between the consumer and the implementation of an interface. It works only with components served by a dependency injection framework.
Fody
Fody is an extensible tool for weaving .NET assemblies. It is not an aspect framework in itself, but some plug-ins allow for PostSharp-style decoration of methods, allowing for the implementation of some simple aspects.
Fody has a long list of plug-ins that implement specific code transformations. Because these transformations must be coded directly in MSIL and not in C#, Fody does not fully qualify as an aspect-oriented framework.
Comparison
Metalama | PostSharp | AspectInjector | Rougamo | AspectCore | |
---|---|---|---|---|---|
Technology | Roslyn | MSIL | MSIL | MSIL | Dynamic Proxies |
Override virtual members | Yes | Yes | Yes | Yes | Yes |
Override non-virtual members | Yes | Yes | Yes | Yes | No |
Implement interfaces | Yes | Yes | Yes | No | Yes |
Introduce new members | Yes | Yes | Yes | No | No |
Reference introduced members from source code | Yes | No | No | No | No |
Allocationless context passing | Yes | No | Yes | No | No |
IDE: Aspect Explorer | Yes | Yes | No | No | No |
IDE: CodeLens | Yes | Yes | No | No | No |
View/Debug Generated C# | Yes | No | No | No | No |
Large library of pre-built aspects | Yes | Yes | No | No | No |