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
| Concern | Example |
|---|---|
| Logging | Log every method entry/exit with parameters |
| Caching | Cache the return value of expensive methods |
| Authorization | Check user permissions before method execution |
| Validation | Validate method parameters (not null, in range) |
| Exception Handling | Retry on transient failures, circuit breaker patterns |
| Auditing | Record who did what, when, for compliance |
| Performance | Measure execution time, throttle, debounce |
| Threading | Synchronize access, dispatch to UI thread |
| Transaction Management | Wrap 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:
- Code tangling: Business logic is buried under infrastructure code (1 line of business logic vs. 19 lines of infrastructure)
- Code scattering: The same logging/caching/authorization patterns are duplicated across every service method
- Maintenance nightmare: Changing the logging format requires modifying every method
- Violation of SRP: Each method handles multiple responsibilities
- 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:
| Dimension | OOP | AOP |
|---|---|---|
| Modularization unit | Class | Aspect |
| Handles well | Core business logic, data modeling, encapsulation | Cross-cutting concerns that span multiple classes |
| Decomposition | Vertical (by domain entity/service) | Horizontal (by concern, across all entities) |
| Code reuse | Inheritance, composition, interfaces | Aspects applied declaratively |
| When code runs | Explicit method calls | Implicitly 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 Type | Description | Example |
|---|---|---|
| Before | Code that runs before the original | Authorization check |
| After | Code that runs after the original | Logging the return value |
| Around | Code that wraps the original (before + after) | Retry, timing, caching |
| Introduction | New members added to the target type | Adding 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 Strategy | When | Framework Examples | Pros | Cons |
|---|---|---|---|---|
| Compile-time | During compilation | Metalama, PostSharp | No runtime overhead, catches errors early, debuggable | Requires build tooling |
| Runtime (proxy) | At runtime via proxies | Castle DynamicProxy, DispatchProxy | Simple setup, no build changes | Runtime overhead, limited to virtual/interface methods |
| Runtime (IL) | At runtime via IL rewriting | Harmony, MonoMod | Can modify any code | Fragile, hard to debug, runtime overhead |
| Source Generation | During compilation | C# Source Generators | Part of Roslyn, no external tools | Cannot 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
- Your source code stays clean — you only write attributes and business logic
- The compiler generates the infrastructure code — logging, caching, authorization, etc.
- No runtime reflection or proxies — the generated code is just normal C# method calls
- Errors are caught at build time — if an aspect is applied incorrectly, you get a compiler error
- 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:
INotifyPropertyChangedon 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
| Concept | Definition |
|---|---|
| AOP | A paradigm for modularizing cross-cutting concerns |
| Aspect | A reusable module encapsulating a cross-cutting concern |
| Join Point | A point in code where an aspect can intervene |
| Advice | The code an aspect runs at a join point |
| Pointcut | The rule that selects which join points to target |
| Weaving | Combining aspects with original code |
| Compile-time weaving | Weaving during compilation (Metalama's approach) |
Next: Getting Started — Install Metalama and create your first aspect.