最佳實務與常見陷阱
撰寫正確、可維護且高效能 Aspects 的指導方針,來自 Metalama 文件和 GST 框架的實務經驗。
設計原則
1. 單一職責
每個 Aspect 應處理一個橫切關注點:
// ✅ Good: Focused aspects
[Log]
[Retry(MaxAttempts = 3)]
[Cache(AbsoluteExpirationSeconds = 300)]
public async Task<Recipe> GetRecipeAsync(int id) { ... }
// ❌ Bad: Monolithic aspect doing everything
[SuperAspect] // Logs, retries, caches, validates, audits...
public async Task<Recipe> GetRecipeAsync(int id) { ... }
原因:專注的 Aspects 可重複使用、可測試且可組合。單體式 Aspect 會變得難以維護。
2. 防錯設計
Aspects 應永遠不會中斷應用程式,即使它們的相依項不可用:
// ✅ Good: Graceful degradation
public override dynamic? OverrideMethod()
{
var logger = AspectServiceLocator.GetLogger();
logger?.Debug(meta.Target.Type.Name, $"Entering {meta.Target.Method.Name}");
return meta.Proceed(); // Always call the original method
}
// ❌ Bad: Aspect failure breaks the application
public override dynamic? OverrideMethod()
{
var logger = AspectServiceLocator.GetLogger()!; // Throws if null!
logger.Debug(meta.Target.Type.Name, $"Entering {meta.Target.Method.Name}");
return meta.Proceed();
}
3. 最小化產生的程式碼
保持 Templates 精簡 — Template 程式碼的每一行都會被複製到每個目標方法中:
// ✅ Good: Delegate heavy logic to a helper method
public override dynamic? OverrideMethod()
{
AuditHelper.RecordEntry(meta.Target.Type.Name, meta.Target.Method.Name);
var result = meta.Proceed();
AuditHelper.RecordExit(meta.Target.Type.Name, meta.Target.Method.Name, result);
return result;
}
// ❌ Bad: Complex logic in the template (duplicated into every target)
public override dynamic? OverrideMethod()
{
var user = Thread.CurrentPrincipal?.Identity?.Name ?? "anonymous";
var timestamp = DateTime.UtcNow.ToString("o");
var parameters = new Dictionary<string, object>();
foreach (var p in meta.Target.Parameters)
{
try { parameters[p.Name] = JsonSerializer.Serialize(p.Value); }
catch { parameters[p.Name] = p.Value?.ToString() ?? "null"; }
}
var entry = new AuditEntry(user, timestamp, meta.Target.Method.Name, parameters);
// ... 20 more lines of audit logic
// All of this is duplicated into EVERY method with [Audit]
return meta.Proceed();
}
4. 定義資格條件
始終告訴使用者 Aspect 可以和不能套用在哪裡:
public class CacheAttribute : OverrideMethodAspect
{
public override void BuildEligibility(IEligibilityBuilder builder)
{
base.BuildEligibility(builder);
builder.ReturnType().MustNotBe(typeof(void)); // Can't cache void
builder.MustNotBeAbstract(); // Can't override abstract
}
}
5. 提供同步和非同步 Templates
始終處理同步和非同步方法:
public class MyAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// Synchronous path
return meta.Proceed();
}
public override async Task<dynamic?> OverrideAsyncMethod()
{
// Asynchronous path — use await
return await meta.ProceedAsync();
}
}
如果你只覆寫 OverrideMethod(),Metalama 會為非同步方法進行包裝 — 但明確處理非同步會更正確且更有效率。
常見陷阱
陷阱 1:對引入的成員使用 nameof()
// ❌ Wrong: nameof resolves at ASPECT compile time, not target compile time
[Introduce]
public event PropertyChangedEventHandler? PropertyChanged;
[Template]
public void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(meta.This,
new PropertyChangedEventArgs(nameof(PropertyChanged))); // Always "PropertyChanged"!
}
// ✅ Correct: Use string literals for introduced member names
PropertyChanged?.Invoke(meta.This,
new PropertyChangedEventArgs("PropertyChanged"));
陷阱 2:在原始碼檔案中設定中斷點
// ❌ Won't work: Breakpoint in your source code
[Log]
public void DoWork() // ← Breakpoint here won't hit (code is transformed)
{
// ...
}
// ✅ Works: Breakpoint in obj/.../metalama/MyClass.cs (transformed code)
// Or use meta.DebugBreak() in the template
陷阱 3:忘記在目標類別加上 partial
// ❌ Compile error
[NotifyPropertyChanged]
public class ViewModel { } // Missing 'partial'!
// ✅ Correct
[NotifyPropertyChanged]
public partial class ViewModel { }
陷阱 4:在 Templates 中使用 Debugger.Break()
// ❌ Wrong: This generates Debugger.Break() in EVERY target method
public override dynamic? OverrideMethod()
{
Debugger.Break(); // This is run-time code! Ships to production!
return meta.Proceed();
}
// ✅ Correct: meta.DebugBreak() only works during compilation
public override dynamic? OverrideMethod()
{
meta.DebugBreak(); // Only triggers when debugging the compiler
return meta.Proceed();
}
陷阱 5:在 Templates 中放入過重的邏輯
// ❌ Bad: Complex serialization in template (duplicated into every method)
public override dynamic? OverrideMethod()
{
var json = JsonSerializer.Serialize(new
{
Method = meta.Target.Method.Name,
Time = DateTime.UtcNow,
Params = /* complex parameter serialization */
});
File.AppendAllText("audit.log", json);
return meta.Proceed();
}
// ✅ Good: Delegate to a helper
public override dynamic? OverrideMethod()
{
AuditHelper.LogInvocation(meta.Target.Method.Name,
meta.Target.Parameters.ToValueArray());
return meta.Proceed();
}
陷阱 6:在 Aspect 屬性中儲存狀態
// ❌ Wrong: Aspect instances don't exist at runtime
public class CounterAspect : OverrideMethodAspect
{
private int _callCount = 0; // This is a compile-time field!
public override dynamic? OverrideMethod()
{
_callCount++; // Won't work — aspect doesn't exist at runtime
return meta.Proceed();
}
}
// ✅ Correct: Use a runtime static or instance mechanism
public class CounterAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// Use a runtime counter (static ConcurrentDictionary, etc.)
CallCounter.Increment(meta.Target.Method.Name);
return meta.Proceed();
}
}
陷阱 7:在 Fabrics 中依命名空間過濾型別
// ❌ Inefficient: Iterates all types, then filters
amender.SelectMany(p => p.Types)
.Where(t => t.Namespace == "MyApp.Services")
.SelectMany(t => t.Methods)
.AddAspectIfEligible<LogAttribute>();
// ✅ Better: Use GlobalNamespace.GetDescendant() or NamespaceFabric
amender.SelectMany(p => p.GlobalNamespace
.GetDescendant("MyApp.Services")?.Types ?? Enumerable.Empty<INamedType>())
.SelectMany(t => t.Methods)
.AddAspectIfEligible<LogAttribute>();
陷阱 8:忽略非同步方法
// ❌ Bug: Thread.Sleep in async context blocks the thread pool
public override dynamic? OverrideMethod()
{
for (var i = 0; i < MaxAttempts; i++)
{
try { return meta.Proceed(); }
catch when (i < MaxAttempts - 1)
{
Thread.Sleep(DelayMs); // Blocks thread pool thread!
}
}
throw new Exception("Failed");
}
// ✅ Correct: Provide separate async template with Task.Delay
public override async Task<dynamic?> OverrideAsyncMethod()
{
for (var i = 0; i < MaxAttempts; i++)
{
try { return await meta.ProceedAsync(); }
catch when (i < MaxAttempts - 1)
{
await Task.Delay(DelayMs); // Non-blocking
}
}
throw new Exception("Failed");
}
命名慣例
Aspect 類別
| 慣例 | 範例 |
|---|---|
以 Attribute 為後綴 | LogAttribute、CacheAttribute |
| 名稱對應關注點 | RetryAttribute(非 MethodWrapperAttribute) |
使用時為 [Log](C# 允許省略 Attribute 後綴) | [Log]、[Cache]、[Retry] |
Aspect 屬性
| 慣例 | 範例 |
|---|---|
| 組態屬性為 public | public int MaxAttempts { get; set; } |
| 提供合理的預設值 | = 3 |
| 使用描述性名稱 | AbsoluteExpirationSeconds(非 Exp) |
布林屬性以 Is/Should/Log 開頭 | IsExponentialBackoffEnabled、LogCacheActivity |
Aspect 組織結構
MyCompany.Aspects/
├── Logging/
│ ├── LogAttribute.cs
│ ├── LogPerformanceAttribute.cs
│ └── LogExceptionAttribute.cs
├── Validation/
│ ├── NotNullAttribute.cs
│ ├── NotEmptyAttribute.cs
│ └── RangeAttribute.cs
├── Caching/
│ ├── CacheAttribute.cs
│ └── CacheInvalidateAttribute.cs
├── Internal/
│ └── AspectHelper.cs ← Shared runtime helpers
└── MyCompany.Aspects.csproj
效能考量
1. 保持 Templates 精簡
產生的程式碼乘以 N 個目標方法 = N × Template 大小。最小化 Template 程式碼,委派給輔助方法。
2. 快取服務查詢
// ✅ Good: Cache service reference
public override dynamic? OverrideMethod()
{
var logger = AspectServiceLocator.GetLogger(); // Cached internally
logger?.Debug(/* ... */);
return meta.Proceed();
}
3. 避免過度的參數記錄
// Consider: Do you need to log ALL parameters?
[Log(LogParameters = false)] // Skip parameter logging for performance
public async Task ProcessBulkDataAsync(List<Record> records) { ... }
4. 小心編譯期 Foreach
// The foreach is unrolled — for methods with 10 parameters,
// this generates 10 Console.WriteLine calls
foreach (var param in meta.Target.Parameters)
{
Console.WriteLine($"{param.Name} = {param.Value}");
}
// Consider: Is this acceptable for your use case?
測試指導方針
1. 測試每個 Aspect
每個 Aspect 至少應有:
- 正向測試(Aspect 正確運作)
- 負向測試(Aspect 透過資格條件拒絕無效目標)
- 非同步測試(如果 Aspect 支援非同步)
2. 測試 Aspect 互動
測試常見的 Aspect 組合:
[Fact]
public void Log_And_Retry_WorkTogether()
{
// Test that [Log] + [Retry] don't interfere
}
3. 測試服務不可用的情況
[Fact]
public void Cache_WithNoCacheService_FallsBackGracefully()
{
// Don't register ICacheService
AspectServiceLocator.Initialize(new ServiceCollection().BuildServiceProvider());
var service = new TestService();
var result = service.GetData(); // Should work without caching
}
檢查清單:建立新 Aspect 之前
- 是否有現成的 Aspect? 先檢查 GST.Core.Aspects 和官方 Metalama 套件
- AOP 是正確的工具嗎? 簡單的基底類別或工具方法是否就足夠?
- 定義資格條件 — 這個 Aspect 可以套用在哪裡?
- 處理非同步 — Aspect 是否需要獨立的非同步處理?
- 防錯設計 — 服務不可用時會發生什麼?
- Template 大小 — 產生的程式碼是否最小化?是否委派給輔助方法?
- 組態 — 哪些部分應透過屬性設定?
- 文件 — 為 Aspect 類別和屬性加入 XML 註解
- 測試 — 撰寫快照和執行期測試
- 審查 — 讓另一位開發人員審查 Aspect
快速參考卡
// Aspect template
public class [Name]Attribute : OverrideMethodAspect
{
// Configuration
public int MyProperty { get; set; } = defaultValue;
// Eligibility
public override void BuildEligibility(IEligibilityBuilder builder)
{
base.BuildEligibility(builder);
builder.MustNotBeAbstract();
}
// Sync template
public override dynamic? OverrideMethod()
{
// Before
try
{
var result = meta.Proceed();
// After (success)
return result;
}
catch (Exception ex)
{
// After (failure)
throw;
}
finally
{
// Always
}
}
// Async template
public override async Task<dynamic?> OverrideAsyncMethod()
{
// Before
try
{
var result = await meta.ProceedAsync();
// After (success)
return result;
}
catch (Exception ex)
{
// After (failure)
throw;
}
finally
{
// Always
}
}
}
延伸閱讀
| 資源 | 連結 |
|---|---|
| Metalama 文件 | doc.metalama.net |
| Metalama GitHub | github.com/metalama/Metalama |
| Metalama 範例 | github.com/metalama/Metalama.Samples |
| GST Aspect 使用指南 | 內部:GST Aspect Usage Guide |
| GST 框架指南 | 內部:GST Framework Guide |
Metalama 技術指南結束
版本 1.0 — 2026-03-31