Advanced Topics
This chapter covers advanced Metalama features for developers who want to create sophisticated aspects.
Imperative Advising with BuildAspect
The BuildAspect() method gives you full programmatic control over what an aspect does. Instead of just overriding a template, you can:
- Introspect the target declaration
- Conditionally add or skip transformations
- Introduce new members
- Implement interfaces
- Report diagnostics (errors/warnings)
- Pass data to templates via tags
Basic Pattern
public class MyAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Introspect the target
var method = builder.Target;
// Conditionally apply
if (method.Parameters.Count > 0)
{
builder.Override(nameof(LoggedTemplate));
}
else
{
builder.Override(nameof(SimpleTemplate));
}
}
[Template]
public dynamic? LoggedTemplate() { /* ... */ }
[Template]
public dynamic? SimpleTemplate() { /* ... */ }
}
Passing Data via Tags
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Compute something at compile time
var sensitiveParams = builder.Target.Parameters
.Where(p => p.Attributes.Any(a => a.Type.Name == "SensitiveAttribute"))
.Select(p => p.Name)
.ToList();
// Pass to template
builder.Override(nameof(Template),
tags: new { SensitiveParams = sensitiveParams });
}
[Template]
public dynamic? Template()
{
var sensitive = (List<string>)meta.Tags["SensitiveParams"]!;
foreach (var param in meta.Target.Parameters)
{
if (sensitive.Contains(param.Name))
Console.WriteLine($" {param.Name} = [REDACTED]");
else
Console.WriteLine($" {param.Name} = {param.Value}");
}
return meta.Proceed();
}
Introducing Members
Aspects can add new members to the target type.
Declarative Introduction
Use [Introduce] on aspect members:
public class TimestampAspect : TypeAspect
{
[Introduce]
public DateTime CreatedAt { get; } = DateTime.UtcNow;
[Introduce]
public DateTime? ModifiedAt { get; set; }
[Introduce]
public void Touch()
{
ModifiedAt = DateTime.UtcNow;
}
}
Programmatic Introduction
Use BuildAspect() for dynamic member introduction:
public class CloneableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Introduce a Clone method
builder.IntroduceMethod(nameof(CloneTemplate),
buildMethod: m => m.Name = "Clone");
}
[Template]
public object CloneTemplate()
{
var clone = meta.Target.Type.Constructors
.First(c => c.Parameters.Count == 0)
.Invoke();
foreach (var prop in meta.Target.Type.Properties
.Where(p => p.Writeability == Writeability.All && !p.IsStatic))
{
prop.With(clone).Value = prop.With(meta.This).Value;
}
return clone;
}
}
Override Strategy
Control what happens when a member already exists:
[Introduce(WhenExists = OverrideStrategy.Override)] // Replace existing
[Introduce(WhenExists = OverrideStrategy.Ignore)] // Skip if exists
[Introduce(WhenExists = OverrideStrategy.New)] // Add with 'new' keyword
[Introduce(WhenExists = OverrideStrategy.Fail)] // Report error (default)
Implementing Interfaces
TypeAspect can make the target type implement an interface:
public class EquatableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Only implement if not already implemented
if (!builder.Target.ImplementedInterfaces.Any(
i => i.Name == "IEquatable"))
{
builder.ImplementInterface(typeof(IEquatable<>)
.MakeGenericType(builder.Target.ToType()));
}
}
[InterfaceMember]
public bool Equals(dynamic? other)
{
if (other == null || other.GetType() != meta.This.GetType())
return false;
foreach (var prop in meta.Target.Type.Properties
.Where(p => !p.IsStatic))
{
if (!Equals(prop.With(meta.This).Value, prop.With(other).Value))
return false;
}
return true;
}
}
Interface Member Template
[InterfaceMember] marks a member as implementing an interface member:
public class DisposableAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
builder.ImplementInterface(typeof(IDisposable));
}
[InterfaceMember]
public void Dispose()
{
// Auto-dispose all IDisposable fields
foreach (var field in meta.Target.Type.Fields
.Where(f => f.Type.Is(typeof(IDisposable)) && !f.IsStatic))
{
((IDisposable?)field.Value)?.Dispose();
}
}
}
Eligibility
Eligibility defines which declarations an aspect can legally be applied to. When an aspect is applied to an ineligible target, Metalama reports a compile error.
Defining Eligibility
Override BuildEligibility():
public class CacheAttribute : OverrideMethodAspect
{
public override void BuildEligibility(IEligibilityBuilder builder)
{
base.BuildEligibility(builder);
// Must not be void (nothing to cache)
builder.ReturnType().MustNotBe(typeof(void));
// Must not be static (instance-level cache)
builder.MustNotBeStatic();
// Must not be abstract
builder.MustNotBeAbstract();
}
public override dynamic? OverrideMethod() { /* ... */ }
}
Custom Eligibility Conditions
public override void BuildEligibility(IEligibilityBuilder builder)
{
builder.MustSatisfy(
method => method.Parameters.Count > 0,
method => $"{method} must have at least one parameter for cache key generation"
);
builder.MustSatisfy(
method => !method.ReturnType.Is(typeof(void)),
method => $"{method} must have a return value to cache"
);
}
Eligibility vs. Diagnostics
| Feature | Eligibility | Diagnostics |
|---|---|---|
| Purpose | "This aspect cannot work here" | "This aspect can work but there's an issue" |
| Effect | Prevents aspect application | Warning/error during build |
| IDE | Aspect not suggested in quick-fix menu | Aspect can be applied, warning shown |
| Example | [Cache] on void method | [Cache] on method with unhashable parameters |
Diagnostics
Report custom warnings and errors from aspects:
Defining Diagnostics
public class MyAspect : OverrideMethodAspect
{
// Define diagnostic descriptors
private static readonly DiagnosticDefinition<IMethod> _warning =
new("MY001", Severity.Warning,
"Method '{0}' has too many parameters ({1}). Consider refactoring.");
private static readonly DiagnosticDefinition<IParameter> _error =
new("MY002", Severity.Error,
"Parameter '{0}' of type '{1}' is not serializable.");
}
Reporting Diagnostics
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Report warning
if (builder.Target.Parameters.Count > 5)
{
builder.Diagnostics.Report(
_warning.WithArguments(builder.Target, builder.Target.Parameters.Count));
}
// Report error (prevents compilation)
foreach (var param in builder.Target.Parameters)
{
if (!IsSerializable(param.Type))
{
builder.Diagnostics.Report(
_error.WithArguments(param, param.Type));
}
}
// Suppress existing diagnostics
builder.Diagnostics.Suppress(
new SuppressionDefinition("CS0067")); // Suppress "event never used"
}
Aspect Composition Patterns
Aspect Aggregation
One aspect can add other aspects:
public class ServiceMethodAttribute : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// This single attribute adds multiple aspects
builder.Outbound.AddAspect<LogAttribute>();
builder.Outbound.AddAspect(new RetryAttribute { MaxAttempts = 3 });
builder.Outbound.AddAspect<TimingAttribute>();
}
}
// Usage: one attribute instead of three
[ServiceMethod]
public async Task ProcessOrderAsync(Order order) { ... }
Aspect Inheritance
Aspects can inherit from other aspects:
public class DetailedLogAttribute : LogAttribute
{
public override dynamic? OverrideMethod()
{
Console.WriteLine($"[{DateTime.UtcNow:O}] Thread: {Thread.CurrentThread.ManagedThreadId}");
return base.OverrideMethod(); // Call base aspect template
}
}
Conditional Aspect Application
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
// Only apply if method is in a specific namespace
if (builder.Target.DeclaringType.Namespace.StartsWith("MyApp.Services"))
{
builder.Override(nameof(Template));
}
else
{
builder.SkipAspect(); // Don't apply to this target
}
}
Adding Contracts Programmatically
You can add parameter validation from BuildAspect():
public class ValidateInputAspect : MethodAspect
{
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
foreach (var param in builder.Target.Parameters)
{
if (param.Type.IsReferenceType == true &&
param.Type.IsNullable != true)
{
// Add NotNull contract to all non-nullable reference parameters
builder.With(param).AddContract(
nameof(NotNullTemplate),
ContractDirection.Input);
}
}
}
[Template]
public void NotNullTemplate(dynamic? value)
{
if (value == null)
{
throw new ArgumentNullException(meta.Target.Parameter.Name);
}
}
}
Constructor Initialization
Add code to constructors:
public class InitializeFieldsAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Add initialization code to all constructors
builder.AddInitializer(nameof(InitTemplate),
InitializerKind.BeforeInstanceConstructor);
}
[Template]
public void InitTemplate()
{
Console.WriteLine($"Creating instance of {meta.Target.Type.Name}");
}
}
Summary
| Feature | API | Use Case |
|---|---|---|
BuildAspect() | Imperative advising | Complex, conditional transformations |
meta.Tags | Data passing | Compile-time computed data to templates |
[Introduce] | Declarative member introduction | Add fields, properties, methods |
IntroduceMethod() | Programmatic introduction | Dynamic member generation |
ImplementInterface() | Interface implementation | Add IDisposable, INPC, etc. |
BuildEligibility() | Eligibility rules | Restrict aspect targets |
Diagnostics.Report() | Custom warnings/errors | Guide users, enforce rules |
Outbound.AddAspect() | Aspect aggregation | Combine multiple aspects |
AddContract() | Programmatic contracts | Dynamic validation |
AddInitializer() | Constructor injection | Initialization code |
Next: Testing & Debugging — How to test and debug aspects.