跳至主要内容

GST 切面使用規範

適用範圍:所有使用 GST 底層框架的應用層專案

最後更新:2026-02-03


概述

GST 底層框架提供完整的 Metalama 切面設計(GST.Core.Aspects),應用層專案必須正確使用這些切面,以確保:

  • 統一的 Logging 格式與行為
  • FDA Part 11 合規性(稽核追蹤)
  • 一致的例外處理與重試機制
  • 可維護的橫切關注點管理

何時該用 Aspect(決策樹)

這個橫切關注點該不該用 aspect?」是 GST 應用層最常見的設計問題。下面的決策樹從 5 個面向篩出正確路徑:是不是橫切關注點 → 多處使用 → 編譯期成本 → 執行期替代方案 → 是否已有現成 aspect。

各葉節點的選用情境與成本:

結論何時選開發成本Runtime 成本
普通方法只本方法用一次最低最低
原地處理1-2 處用,且邏輯短(< 5 行)低(複製貼上)最低
直接套用 GST aspect多處 + 已有現成 aspect(90% 場景)最低(一個 attribute)零執行期 overhead(編譯期織入)
新建自訂 Metalama aspect多處 + GST 沒有對應 + 願意多花 build time中(需學 Metalama API)零執行期 overhead
Decorator / Middleware多處 + 不能多花 build time / 介面 + DI 容器架構中(需設計介面)中(執行期反射 / proxy)

讀圖重點:

  • Q1(橫切關注點)是入口閥門:如果只是「這一個方法要做某件事」,那是商業邏輯,不該用 aspect。橫切關注點的定義是「多個原本不相關的方法都需要同一段行為」(如所有 Service 方法都要 log)。
  • Q2(多處使用)防止過度抽象:1-2 處就抽 aspect 反而是 over-engineering;3 處以上才開始有 reuse 價值。
  • Q3 + Q4 是成本/替代方案的權衡:Metalama 編譯期織入零執行期 overhead,但會增加 build time 與 obj 檔;如果接受不了,Decorator / Castle DynamicProxy 是執行期替代方案。
  • Q5 防止重造輪子:GST 已經提供 Logging / Audit / Cache / Retry / Validation 等常見 aspect(見下方「可用切面清單」),絕大多數場景都應該直接套用,不要自己寫。

切面所在位置

專案命名空間NuGet
GST.Core.AspectsGST.Core.Aspects.*內部套件

命名空間對照

命名空間切面類別
GST.Core.Aspects.Logging[Log], [LogPerformance], [LogException]
GST.Core.Aspects.Audit[Audit], [Part11Audit], [AuditDataChange]
GST.Core.Aspects.Caching[Cache], [CacheInvalidate]
GST.Core.Aspects.Exception[HandleException], [CircuitBreaker], [Retry]
GST.Core.Aspects.Validation[NotNull], [NotEmpty], [Range]
GST.Core.Aspects.Authorization[Authorize], [RequirePermission]
GST.Core.Aspects.Performance[Throttle], [Debounce]
GST.Core.Aspects.Threading[RunOnUiThread], [Synchronized]
GST.Core.Aspects.Localization[Localized]
GST.Core.Aspects.Observability[NotifyPropertyChanged]

可用切面清單

1. Logging 切面

切面用途參數
[Log]記錄方法進入/退出LogParameters, LogReturnValue, LogExceptions
[LogPerformance]記錄執行時間ThresholdMs(超過才記錄)
[LogException]記錄例外資訊-
[Log(LogParameters = true, LogReturnValue = true)]
public async Task<Recipe> GetRecipeAsync(int id)
{
// 自動記錄:方法進入、參數、返回值、例外
}

[LogPerformance(ThresholdMs = 100)]
public void ExpensiveOperation()
{
// 僅當執行超過 100ms 才記錄
}

2. Audit 切面(FDA Part 11)

切面用途參數
[Audit]基本稽核OperationName, IncludeParameters
[Part11Audit]FDA 合規稽核EntityType, RequireChangeReason
[AuditDataChange]資料變更追蹤EntityType, OldValueParameter
[RequireChangeReason]強制變更理由-
[Part11Audit(EntityType = "Recipe", RequireChangeReason = true)]
public async Task UpdateRecipeAsync(int id, Recipe recipe, string changeReason)
{
// 自動記錄:User、timestamp、Hash Chain、完整性驗證
}

[AuditDataChange(EntityType = "Recipe", OldValueParameter = "existing")]
public void Update(Recipe existing, Recipe newValues)
{
// 自動記錄修改前後的值
}

3. Exception 切面

切面用途參數
[HandleException]捕捉並處理例外Suppress
[CircuitBreaker]熔斷器模式FailureThreshold, BreakDurationMs
[Retry]自動重試MaxAttempts, DelayMs, IsExponentialBackoffEnabled
[CircuitBreaker(FailureThreshold = 5, BreakDurationMs = 30000)]
[Retry(MaxAttempts = 3, IsExponentialBackoffEnabled = true)]
public async Task<Data> FetchFromExternalApiAsync()
{
// 5 次失敗後熔斷 30 秒
// 重試 3 次,延遲 500ms → 1000ms → 2000ms
}

4. Caching 切面

切面用途參數
[Cache]快取返回值AbsoluteExpirationSeconds, SlidingExpirationSeconds
[CacheInvalidate]清除快取MethodNames, Pattern
[Cache(AbsoluteExpirationSeconds = 300)]
public async Task<Product> GetProductAsync(int id)
{
// 結果快取 5 分鐘
}

[CacheInvalidate(MethodNames = new[] { "GetProduct", "GetAllProducts" })]
public async Task UpdateProductAsync(Product product)
{
// 更新後自動清除相關快取
}

5. Validation 切面

切面用途
[NotNull]參數/屬性非 null
[NotEmpty]字串/集合非空
[Range]數值範圍驗證
public void ProcessUser(
[NotNull] User user,
[NotEmpty] string username,
[Range(1, 120)] int age)
{
// 參數自動驗證,失敗拋出 ArgumentException
}

6. Threading 切面

切面用途參數
[RunOnUiThread]強制 UI 執行緒-
[Synchronized]互斥鎖TimeoutMs, IsPerInstance
[RunOnUiThread]
public void UpdateStatusBar(string message)
{
// 自動切換到 UI 執行緒
}

[Synchronized(TimeoutMs = 5000)]
public void UpdateSharedResource()
{
// 自動互斥鎖,5 秒超時
}

7. Performance 切面

切面用途參數
[Throttle]節流IntervalMs, ReturnLastResult
[Debounce]防抖DelayMs
[Throttle(IntervalMs = 1000)]
public void UpdateUI()
{
// 最多 1 秒執行一次
}

[Debounce(DelayMs = 300)]
public async Task SearchAsync(string query)
{
// 300ms 無新呼叫後才執行
}

應用層必須使用切面的場景

強制使用(必須遵循)

場景必須使用的切面說明
Public 方法[Log]所有 public 方法必須有 logging
外部 API 呼叫[CircuitBreaker] + [Retry]防止外部服務故障影響系統
硬體通訊[CircuitBreaker] + [Retry]設備通訊可能失敗
FDA 相關操作[Part11Audit]合規性要求
資料修改[AuditDataChange]追蹤變更歷史
UI 更新[RunOnUiThread]確保 UI 執行緒安全
快取資料[Cache] / [CacheInvalidate]統一快取管理

範例:Service 類別

public class RecipeService : IRecipeService
{
[Log]
[Cache(AbsoluteExpirationSeconds = 300)]
public async Task<Recipe?> GetRecipeAsync(int id)
{
return await _repository.GetByIdAsync(id);
}

[Log]
[Part11Audit(EntityType = "Recipe", RequireChangeReason = true)]
[AuditDataChange(EntityType = "Recipe")]
[CacheInvalidate(MethodNames = new[] { "GetRecipe", "GetAllRecipes" })]
public async Task UpdateRecipeAsync(Recipe recipe, string changeReason)
{
await _repository.UpdateAsync(recipe);
}
}

範例:外部通訊

public class ExternalApiClient
{
[Log]
[CircuitBreaker(FailureThreshold = 5, BreakDurationMs = 30000)]
[Retry(MaxAttempts = 3, IsExponentialBackoffEnabled = true)]
public async Task<ApiResponse> CallExternalServiceAsync(ApiRequest request)
{
return await _httpClient.PostAsync(request);
}
}

範例:ViewModel

[NotifyPropertyChanged]
public partial class RecipeEditorViewModel : ViewModelBase
{
public string RecipeName { get; set; }

[Log]
[RunOnUiThread]
public async Task LoadRecipeAsync(int id)
{
var recipe = await _recipeService.GetRecipeAsync(id);
RecipeName = recipe?.Name ?? string.Empty;
}
}

允許直接 Logging 的場景

以下情況可以直接使用 _logger.Log*()

場景說明範例
底層框架內部GST.Core.* 專案框架需要細粒度控制
迴圈內部詳細記錄每次迭代的細節_logger.Debug("Processing item {i}", i)
條件式記錄根據條件決定是否記錄if (isVerbose) _logger.Debug(...)
Private 方法內部非入口點的內部邏輯內部輔助方法

注意事項

  • 直接 logging 時必須在程式碼註解中說明原因
  • 優先考慮是否可以用切面取代
// Allowed: Loop iteration details cannot be captured by aspect
for (var i = 0; i < items.Count; i++)
{
_logger.Debug("Processing item {Index} of {Total}", i, items.Count);
ProcessItem(items[i]);
}

禁止事項

應用層禁止

禁止事項說明
Console.WriteLine()絕對禁止,使用 logging
Debug.WriteLine()絕對禁止,使用 logging
未包裝的 _logger.Log*() 在 public 方法必須使用 [Log] 切面
手動 try-catch 僅為 logging使用 [LogException][HandleException]

錯誤範例

// Public method WITHOUT [Log] aspect
public async Task<Recipe> GetRecipeAsync(int id)
{
_logger.LogInformation("Getting recipe {Id}", id); // 直接 logging
try
{
return await _repository.GetByIdAsync(id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get recipe"); // 手動 catch 僅為 logging
throw;
}
}

正確範例

// Public method WITH [Log] aspect
[Log(LogParameters = true, LogExceptions = true)]
public async Task<Recipe> GetRecipeAsync(int id)
{
return await _repository.GetByIdAsync(id);
// 切面自動處理:入口 logging、參數記錄、例外記錄
}

切面初始化

應用程式啟動時必須初始化切面服務

// Program.cs 或 App.xaml.cs
var services = new ServiceCollection();

// 註冊 GST 服務
services.AddGstLogging(config);
services.AddGstAspects();

// 建立 ServiceProvider
var provider = services.BuildServiceProvider();

// 初始化切面服務定位器(必須!)
provider.InitializeAspects();

未初始化的後果

如果未呼叫 InitializeAspects()

  • Logging 切面無法正常運作
  • Cache 切面無法存取快取服務
  • Audit 切面無法取得當前用戶資訊
  • 應用程式可能拋出 InvalidOperationException

Aspect Lifecycle 與 Polly / Serilog 整合

GST 的 [Log] / [Audit] / [Retry] 等 aspect 並非純 inline 程式碼 — 它們在內部會委派給 Polly(韌性策略)與 Serilog(結構化日誌)這兩個第三方框架。下圖是一次帶有 [Log] + [Audit] + [Retry] 三層 aspect 的呼叫,從 Caller 進入到 Polly 內部 retry loop 再回到 Caller 的完整時序:

各 aspect 內部接到的第三方服務:

Aspect委派給透過什麼 API整合產出
[Log]SerilogILogger.LogDebug / LogInformation / LogWarning{Method}, {Args}, {Elapsed} 結構化欄位進入 / 離開 / 例外的 structured log
[LogPerformance]Serilog + StopwatchLogWarning if Elapsed > Threshold慢呼叫告警
[Audit] / [Part11Audit]IAuditService → DBRecordIntent / RecordResult(before/after snapshot)FDA Part 11 audit trail
[Retry]PollyResiliencePipeline.ExecuteAsync(...) 內含 AddRetry 策略自動重試 + 退避 + Jitter
[CircuitBreaker]PollyResiliencePipeline.ExecuteAsync(...) 內含 AddCircuitBreaker連續失敗時短路
[Cache]IDistributedCache / IMemoryCacheGetAsync / SetAsync (key = method + args)跨次呼叫快取
[Throttle] / [Debounce]Rx.NETThrottle / Debounce operator on internal Subject<T>UI / 高頻事件節流

讀圖重點:

  • Aspect ordering 對應「洋蔥式包裹」:宣告順序最上的 aspect 在最外層(圖中 [Log]),最下的在最內層([Retry],緊貼原始方法)。可用 [assembly: AspectOrder(...)] 明確指定。
  • meta.Proceed() 是所有 aspect 的橋接點:每個 aspect 內部呼叫 meta.Proceed() 就是「呼叫下一層」;圖中每條 aspect → 下一個 aspect 的箭頭都對應一次 meta.Proceed()
  • 第三方框架責任分工:Polly 負責 retry / circuit breaker / timeout 等控制流邏輯;Serilog 負責 structured logging 輸出;GST aspect 是黏著層,把宣告式 attribute 翻譯成這兩者的 API 呼叫。
  • Aspect 不直接寫 try/catch try { Polly.Execute(...) }:Polly 在 [Retry] aspect 的 OverrideMethod() template 中只實例化一次(編譯期織入),執行期沒有反射、沒有 proxy。詳見 Metalama 編譯時期 Weaving

常見組合模式

模式 1:完整的 CRUD Service

public class EntityService<T> : IEntityService<T>
{
[Log]
[Cache(AbsoluteExpirationSeconds = 300)]
public async Task<T?> GetByIdAsync(int id) { ... }

[Log]
[Cache(AbsoluteExpirationSeconds = 60)]
public async Task<IReadOnlyList<T>> GetAllAsync() { ... }

[Log]
[Part11Audit(EntityType = nameof(T))]
[CacheInvalidate(MethodNames = new[] { "GetById", "GetAll" })]
public async Task CreateAsync(T entity) { ... }

[Log]
[Part11Audit(EntityType = nameof(T), RequireChangeReason = true)]
[AuditDataChange(EntityType = nameof(T))]
[CacheInvalidate(MethodNames = new[] { "GetById", "GetAll" })]
public async Task UpdateAsync(T entity, string changeReason) { ... }

[Log]
[Part11Audit(EntityType = nameof(T), RequireChangeReason = true)]
[CacheInvalidate(MethodNames = new[] { "GetById", "GetAll" })]
public async Task DeleteAsync(int id, string changeReason) { ... }
}

模式 2:外部 API 整合

public class ExternalServiceClient
{
[Log]
[CircuitBreaker(FailureThreshold = 5, BreakDurationMs = 30000)]
[Retry(MaxAttempts = 3, IsExponentialBackoffEnabled = true)]
[Cache(AbsoluteExpirationSeconds = 60)]
public async Task<ExternalData> FetchDataAsync(string id) { ... }

[Log]
[CircuitBreaker(FailureThreshold = 3, BreakDurationMs = 60000)]
[Retry(MaxAttempts = 2)]
public async Task SendDataAsync(ExternalData data) { ... }
}

模式 3:硬體通訊

public class DeviceController
{
[Log]
[CircuitBreaker(FailureThreshold = 3, BreakDurationMs = 10000)]
[Retry(MaxAttempts = 3, DelayMs = 500)]
[Synchronized]
public async Task<DeviceStatus> GetStatusAsync() { ... }

[Log]
[CircuitBreaker(FailureThreshold = 3, BreakDurationMs = 10000)]
[Retry(MaxAttempts = 2)]
[Synchronized]
public async Task SendCommandAsync(DeviceCommand command) { ... }
}

模式 4:UI ViewModel

[NotifyPropertyChanged]
public partial class DataViewModel : ViewModelBase
{
public string Status { get; set; }
public bool IsLoading { get; set; }

[Log]
[RunOnUiThread]
public async Task LoadDataAsync()
{
IsLoading = true;
try
{
var data = await _service.GetDataAsync();
Status = data.Status;
}
finally
{
IsLoading = false;
}
}

[Log]
[RunOnUiThread]
[Throttle(IntervalMs = 500)]
public void RefreshUI()
{
// UI 更新邏輯
}
}

檢查清單

開發完成後,請確認:

  • 所有 public 方法都有 [Log] 切面
  • 外部通訊有 [CircuitBreaker] + [Retry]
  • FDA 相關操作有 [Part11Audit]
  • 資料修改有 [AuditDataChange]
  • Console.WriteLineDebug.WriteLine
  • 應用程式啟動時有呼叫 InitializeAspects()
  • 直接 logging(如有)已在註解中說明原因

相關文件

文件完整路徑
切面原始碼D:\WorkSpace\GatherTech\Core\GST-develop\src\Core\GST.Core.Aspects\
切面測試D:\WorkSpace\GatherTech\Core\GST-develop\tests\GST.Core.Aspects.Tests\
共用規範D:\WorkSpace\GatherTech\PM\docs\shared\agent-rules\COMMON-RULES.md
命名規範D:\WorkSpace\GatherTech\PM\docs\shared\GST-CSharp-Naming-Conventions.md

版本記錄

版本日期變更
1.0.02026-02-03初始版本