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;
}
執行順序(由外而內):
[Authorize]— 檢查身份驗證[RequirePermission]— 檢查權限[NotNull]/[NotEmpty]— 驗證參數[Log]— 記錄進入點與參數[Audit]— 記錄操作[AuditDataChange]— 追蹤變更前後值[Retry]— 失敗時重試[Timing]— 測量執行時間- 原始方法 — 商業邏輯執行
摘要
| 類別 | 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、成員引入、適用性與診斷。