Skip to main content

Aspect-Oriented Programming Fundamentals

Before diving into Metalama, it's essential to understand the problem it solves and the paradigm it belongs to: Aspect-Oriented Programming (AOP).


The Problem: Cross-Cutting Concerns

In any non-trivial application, certain behaviors need to appear across many parts of the codebase. These are called cross-cutting concerns because they "cut across" the normal modular boundaries of your code.

Common Cross-Cutting Concerns

ConcernExample
LoggingLog every method entry/exit with parameters
CachingCache the return value of expensive methods
AuthorizationCheck user permissions before method execution
ValidationValidate method parameters (not null, in range)
Exception HandlingRetry on transient failures, circuit breaker patterns
AuditingRecord who did what, when, for compliance
PerformanceMeasure execution time, throttle, debounce
ThreadingSynchronize access, dispatch to UI thread
Transaction ManagementWrap operations in database transactions

The Problem with Traditional OOP

Consider a simple service method that needs logging, authorization, caching, and timing:

// ❌ Without AOP — cross-cutting concerns dominate the business logic
public async Task<Recipe> GetRecipeAsync(int id)
{
// Authorization (4 lines)
var user = _currentUserService.GetCurrentUser();
if (user == null || !user.IsAuthenticated)
throw new UnauthorizedException("Must be authenticated");

// Logging (2 lines)
_logger.LogDebug("Entering GetRecipeAsync with id={Id}", id);

// Timing (1 line)
var stopwatch = Stopwatch.StartNew();

try
{
// Caching (6 lines)
var cacheKey = $"Recipe_{id}";
var cached = await _cacheService.TryGetAsync<Recipe>(cacheKey);
if (cached != null)
return cached;

// ✅ Actual business logic (1 line!)
var recipe = await _repository.GetByIdAsync(id);

// More caching (1 line)
await _cacheService.SetAsync(cacheKey, recipe, TimeSpan.FromMinutes(5));

// More logging (1 line)
_logger.LogDebug("Exiting GetRecipeAsync, returning {Recipe}", recipe);

return recipe;
}
catch (Exception ex)
{
// Exception logging (2 lines)
_logger.LogError(ex, "Error in GetRecipeAsync");
throw;
}
finally
{
// More timing (2 lines)
stopwatch.Stop();
_logger.LogDebug("GetRecipeAsync took {Elapsed}ms", stopwatch.ElapsedMilliseconds);
}
}

The problems with this approach:

  1. Code tangling: Business logic is buried under infrastructure code (1 line of business logic vs. 19 lines of infrastructure)
  2. Code scattering: The same logging/caching/authorization patterns are duplicated across every service method
  3. Maintenance nightmare: Changing the logging format requires modifying every method
  4. Violation of SRP: Each method handles multiple responsibilities
  5. Error-prone: Easy to forget to add logging or authorization to a new method

The AOP Solution

With AOP, the same method becomes:

// ✅ With AOP — clean, focused business logic
[Authorize]
[Log]
[Cache(AbsoluteExpirationSeconds = 300)]
[Timing]
public async Task<Recipe> GetRecipeAsync(int id)
{
return await _repository.GetByIdAsync(id);
}

The cross-cutting concerns are now declared as attributes, and the framework handles weaving them into the actual execution flow.


What is Aspect-Oriented Programming?

Aspect-Oriented Programming (AOP) is a programming paradigm that supplements Object-Oriented Programming (OOP) by providing a way to modularize cross-cutting concerns.

Key Idea

AOP allows you to define cross-cutting behaviors once, in a single place called an aspect, and then apply them declaratively to any number of targets in your codebase.

AOP vs. OOP

AOP is not a replacement for OOP. It works alongside OOP to address a specific weakness:

DimensionOOPAOP
Modularization unitClassAspect
Handles wellCore business logic, data modeling, encapsulationCross-cutting concerns that span multiple classes
DecompositionVertical (by domain entity/service)Horizontal (by concern, across all entities)
Code reuseInheritance, composition, interfacesAspects applied declaratively
When code runsExplicit method callsImplicitly woven by the framework

Think of it this way:

         ┌──────────┐  ┌──────────┐  ┌──────────┐
│ Service A │ │ Service B │ │ Service C │
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Logging (aspect)
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Authorization (aspect)
│ │ │ │ │ │
─────────┼──────────┼──┼──────────┼──┼──────────┼──── Caching (aspect)
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘

← OOP (vertical) →
← AOP (horizontal, cross-cutting) →

AOP Terminology

Understanding AOP requires learning a few key terms:

Aspect

An aspect is a modular unit that encapsulates a cross-cutting concern. It defines what to do and where to do it.

In Metalama, an aspect is a C# class that inherits from a base class like OverrideMethodAspect:

public class LogAttribute : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Console.WriteLine($"Entering {meta.Target.Method.Name}");
var result = meta.Proceed();
Console.WriteLine($"Exiting {meta.Target.Method.Name}");
return result;
}
}

Join Point

A join point is a well-defined point in the execution of a program where an aspect can be applied. Examples:

  • Method execution
  • Property access (get/set)
  • Field access
  • Constructor execution
  • Exception handling

In Metalama, join points are determined by the type of aspect base class you use.

Advice

Advice is the actual code that an aspect executes at a join point. It defines what happens when the aspect is triggered.

Types of advice in Metalama:

Advice TypeDescriptionExample
BeforeCode that runs before the originalAuthorization check
AfterCode that runs after the originalLogging the return value
AroundCode that wraps the original (before + after)Retry, timing, caching
IntroductionNew members added to the target typeAdding INotifyPropertyChanged

Pointcut

A pointcut defines where (which join points) an aspect should be applied. In Metalama, pointcuts are defined by:

  • Attributes: Apply [Log] to specific methods
  • Fabrics: Programmatically select targets using LINQ-like queries
  • Eligibility: Define which declarations an aspect can be applied to

Weaving

Weaving is the process of combining aspects with the original code. This is where AOP frameworks differ significantly:

Weaving StrategyWhenFramework ExamplesProsCons
Compile-timeDuring compilationMetalama, PostSharpNo runtime overhead, catches errors early, debuggableRequires build tooling
Runtime (proxy)At runtime via proxiesCastle DynamicProxy, DispatchProxySimple setup, no build changesRuntime overhead, limited to virtual/interface methods
Runtime (IL)At runtime via IL rewritingHarmony, MonoModCan modify any codeFragile, hard to debug, runtime overhead
Source GenerationDuring compilationC# Source GeneratorsPart of Roslyn, no external toolsCannot modify existing code, only add new code

Metalama uses compile-time weaving, which means your aspects are applied during the C# compilation process. The compiler outputs IL that already contains the aspect logic — there is no runtime overhead.


Compile-Time Weaving Explained

This is the most important concept to understand about Metalama. Here's what happens during compilation:

┌─────────────────┐     ┌────────────────────┐     ┌──────────────────┐
│ Your Source │ │ Metalama │ │ Final IL │
│ │────▶│ (Roslyn Plugin) │────▶│ (Assembly) │
│ [Log] │ │ │ │ │
│ void DoWork() │ │ 1. Parse aspects │ │ void DoWork() │
│ { │ │ 2. Find targets │ │ { │
│ // logic │ │ 3. Generate code │ │ Log("Enter"); │
│ } │ │ 4. Weave into IL │ │ // logic │
│ │ │ │ │ Log("Exit"); │
└─────────────────┘ └────────────────────┘ └──────────────────┘

Source Code Metalama Compiler Output Assembly
(what you write) (transforms during build) (what actually runs)

What "Compile-Time" Means in Practice

  1. Your source code stays clean — you only write attributes and business logic
  2. The compiler generates the infrastructure code — logging, caching, authorization, etc.
  3. No runtime reflection or proxies — the generated code is just normal C# method calls
  4. Errors are caught at build time — if an aspect is applied incorrectly, you get a compiler error
  5. You can inspect the generated code — look at files under obj/<Config>/<TFM>/metalama/

Visualizing the Transformation

Before compilation (your source code):

[Log]
public int Add(int a, int b)
{
return a + b;
}

After compilation (what actually runs, visible in obj/.../metalama/):

public int Add(int a, int b)
{
Console.WriteLine("Entering Add(a = {0}, b = {1})", a, b);
try
{
int result;
result = a + b;
Console.WriteLine("Exiting Add with result = {0}", result);
return result;
}
catch (Exception ex)
{
Console.WriteLine("Error in Add: {0}", ex.Message);
throw;
}
}

Why AOP Matters for the GST Framework

The GST framework uses AOP extensively because of the domains it serves:

1. FDA 21 CFR Part 11 Compliance

Pharmaceutical and medical device manufacturing requires complete audit trails. Every data change must be tracked with:

  • Who made the change
  • When it was made
  • What the before/after values were
  • Why (change reason)

Without AOP, every method that modifies data would need 10+ lines of audit code. With AOP:

[Part11Audit]
[AuditDataChange]
[RequireChangeReason]
public async Task UpdateRecipe(Recipe recipe) { ... }

2. Industrial Equipment Communication

SECS/GEM and Modbus protocols require robust error handling:

  • Retry on transient communication failures
  • Circuit breaker for persistent failures
  • Timeout management
  • Thread synchronization for serial ports

3. WPF Desktop Applications

Factory floor applications need:

  • INotifyPropertyChanged on hundreds of view model properties
  • UI thread dispatching from background threads
  • Property change tracking for undo/redo

4. Performance Monitoring

Real-time manufacturing systems need:

  • Execution time monitoring
  • Throttle/debounce for rapid sensor data
  • Cache for frequently accessed configurations

AOP allows the GST framework to provide all of these as simple, declarative attributes that any application can use without understanding the underlying complexity.


Summary

ConceptDefinition
AOPA paradigm for modularizing cross-cutting concerns
AspectA reusable module encapsulating a cross-cutting concern
Join PointA point in code where an aspect can intervene
AdviceThe code an aspect runs at a join point
PointcutThe rule that selects which join points to target
WeavingCombining aspects with original code
Compile-time weavingWeaving during compilation (Metalama's approach)

Next: Getting Started — Install Metalama and create your first aspect.