Aspect Base Classes
Metalama provides several base classes, each designed for a specific type of code transformation. Choosing the right base class is the first step in creating an aspect.
Quick Reference
| Base Class | Target | Key Override | Use Case |
|---|---|---|---|
OverrideMethodAspect | Methods | OverrideMethod() | Wrap/intercept method execution |
OverrideFieldOrPropertyAspect | Fields / Properties | OverrideProperty | Intercept property get/set |
ContractAspect | Parameters / Fields / Properties | Validate() | Validate values (preconditions) |
TypeAspect | Types (classes, structs) | BuildAspect() | Introduce members, implement interfaces |
MethodAspect | Methods | BuildAspect() | Programmatic method transformation |
FieldOrPropertyAspect | Fields / Properties | BuildAspect() | Programmatic field/property transformation |
EventAspect | Events | BuildAspect() | Programmatic event transformation |
ConstructorAspect | Constructors | BuildAspect() | Programmatic constructor transformation |
ParameterAspect | Parameters | BuildAspect() | Programmatic parameter transformation |
OverrideMethodAspect
The most commonly used base class. It intercepts method execution by wrapping the original method body.
Basic Structure
public class MyMethodAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// Code before the original method
try
{
var result = meta.Proceed(); // Call original
// Code after the original method (success path)
return result;
}
catch (Exception ex)
{
// Code on exception
throw;
}
finally
{
// Code that always runs (cleanup)
}
}
}
Async Support
Override OverrideAsyncMethod() for explicit async handling:
public override async Task<dynamic?> OverrideAsyncMethod()
{
Console.WriteLine("Before async call");
try
{
var result = await meta.ProceedAsync();
Console.WriteLine("After async call");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"Async error: {ex.Message}");
throw;
}
}
If you don't override OverrideAsyncMethod(), Metalama wraps your OverrideMethod() template in a Task.Run pattern automatically.
Accessing Method Information
public override dynamic? OverrideMethod()
{
// Method metadata (all compile-time)
var className = meta.Target.Type.Name; // "OrderService"
var methodName = meta.Target.Method.Name; // "CalculateTotal"
var returnType = meta.Target.Method.ReturnType; // IType representing decimal
var isAsync = meta.Target.Method.IsAsync; // true/false
var isStatic = meta.Target.Method.IsStatic; // true/false
// Parameter iteration (compile-time unrolled)
foreach (var param in meta.Target.Parameters)
{
var paramName = param.Name; // Compile-time string
var paramValue = param.Value; // Run-time value of the parameter
Console.WriteLine($"{paramName} = {paramValue}");
}
return meta.Proceed();
}
Configuration via Properties
Aspect properties become attribute parameters:
public class RetryAttribute : OverrideMethodAspect
{
public int MaxAttempts { get; set; } = 3;
public int DelayMs { get; set; } = 500;
public bool UseExponentialBackoff { get; set; } = true;
public override dynamic? OverrideMethod() { /* use properties */ }
}
// Usage:
[Retry(MaxAttempts = 5, DelayMs = 1000, UseExponentialBackoff = false)]
public void SendEmail() { }
GST Example: LogAttribute
// From GST.Core.Aspects.Logging.LogAttribute
public class LogAttribute : OverrideMethodAspect
{
public bool LogParameters { get; set; } = true;
public bool LogReturnValue { get; set; } = true;
public bool LogExceptions { get; set; } = true;
public override dynamic? OverrideMethod()
{
var typeName = meta.Target.Type.Name;
var methodName = meta.Target.Method.Name;
AspectLogger.Debug(typeName, $"Entering {methodName}");
if (LogParameters)
{
foreach (var param in meta.Target.Parameters)
{
AspectLogger.Debug(typeName, $" {param.Name} = {param.Value}");
}
}
try
{
var result = meta.Proceed();
if (LogReturnValue && meta.Target.Method.ReturnType.SpecialType != SpecialType.Void)
{
AspectLogger.Debug(typeName, $"Exiting {methodName} with result: {result}");
}
return result;
}
catch (Exception ex)
{
if (LogExceptions)
{
AspectLogger.Error(typeName, $"Exception in {methodName}: {ex.Message}", ex);
}
throw;
}
}
public override async Task<dynamic?> OverrideAsyncMethod()
{
// Similar but with await meta.ProceedAsync()
// ...
}
}
OverrideFieldOrPropertyAspect
Intercepts field or property access. When applied to a field, Metalama automatically promotes it to a property with a backing field.
Basic Structure
public class MyPropertyAspect : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get
{
// Code before getting the value
var value = meta.Proceed(); // Get the actual value
// Code after getting the value
return value;
}
set
{
// Code before setting the value
meta.Proceed(); // Actually set the value
// Code after setting the value
}
}
}
Practical Example: String Trimming
public class TrimAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
// Trim string values before storing
meta.Target.FieldOrProperty.Value = value?.ToString()?.Trim();
}
}
}
// Usage:
public class UserProfile
{
[Trim]
public string FirstName { get; set; }
[Trim]
public string LastName { get; set; }
}
GST Example: TrackChanges
// From GST.Core.Aspects.Audit.TrackChangesAttribute
public class TrackChangesAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
var oldValue = meta.Target.FieldOrProperty.Value;
meta.Proceed(); // Set the new value
var newValue = meta.Target.FieldOrProperty.Value;
if (!Equals(oldValue, newValue))
{
var propertyName = meta.Target.FieldOrProperty.Name;
AspectLogger.Information(
meta.Target.Type.Name,
$"[CHANGE] {propertyName}: {oldValue} -> {newValue}");
}
}
}
}
Limitations
ref/outfields: Fields used withreforoutcannot be promoted to properties- Field initializers: Preserved during promotion
- Readonly fields: Can only override the getter
ContractAspect
Validates values applied to parameters, fields, or properties. Think of it as a precondition enforcer.
Basic Structure
public class MyContractAspect : ContractAspect
{
public override void Validate(dynamic? value)
{
if (/* value is invalid */)
{
throw new ArgumentException("Validation failed", meta.Target.Parameter.Name);
}
}
}
The value Parameter
The value parameter represents:
- For input parameters: The parameter value passed by the caller
- For output parameters/return values: The value being returned
- For properties/fields: The value being set
Practical Examples
// Not null validation
public class NotNullAttribute : ContractAspect
{
public override void Validate(dynamic? value)
{
if (value == null)
{
throw new ArgumentNullException(meta.Target.Parameter.Name);
}
}
}
// Range validation
public class RangeAttribute : ContractAspect
{
public double Min { get; set; } = double.MinValue;
public double Max { get; set; } = double.MaxValue;
public override void Validate(dynamic? value)
{
if ((double)value < Min || (double)value > Max)
{
throw new ArgumentOutOfRangeException(
meta.Target.Parameter.Name,
$"Value must be between {Min} and {Max}");
}
}
}
// Not empty validation
public class NotEmptyAttribute : ContractAspect
{
public override void Validate(dynamic? value)
{
if (value is string s && string.IsNullOrWhiteSpace(s))
{
throw new ArgumentException(
"Value cannot be empty or whitespace",
meta.Target.Parameter.Name);
}
}
}
Usage
public class RecipeService
{
public void CreateRecipe(
[NotNull][NotEmpty] string name,
[Range(Min = 0, Max = 100)] int temperature)
{
// Parameters are validated BEFORE this line executes
// ...
}
}
Contracts on Properties
public class Temperature
{
[Range(Min = -273.15, Max = 1000)]
public double Value { get; set; }
}
Contract Direction
Contracts can enforce preconditions (input) or postconditions (output):
// Precondition: validates input parameter
public void Process([NotNull] string input) { }
// Postcondition: validates return value
[return: NotNull]
public string GetName() { return _name; }
TypeAspect
Transforms entire types — introducing members, implementing interfaces, and overriding existing members programmatically.
Basic Structure
public class MyTypeAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Programmatically add transformations
builder.IntroduceMethod(nameof(MyMethodTemplate));
builder.ImplementInterface(typeof(IMyInterface));
// ...
}
[Template]
public void MyMethodTemplate()
{
// Template for the introduced method
}
}
Introducing Members
public class AddToStringAttribute : TypeAspect
{
[Introduce(WhenExists = OverrideStrategy.Override)]
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(meta.Target.Type.Name);
sb.Append(" { ");
foreach (var prop in meta.Target.Type.Properties.Where(p => !p.IsStatic))
{
sb.Append($"{prop.Name} = {prop.Value}, ");
}
sb.Append("}");
return sb.ToString();
}
}
Implementing Interfaces
public class DisposableAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
builder.ImplementInterface(typeof(IDisposable));
}
[InterfaceMember]
public void Dispose()
{
// Template for IDisposable.Dispose()
foreach (var field in meta.Target.Type.Fields
.Where(f => f.Type.Is(typeof(IDisposable))))
{
field.Value?.Dispose();
}
}
}
GST Example: NotifyPropertyChanged
// Simplified from GST.Core.Aspects.Observability.NotifyPropertyChangedAttribute
[AttributeUsage(AttributeTargets.Class)]
public class NotifyPropertyChangedAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// 1. Implement INotifyPropertyChanged
builder.ImplementInterface(typeof(INotifyPropertyChanged));
// 2. Override each auto-property's setter
foreach (var property in builder.Target.Properties
.Where(p => !p.IsStatic && p.Writeability == Writeability.All))
{
builder.With(property).Override(nameof(OverridePropertySetter));
}
}
[InterfaceMember]
public event PropertyChangedEventHandler? PropertyChanged;
[Introduce]
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(meta.This, new PropertyChangedEventArgs(propertyName));
}
[Template]
public dynamic? OverridePropertySetter
{
get => meta.Proceed();
set
{
if (!Equals(value, meta.Target.FieldOrProperty.Value))
{
meta.Proceed();
OnPropertyChanged(meta.Target.FieldOrProperty.Name);
}
}
}
}
Important: Target classes must be declared
partialwhen usingTypeAspectthat introduces members.
MethodAspect
Similar to OverrideMethodAspect but uses the programmatic API exclusively:
public class MyMethodAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
builder.Override(nameof(Template));
}
[Template]
public dynamic? Template()
{
Console.WriteLine($"Intercepted: {meta.Target.Method.Name}");
return meta.Proceed();
}
}
When to use MethodAspect vs OverrideMethodAspect:
| Feature | OverrideMethodAspect | MethodAspect |
|---|---|---|
| Simplicity | Simpler (just override one method) | More verbose |
| Multiple templates | No | Yes |
| Conditional advice | Limited | Full control in BuildAspect() |
| Introduce members | No | Yes (via builder.IntroduceMethod()) |
| Report diagnostics | No | Yes (via builder.Diagnostics) |
Choosing the Right Base Class
Use this decision tree:
What do you want to transform?
│
├── A method's behavior → Do you need BuildAspect()?
│ ├── No → OverrideMethodAspect ✅ (simplest)
│ └── Yes → MethodAspect
│
├── A property/field value → Do you need BuildAspect()?
│ ├── No → OverrideFieldOrPropertyAspect ✅
│ └── Yes → FieldOrPropertyAspect
│
├── Validate input/output values → ContractAspect ✅
│
├── Add members to a type → TypeAspect ✅
│
├── Implement an interface → TypeAspect ✅
│
└── Apply aspects to many targets programmatically → Fabric (see next chapter)
Summary
| Base Class | Simplicity | Power | Best For |
|---|---|---|---|
OverrideMethodAspect | ⭐⭐⭐ | ⭐⭐ | Logging, retry, timing, caching |
ContractAspect | ⭐⭐⭐ | ⭐ | Input validation |
OverrideFieldOrPropertyAspect | ⭐⭐⭐ | ⭐⭐ | Value transformation, change tracking |
TypeAspect | ⭐ | ⭐⭐⭐ | INotifyPropertyChanged, IDisposable, ToString |
MethodAspect | ⭐⭐ | ⭐⭐⭐ | Complex conditional transformations |
Next: Fabrics — Apply aspects in bulk without individual attributes.