跳到主要内容

最佳實務與常見陷阱

撰寫正確、可維護且高效能 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 為後綴LogAttributeCacheAttribute
名稱對應關注點RetryAttribute(非 MethodWrapperAttribute
使用時為 [Log](C# 允許省略 Attribute 後綴)[Log][Cache][Retry]

Aspect 屬性

慣例範例
組態屬性為 publicpublic int MaxAttempts { get; set; }
提供合理的預設值= 3
使用描述性名稱AbsoluteExpirationSeconds(非 Exp
布林屬性以 Is/Should/Log 開頭IsExponentialBackoffEnabledLogCacheActivity

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 之前

  1. 是否有現成的 Aspect? 先檢查 GST.Core.Aspects 和官方 Metalama 套件
  2. AOP 是正確的工具嗎? 簡單的基底類別或工具方法是否就足夠?
  3. 定義資格條件 — 這個 Aspect 可以套用在哪裡?
  4. 處理非同步 — Aspect 是否需要獨立的非同步處理?
  5. 防錯設計 — 服務不可用時會發生什麼?
  6. Template 大小 — 產生的程式碼是否最小化?是否委派給輔助方法?
  7. 組態 — 哪些部分應透過屬性設定?
  8. 文件 — 為 Aspect 類別和屬性加入 XML 註解
  9. 測試 — 撰寫快照和執行期測試
  10. 審查 — 讓另一位開發人員審查 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 GitHubgithub.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