Skip to main content

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 ClassTargetKey OverrideUse Case
OverrideMethodAspectMethodsOverrideMethod()Wrap/intercept method execution
OverrideFieldOrPropertyAspectFields / PropertiesOverridePropertyIntercept property get/set
ContractAspectParameters / Fields / PropertiesValidate()Validate values (preconditions)
TypeAspectTypes (classes, structs)BuildAspect()Introduce members, implement interfaces
MethodAspectMethodsBuildAspect()Programmatic method transformation
FieldOrPropertyAspectFields / PropertiesBuildAspect()Programmatic field/property transformation
EventAspectEventsBuildAspect()Programmatic event transformation
ConstructorAspectConstructorsBuildAspect()Programmatic constructor transformation
ParameterAspectParametersBuildAspect()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/out fields: Fields used with ref or out cannot 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 partial when using TypeAspect that 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:

FeatureOverrideMethodAspectMethodAspect
SimplicitySimpler (just override one method)More verbose
Multiple templatesNoYes
Conditional adviceLimitedFull control in BuildAspect()
Introduce membersNoYes (via builder.IntroduceMethod())
Report diagnosticsNoYes (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 ClassSimplicityPowerBest 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.