Aspect-Oriented Programming in C#

What is aspect-oriented programming?

Aspect-Oriented Programming (AOP) extends Object-Oriented Programming with a paradigm that focuses 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 boring) work, excessive code complexity, and more defects.

Telemetry data show 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, implementing INotifyPropertyChanged for a class C involves the following advice:

    • Add INotifyPropertyChanged to the base list of C,
    • Add the PropertyChanged public event,
    • Add the OnPropertyChanged protected method, calling PropertyChanged, and
    • Override property setters to call OnPropertyChanged.

    Additionally, to advising the code, some 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 of dependency injection have greatly improved the 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 applications 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 show 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%-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.

How can AOP concepts be implemented in .NET?

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.

What are the most important features of AOP frameworks?

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.

Testing and 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.

With non-trivial aspects, the ability to unit test them can prove crucial. 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.

List of AOP Frameworks for .NET

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 run time 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.

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