核心概念
本章說明 Metalama Aspect 的內部運作方式 — 包括其生命週期、排序、組合,以及編譯時期與執行時期之間的關係。
Aspect 的運作方式
Metalama 中的 Aspect 本質上是一個編譯時期程式碼轉換器。當編譯器在目標宣告上遇到 Aspect Attribute 時,它會:
- 在編譯時期實例化 Aspect 類別
- 呼叫
BuildAspect()(若使用命令式 API)以收集建議(advice) - 展開範本以產生轉換後的程式碼
- 將轉換後的程式碼合併到編譯輸出中
下圖展示一個 [Log] Aspect 從宣告 → Compiler 發現 → BuildAspect → Template 展開 → 產生 method body 的完整序列:
各步驟對應 Aspect API:
| 步驟 | Compiler 動作 | Aspect 端 hook |
|---|---|---|
| 1–2 | 掃描 attribute、實例化 Aspect | Aspect 的 properties 由 attribute 參數設定 |
| 3 | 詢問適用條件 | BuildEligibility(IEligibilityBuilder) |
| 4 | 收集 Advice | BuildAspect(IAspectBuilder) 內呼叫 Override / Introduce / ImplementInterface |
| 5 | 展開 Template | OverrideMethod() / [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 / after 於
meta.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;
}
}
組合規則
- Aspect 彼此互不知曉 — 每個 Aspect 只看到
meta.Proceed(),它會呼叫下一層 - 順序很重要 —
[Retry]在[Cache]外層,與[Cache]在[Retry]外層是不同的 - 同型別堆疊 — 你可以多次套用同一個 Aspect 型別(如果該 Aspect 支援的話)
- 無衝突 — 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()- 支援的回傳型別:
Task、Task<T>、ValueTask、ValueTask<T>、IAsyncEnumerable<T>
總結
| 概念 | 描述 |
|---|---|
| 兩種 API | 簡單式(範本覆寫)與程式化(BuildAspect()) |
| 編譯時期生命週期 | Aspect 在編譯期間被實例化、設定與展開 |
| 無執行時期實例 | Aspect 類別在執行時期不存在 |
| 管線模型 | 堆疊的 Aspect 圍繞 meta.Proceed() 形成多層包裹 |
| 排序 | 由 [AspectOrder] 或 Attribute 宣告順序控制 |
| 組合 | Aspect 是獨立且可組合的 |
| 非同步支援 | 自動處理或透過 OverrideAsyncMethod() 明確控制 |
下一篇:T# 範本語言 — 掌握編譯時期範本系統。