メインコンテンツまでスキップ

核心概念

本章說明 Metalama Aspect 的內部運作方式 — 包括其生命週期、排序、組合,以及編譯時期與執行時期之間的關係。


Aspect 的運作方式

Metalama 中的 Aspect 本質上是一個編譯時期程式碼轉換器。當編譯器在目標宣告上遇到 Aspect Attribute 時,它會:

  1. 在編譯時期實例化 Aspect 類別
  2. 呼叫 BuildAspect()(若使用命令式 API)以收集建議(advice)
  3. 展開範本以產生轉換後的程式碼
  4. 將轉換後的程式碼合併到編譯輸出中

下圖展示一個 [Log] Aspect 從宣告 → Compiler 發現 → BuildAspect → Template 展開 → 產生 method body 的完整序列:

各步驟對應 Aspect API:

步驟Compiler 動作Aspect 端 hook
1–2掃描 attribute、實例化 AspectAspect 的 properties 由 attribute 參數設定
3詢問適用條件BuildEligibility(IEligibilityBuilder)
4收集 AdviceBuildAspect(IAspectBuilder) 內呼叫 Override / Introduce / ImplementInterface
5展開 TemplateOverrideMethod() / [Template] 屬性 / meta.* API
6寫回最終 method body無(純 Compiler 端動作)

兩種 API

Metalama 提供兩種方式來定義 Aspect 行為:

簡單 API(基於範本)

覆寫一個範本方法。範本即為完整行為:

public class RetryAttribute : OverrideMethodAspect
{
public int MaxAttempts { get; set; } = 3;

// This IS the template — it defines the transformed code
public override dynamic? OverrideMethod()
{
for (var i = 0; i < MaxAttempts; i++)
{
try
{
return meta.Proceed();
}
catch (Exception ex) when (i < MaxAttempts - 1)
{
Console.WriteLine($"Attempt {i + 1} failed: {ex.Message}");
}
}
throw new InvalidOperationException("Should not reach here");
}
}

適用時機:Aspect 僅有單一、直接的轉換。

程式化 API(基於 BuildAspect)

覆寫 BuildAspect() 以處理複雜場景:

public class NotifyPropertyChangedAttribute : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Step 1: Implement INotifyPropertyChanged
builder.ImplementInterface(typeof(INotifyPropertyChanged));

// Step 2: Introduce the event
builder.IntroduceEvent(nameof(PropertyChanged));

// Step 3: Override each property setter
foreach (var property in builder.Target.Properties
.Where(p => !p.IsStatic && p.Writeability == Writeability.All))
{
builder.With(property).Override(nameof(PropertyTemplate));
}
}

[Template]
public dynamic? PropertyTemplate
{
get => meta.Proceed();
set
{
if (!Equals(value, meta.Target.FieldOrProperty.Value))
{
meta.Proceed();
OnPropertyChanged(meta.Target.FieldOrProperty.Name);
}
}
}
}

適用時機:當你需要檢查目標、引入成員、實作介面,或有條件地套用建議(advice)時。


Aspect 生命週期

了解 Aspect 程式碼何時執行至關重要:

┌─────────────────────────────────────────────────┐
│ COMPILE TIME │
│ │
│ 1. Aspect class is instantiated │
│ 2. Aspect properties are set (from attribute) │
│ 3. BuildEligibility() is called │
│ 4. BuildAspect() is called │
│ 5. Templates are expanded into C# code │
│ 6. Generated code is compiled into IL │
│ │
│ ⚠ All aspect class members are evaluated │
│ at compile time, NOT runtime! │
│ │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ RUN TIME │
│ │
│ Only the EXPANDED template code runs. │
│ The aspect class itself does NOT exist. │
│ │
│ meta.Proceed() → original method body │
│ meta.Target.Method.Name → string literal │
│ foreach(param in meta.Target.Parameters) │
│ → unrolled individual lines │
│ │
└─────────────────────────────────────────────────┘

關鍵認知

Aspect 類別是一個編譯時期產物。它在執行時期永遠不會被實例化。它所包含的範本會被展開到目標方法中,最終組件中只存在展開後的程式碼。

這與執行時期 AOP 框架(如 Castle DynamicProxy)截然不同,在那些框架中,攔截器是實際存在於執行時期的物件。


Aspect 排序

當多個 Aspect 套用到同一個方法時,它們會形成一條管線。順序很重要:

[Authorize]    // Runs first (outermost)
[Log] // Runs second
[Cache] // Runs third
[Retry] // Runs fourth (innermost, closest to original method)
public async Task<Recipe> GetRecipeAsync(int id) { ... }

執行順序(管線模型)

多個 Aspect 形成「洋蔥式包裹」:宣告順序最上的 Aspect 在最外層,最內層的 Aspect 緊貼原始方法。每一層的 meta.Proceed() 都是「叫下一層」的橋接。

讀圖重點:

  • 實線箭頭:請求進入時的呼叫鏈(外層 → 內層 → 原始方法)
  • 虛線箭頭:原始方法 return 後的回傳鏈(內層 → 外層 → 呼叫端)
  • 每個 Aspect 都可在 before / aftermeta.Proceed() 兩側插入邏輯(例如 [Authorize] 在前置攔截、[Log] 在前後皆寫入、[Cache] 在前置可短路、[Retry]try/for 包裹 meta.Proceed())。
  • 短路的 Aspect(如 [Cache] 命中)會直接 return,後面所有內層 Aspect 與原始方法都不會執行

控制順序

在組件層級使用 [AspectOrder]

[assembly: AspectOrder(
AspectOrderDirection.RunTime,
typeof(AuthorizeAttribute),
typeof(LogAttribute),
typeof(CacheAttribute),
typeof(RetryAttribute)
)]
  • RunTime 方向表示清單中排第一的型別在執行時期最先執行(最外層)
  • CompileTime 方向則相反 — 排第一的型別最先編譯(最內層)

預設排序

若未明確指定排序,Aspect 會依照原始碼中出現的順序執行(Attribute 清單上從上到下)。


Aspect 組合

多個 Aspect 可以堆疊在同一個目標上。每個 Aspect 都是獨立且可組合的:

[Log]
[Retry(MaxAttempts = 3)]
[Timing(WarningThresholdMs = 1000)]
public async Task SendNotificationAsync(string userId, string message)
{
await _notificationService.SendAsync(userId, message);
}

堆疊的運作方式

每個 Aspect 透過 meta.Proceed() 包裹下一層。編譯後的結果等同於:

// Conceptual equivalent of stacked aspects
public async Task SendNotificationAsync(string userId, string message)
{
// [Log] layer
_logger.LogDebug("Entering SendNotificationAsync...");
try
{
// [Retry] layer
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
// [Timing] layer
var sw = Stopwatch.StartNew();
try
{
// Original method
await _notificationService.SendAsync(userId, message);
}
finally
{
sw.Stop();
if (sw.ElapsedMilliseconds > 1000)
_logger.LogWarning("Slow: {Elapsed}ms", sw.ElapsedMilliseconds);
}
break; // Success, exit retry loop
}
catch when (attempt < 2)
{
await Task.Delay(500 * (int)Math.Pow(2, attempt));
}
}
_logger.LogDebug("Exiting SendNotificationAsync");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in SendNotificationAsync");
throw;
}
}

組合規則

  1. Aspect 彼此互不知曉 — 每個 Aspect 只看到 meta.Proceed(),它會呼叫下一層
  2. 順序很重要[Retry][Cache] 外層,與 [Cache][Retry] 外層是不同的
  3. 同型別堆疊 — 你可以多次套用同一個 Aspect 型別(如果該 Aspect 支援的話)
  4. 無衝突 — Metalama 會自動處理織入(weaving)

編譯時期 vs. 執行時期:心智模型

這是 Metalama 開發中最重要的心智模型。Aspect 範本中的每段程式碼不是在編譯時期執行,就是在執行時期執行:

類別何時執行產出什麼
編譯時期建置期間C# 原始碼
執行時期應用程式執行時實際行為

範例

public override dynamic? OverrideMethod()
{
// COMPILE-TIME: meta.Target.Method.Name is resolved to a string literal
var name = meta.Target.Method.Name; // → "GetRecipeAsync"

// RUN-TIME: Console.WriteLine is generated into the output code
Console.WriteLine($"Entering {name}");

// COMPILE-TIME: This loop iterates over parameters during compilation
foreach (var p in meta.Target.Parameters)
{
// RUN-TIME: Each iteration generates a Console.WriteLine call
Console.WriteLine($" {p.Name} = {p.Value}");
}

// RUN-TIME: meta.Proceed() is replaced with the original method body
return meta.Proceed();
}

套用到 GetRecipeAsync(int id, string name) 時:

// Generated code (what actually runs)
public async Task<Recipe> GetRecipeAsync(int id, string name)
{
var name_1 = "GetRecipeAsync"; // Compile-time resolved
Console.WriteLine($"Entering {name_1}");
Console.WriteLine($" id = {id}"); // Unrolled from foreach
Console.WriteLine($" name = {name}"); // Unrolled from foreach
return await _repository.GetRecipeAsync(id, name); // meta.Proceed() expanded
}

非同步支援

Metalama 會自動處理非同步方法。你的 Aspect 範本可以為同步與非同步方法提供不同的處理方式:

public class TimingAttribute : OverrideMethodAspect
{
// Called for synchronous methods
public override dynamic? OverrideMethod()
{
var sw = Stopwatch.StartNew();
try
{
return meta.Proceed();
}
finally
{
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
}
}

// Called for async methods (Task, Task<T>, ValueTask, ValueTask<T>)
public override async Task<dynamic?> OverrideAsyncMethod()
{
var sw = Stopwatch.StartNew();
try
{
return await meta.ProceedAsync();
}
finally
{
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
}
}
}

重點

  • 如果你只覆寫 OverrideMethod(),Metalama 會自動正確包裝非同步方法
  • 覆寫 OverrideAsyncMethod() 以取得明確的非同步控制(例如,在 Aspect 本身中使用 await
  • meta.Proceed() 在非同步情境中會自動變成 await meta.ProceedAsync()
  • 支援的回傳型別:TaskTask<T>ValueTaskValueTask<T>IAsyncEnumerable<T>

總結

概念描述
兩種 API簡單式(範本覆寫)與程式化(BuildAspect()
編譯時期生命週期Aspect 在編譯期間被實例化、設定與展開
無執行時期實例Aspect 類別在執行時期不存在
管線模型堆疊的 Aspect 圍繞 meta.Proceed() 形成多層包裹
排序[AspectOrder] 或 Attribute 宣告順序控制
組合Aspect 是獨立且可組合的
非同步支援自動處理或透過 OverrideAsyncMethod() 明確控制

下一篇T# 範本語言 — 掌握編譯時期範本系統。