跳到主要内容

進階主題

本章涵蓋 Metalama 進階功能,適合想建立精密 Aspect 的開發者。


使用 BuildAspect 的命令式 Advising

BuildAspect() 方法讓你完全以程式碼控制 Aspect 的行為。不只是覆寫 Template,你還可以:

  1. 內省目標宣告
  2. 條件式新增或略過轉換
  3. 引入新成員
  4. 實作介面
  5. 回報診斷(錯誤/警告)
  6. 透過 Tag 傳遞資料給 Template

基本模式

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() { /* ... */ }
}

透過 Tag 傳遞資料

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();
}

引入成員

Aspect 可以為目標型別新增成員。

宣告式引入

在 Aspect 成員上使用 [Introduce]

public class TimestampAspect : TypeAspect
{
[Introduce]
public DateTime CreatedAt { get; } = DateTime.UtcNow;

[Introduce]
public DateTime? ModifiedAt { get; set; }

[Introduce]
public void Touch()
{
ModifiedAt = DateTime.UtcNow;
}
}

程式化引入

使用 BuildAspect() 進行動態成員引入:

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;
}
}

覆寫策略

控制成員已存在時的行為:

[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)

實作介面

TypeAspect 可以讓目標型別實作介面:

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;
}
}

介面成員 Template

[InterfaceMember] 標記成員為介面成員的實作:

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();
}
}
}

適用性

適用性定義了 Aspect 可以合法套用在哪些宣告上。當 Aspect 被套用到不符合適用性的目標時,Metalama 會回報編譯錯誤。

定義適用性

覆寫 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() { /* ... */ }
}

自訂適用性條件

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"
);
}

適用性 vs. 診斷

功能適用性診斷
用途「此 Aspect 無法在此處運作」「此 Aspect 可以運作,但有問題」
效果阻止 Aspect 套用建置期間產生警告/錯誤
IDEAspect 不會出現在快速修正選單Aspect 可以套用,顯示警告
範例[Cache] 用在 void 方法上[Cache] 用在參數不可雜湊的方法上

診斷

從 Aspect 回報自訂警告與錯誤:

定義診斷

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.");
}

回報診斷

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 組合模式

Aspect 聚合

一個 Aspect 可以加入其他 Aspect:

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 繼承

Aspect 可以繼承自其他 Aspect:

public class DetailedLogAttribute : LogAttribute
{
public override dynamic? OverrideMethod()
{
Console.WriteLine($"[{DateTime.UtcNow:O}] Thread: {Thread.CurrentThread.ManagedThreadId}");
return base.OverrideMethod(); // Call base aspect template
}
}

條件式 Aspect 套用

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
}
}

以程式碼新增 Contract

你可以從 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);
}
}
}

建構函式初始化

將程式碼加入建構函式:

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}");
}
}

摘要

功能API使用情境
BuildAspect()命令式 Advising複雜的條件式轉換
meta.Tags資料傳遞編譯期計算資料傳給 Template
[Introduce]宣告式成員引入新增欄位、屬性、方法
IntroduceMethod()程式化引入動態成員產生
ImplementInterface()介面實作新增 IDisposable、INPC 等
BuildEligibility()適用性規則限制 Aspect 目標
Diagnostics.Report()自訂警告/錯誤引導使用者、強制規則
Outbound.AddAspect()Aspect 聚合組合多個 Aspect
AddContract()程式化 Contract動態驗證
AddInitializer()建構函式注入初始化程式碼

下一篇測試與除錯 — 如何測試與除錯 Aspect。