Skip to main content

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:

  1. Introspect the target declaration
  2. Conditionally add or skip transformations
  3. Introduce new members
  4. Implement interfaces
  5. Report diagnostics (errors/warnings)
  6. 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

FeatureEligibilityDiagnostics
Purpose"This aspect cannot work here""This aspect can work but there's an issue"
EffectPrevents aspect applicationWarning/error during build
IDEAspect not suggested in quick-fix menuAspect 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

FeatureAPIUse Case
BuildAspect()Imperative advisingComplex, conditional transformations
meta.TagsData passingCompile-time computed data to templates
[Introduce]Declarative member introductionAdd fields, properties, methods
IntroduceMethod()Programmatic introductionDynamic member generation
ImplementInterface()Interface implementationAdd IDisposable, INPC, etc.
BuildEligibility()Eligibility rulesRestrict aspect targets
Diagnostics.Report()Custom warnings/errorsGuide users, enforce rules
Outbound.AddAspect()Aspect aggregationCombine multiple aspects
AddContract()Programmatic contractsDynamic validation
AddInitializer()Constructor injectionInitialization code

Next: Testing & Debugging — How to test and debug aspects.