GST 切面使用規範
適用範圍:所有使用 GST 底層框架的應用層專案
最後更新:2026-02-03
概述
GST 底層框架提供完整的 Metalama 切面設計(GST.Core.Aspects),應用層專案必須正確使用這些切面,以確保:
- 統一的 Logging 格式與行為
- FDA Part 11 合規性(稽核追蹤)
- 一致的例外處理與重試機制
- 可維護的橫切關注點管理
切面所在位置
| 專案 | 命名空間 | NuGet |
|---|---|---|
GST.Core.Aspects | GST.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
常見組合模式
模式 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.WriteLine或Debug.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.0 | 2026-02-03 | 初始版本 |