跳至主要内容

GST 框架:Metalama 實戰範例

GST 框架(GST.Core.Aspects)包含 32 個正式環境 Aspect,應用於工業自動化、藥品法規合規及製造系統。本章展示 Metalama 在實務中的使用方式。


架構概觀

Aspect 中的服務解析

編譯期 AOP 最大的挑戰是從編譯期展開的 Aspect 中存取執行階段服務(日誌、快取、使用者上下文)。GST 框架透過 AspectServiceLocator 模式解決此問題:

┌─────────────────────────────────────────────────┐
│ APPLICATION STARTUP │
│ │
│ var host = Host.CreateDefaultBuilder() │
│ .ConfigureServices(services => │
│ { │
│ services.AddGstAspects(); // Register │
│ }) │
│ .Build(); │
│ │
│ host.Services.InitializeAspects(); // Connect │
│ │
└─────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────┐
│ AspectServiceLocator (Static) │
│ │
│ IServiceProvider _provider; │
│ │
│ GetService<T>() → resolve from DI │
│ GetLogger() → ILoggerService (cached) │
│ GetLocalizer() → ILocalizer (cached) │
│ │
└─────────────────────────────────────────────────┘

┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ [Log] │ │ [Cache] │ │ [Audit] │ ... (32 aspects)
│ Aspect │ │ Aspect │ │ Aspect │
└─────────┘ └─────────┘ └─────────┘

AspectServiceLocator

// GST.Core.Aspects.AspectServiceLocator
public static class AspectServiceLocator
{
private static IServiceProvider? _serviceProvider;
private static ILoggerService? _loggerService;
private static ILocalizer? _localizer;

public static void Initialize(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_loggerService = null; // Reset cache
_localizer = null;
}

public static T? GetService<T>() where T : class
{
return _serviceProvider?.GetService<T>();
}

public static ILoggerService? GetLogger()
{
_loggerService ??= GetService<ILoggerService>();
return _loggerService;
}

public static ILocalizer? GetLocalizer()
{
_localizer ??= GetService<ILocalizer>();
return _localizer;
}
}

DI 註冊

// GST.Core.Aspects.AspectsServiceCollectionExtensions
public static class AspectsServiceCollectionExtensions
{
public static IServiceCollection AddGstAspects(this IServiceCollection services)
{
services.AddSingleton<ICacheKeyGenerator, DefaultCacheKeyGenerator>();
services.AddSingleton<ICacheService, MemoryCacheService>();
return services;
}

public static void InitializeAspects(this IServiceProvider provider)
{
AspectServiceLocator.Initialize(provider);
}
}

優雅降級

所有 GST Aspect 皆設計為即使服務不可用也能運作

// Pattern used in all aspects
var logger = AspectServiceLocator.GetLogger();
if (logger != null)
{
logger.Debug(typeName, message);
}
else
{
// Fallback: AspectLogger writes to Console
AspectLogger.Debug(typeName, message);
}

依類別分類的 Aspect 目錄

日誌 Aspect

[Log] — 方法日誌

public class LogAttribute : OverrideMethodAspect
{
public bool LogParameters { get; set; } = true;
public bool LogReturnValue { get; set; } = true;
public bool LogExceptions { get; set; } = true;

public override dynamic? OverrideMethod()
{
var typeName = meta.Target.Type.Name;
var methodName = meta.Target.Method.Name;

AspectLogger.Debug(typeName, $"Entering {methodName}");

if (LogParameters)
{
foreach (var param in meta.Target.Parameters)
{
AspectLogger.Debug(typeName, $" {param.Name} = {param.Value}");
}
}

try
{
var result = meta.Proceed();

if (LogReturnValue &&
meta.Target.Method.ReturnType.SpecialType != SpecialType.Void)
{
AspectLogger.Debug(typeName, $"Exiting {methodName} → {result}");
}
return result;
}
catch (Exception ex)
{
if (LogExceptions)
{
AspectLogger.Error(typeName, $"Exception in {methodName}", ex);
}
throw;
}
}
}

使用方式

[Log]
public async Task<Recipe> GetRecipeAsync(int id) { ... }

[Log(LogParameters = false)] // Don't log sensitive parameters
public async Task LoginAsync(string username, string password) { ... }

[LogPerformance] — 執行時間日誌

[LogPerformance(ThresholdMs = 500)]  // Only log if execution exceeds 500ms
public async Task<Report> GenerateReportAsync() { ... }

[LogException] — 僅記錄例外

[LogException]  // Log exceptions without logging entry/exit
public void ProcessBatch(List<Item> items) { ... }

驗證 Aspect

[NotNull] — 空值檢查

public class NotNullAttribute : ContractAspect
{
public override void Validate(dynamic? value)
{
if (value == null)
{
throw new ArgumentNullException(meta.Target.Parameter.Name);
}
}
}

使用方式

public void SaveRecipe([NotNull] Recipe recipe, [NotNull] string userId) { ... }

[NotEmpty] — 空字串/集合檢查

public void ImportData([NotEmpty] string filePath, [NotEmpty] List<Record> records) { ... }

[Range] — 數值範圍驗證

public void SetTemperature([Range(Min = 0, Max = 400)] double temperature) { ... }

快取 Aspect

[Cache] — 方法結果快取

public class CacheAttribute : OverrideMethodAspect
{
public int AbsoluteExpirationSeconds { get; set; } = 300;
public int SlidingExpirationSeconds { get; set; } = 0;
public string? KeyPrefix { get; set; }
public bool LogCacheActivity { get; set; } = false;

public override dynamic? OverrideMethod()
{
var cacheService = AspectServiceLocator.GetService<ICacheService>();
if (cacheService == null)
{
// No cache service → just execute normally
return meta.Proceed();
}

var keyGenerator = AspectServiceLocator.GetService<ICacheKeyGenerator>();
var cacheKey = keyGenerator?.GenerateKey(
meta.Target.Type.Name,
meta.Target.Method.Name,
meta.Target.Parameters.ToValueArray())
?? $"{meta.Target.Type.Name}.{meta.Target.Method.Name}";

// Try to get from cache
if (cacheService.TryGet(cacheKey, out var cached))
{
if (LogCacheActivity)
AspectLogger.Debug(meta.Target.Type.Name, $"Cache HIT: {cacheKey}");
return cached;
}

// Execute and cache
var result = meta.Proceed();
cacheService.Set(cacheKey, result, AbsoluteExpirationSeconds);

if (LogCacheActivity)
AspectLogger.Debug(meta.Target.Type.Name, $"Cache SET: {cacheKey}");

return result;
}
}

使用方式

[Cache(AbsoluteExpirationSeconds = 600)]
public async Task<List<Recipe>> GetAllRecipesAsync() { ... }

[Cache(KeyPrefix = "config", SlidingExpirationSeconds = 120)]
public AppConfig GetConfiguration() { ... }

例外處理 Aspect

[Retry] — 指數退避重試

public class RetryAttribute : OverrideMethodAspect
{
public int MaxAttempts { get; set; } = 3;
public int DelayMs { get; set; } = 500;
public bool IsExponentialBackoffEnabled { get; set; } = true;

public override dynamic? OverrideMethod()
{
Exception? lastException = null;

for (var attempt = 0; attempt < MaxAttempts; attempt++)
{
try
{
return meta.Proceed();
}
catch (Exception ex)
{
lastException = ex;
AspectLogger.Warning(meta.Target.Type.Name,
$"Attempt {attempt + 1}/{MaxAttempts} failed: {ex.Message}");

if (attempt < MaxAttempts - 1)
{
var delay = IsExponentialBackoffEnabled
? (int)(DelayMs * Math.Pow(2, attempt))
: DelayMs;
Thread.Sleep(delay);
}
}
}

throw lastException!;
}

public override async Task<dynamic?> OverrideAsyncMethod()
{
// Same logic but with Task.Delay instead of Thread.Sleep
// and await meta.ProceedAsync() instead of meta.Proceed()
}
}

使用方式

[Retry(MaxAttempts = 5, DelayMs = 1000)]
public async Task SendSecsMessageAsync(SecsMessage message) { ... }

[Retry(MaxAttempts = 3, IsExponentialBackoffEnabled = false)]
public ModbusResponse ReadRegister(int address) { ... }

[CircuitBreaker] — 斷路器模式

實作三態斷路器:

    ┌──────────┐   Failure threshold    ┌──────────┐
│ CLOSED │ ──────────────────────▶ │ OPEN │
│ (normal) │ │ (reject) │
└──────────┘ └────┬─────┘
▲ │
│ Recovery timeout │
│ ▼
│ ┌───────────┐
└──────── Success ────────────│ HALF-OPEN │
│ (probe) │
└───────────┘

使用方式

[CircuitBreaker(FailureThreshold = 5, RecoveryTimeoutSeconds = 30)]
public async Task<byte[]> ReadPlcDataAsync(string address) { ... }

效能 Aspect

[Timing] — 執行時間測量

[Timing(WarningThresholdMs = 2000)]
public async Task<Report> GenerateDailyReportAsync() { ... }

[Throttle] — 頻率限制

[Throttle(IntervalMs = 1000)]  // At most once per second
public void SendStatusUpdate(MachineStatus status) { ... }

[Debounce] — 防抖動

[Debounce(DelayMs = 500)]  // Wait 500ms of quiet before executing
public void OnSearchTextChanged(string text) { ... }

稽核 Aspect(FDA Part 11)

[Audit] — 操作稽核軌跡

[Audit(OperationName = "UpdateRecipe", IncludeParameters = true)]
public async Task UpdateRecipeAsync(Recipe recipe) { ... }

產生的稽核日誌:

[AUDIT] User: john.doe | Operation: UpdateRecipe | Time: 2026-03-31T10:15:30Z
Parameters: recipe = { Id: 42, Name: "Process A", Temperature: 250 }

[AuditDataChange] — 變更前後資料追蹤

[AuditDataChange]
public async Task UpdateTemperatureAsync(int recipeId, double newTemp)
{
var recipe = await _repo.GetByIdAsync(recipeId);
recipe.Temperature = newTemp; // Change tracked
await _repo.SaveAsync(recipe);
}

產生的紀錄:

[DATA_CHANGE] User: john.doe | Entity: Recipe
Temperature: 200 → 250
ModifiedAt: 2026-03-31T10:15:30Z

[RequireChangeReason] — 強制填寫變更原因

[RequireChangeReason]
public async Task UpdateCriticalParameterAsync(
int parameterId, double newValue, string changeReason)
{
// changeReason is validated as not-null/not-empty
// and included in the audit trail
}

授權 Aspect

[Authorize] — 身份驗證檢查

public class AuthorizeAttribute : OverrideMethodAspect
{
public bool ThrowOnFailure { get; set; } = true;

[CompileTime]
private static bool HasAllowAnonymous(IMethod method)
{
return method.Attributes.Any(
a => a.Type.Name == "AllowAnonymousAttribute");
}

public override dynamic? OverrideMethod()
{
if (HasAllowAnonymous(meta.Target.Method))
{
return meta.Proceed();
}

var userService = AspectServiceLocator.GetService<ICurrentUserService>();
if (userService == null || !userService.IsAuthenticated)
{
if (ThrowOnFailure)
throw new PermissionException("Authentication required");
return default;
}

return meta.Proceed();
}
}

[RequirePermission] — 權限檢查

[Authorize]
[RequirePermission("recipe.edit")]
public async Task EditRecipeAsync(Recipe recipe) { ... }

[RequireAnyPermission("admin", "supervisor")]
public async Task ApproveChangeAsync(int changeId) { ... }

[RequireAllPermissions("recipe.view", "audit.view")]
public async Task ViewAuditTrailAsync(int recipeId) { ... }

執行緒 Aspect

[Synchronized] — 執行緒安全執行

[Synchronized(TimeoutMs = 5000, IsPerInstance = true)]
public void WriteToSerialPort(byte[] data)
{
// Only one thread can execute this at a time (per instance)
_serialPort.Write(data);
}

[RunOnUiThread] — WPF UI 執行緒調派

[RunOnUiThread]
public void UpdateDisplay(MachineStatus status)
{
StatusText = status.Description;
ProgressBar = status.Progress;
}

可觀測性 Aspect

[NotifyPropertyChanged] — INotifyPropertyChanged

[NotifyPropertyChanged]
public partial class MachineViewModel
{
public string MachineName { get; set; }
public MachineState State { get; set; }
public double Temperature { get; set; }

[DependsOn(nameof(State))]
public bool IsRunning => State == MachineState.Running;

[DependsOn(nameof(Temperature))]
public string TemperatureDisplay => $"{Temperature:F1}°C";
}

授權 Aspect

[LicenseRequired] — 功能授權閘控

[LicenseRequired("AuditTrail")]
public async Task<List<AuditEntry>> GetAuditHistoryAsync() { ... }

[LicenseRequired("AdvancedReporting")]
public async Task<Report> GenerateComplianceReportAsync() { ... }

實務中的 Aspect 堆疊

實際 GST 服務方法經常組合多個 Aspect:

[Authorize]
[RequirePermission("recipe.edit")]
[Log(LogParameters = true)]
[Audit(OperationName = "ModifyRecipe")]
[AuditDataChange]
[Retry(MaxAttempts = 3)]
[Timing(WarningThresholdMs = 5000)]
public async Task<Recipe> UpdateRecipeAsync(
[NotNull] Recipe recipe,
[NotEmpty] string changeReason)
{
// Business logic only — all cross-cutting concerns handled by aspects
var existing = await _repository.GetByIdAsync(recipe.Id);
existing.Update(recipe);
await _repository.SaveAsync(existing);
return existing;
}

執行順序(由外而內):

  1. [Authorize] — 檢查身份驗證
  2. [RequirePermission] — 檢查權限
  3. [NotNull]/[NotEmpty] — 驗證參數
  4. [Log] — 記錄進入點與參數
  5. [Audit] — 記錄操作
  6. [AuditDataChange] — 追蹤變更前後值
  7. [Retry] — 失敗時重試
  8. [Timing] — 測量執行時間
  9. 原始方法 — 商業邏輯執行

摘要

類別Aspect數量
日誌[Log], [LogPerformance], [LogException]3
驗證[NotNull], [NotEmpty], [Range]3
快取[Cache], [CacheInvalidate]2
例外[Retry], [CircuitBreaker], [HandleException]3
效能[Timing], [Throttle], [Debounce]3
執行緒[Synchronized], [RunOnUiThread]2
稽核[Audit], [AuditDataChange], [TrackChanges], [RequireChangeReason], [Part11Audit]5
授權[Authorize], [AllowAnonymous], [RequirePermission], [RequireAllPermissions], [RequireAnyPermission]5
可觀測性[NotifyPropertyChanged], [DependsOn]2
在地化[Localized], [NoLocalized]2
授權許可[LicenseRequired], [LicenseCheck]2
合計32

下一篇進階主題 — Advising、成員引入、適用性與診斷。