Skip to main content

GST Framework: Real-World Metalama Examples

The GST framework (GST.Core.Aspects) contains 32 production aspects used across industrial automation, pharmaceutical compliance, and manufacturing systems. This chapter shows how Metalama is used in practice.


Architecture Overview

Service Resolution in Aspects

The biggest challenge with compile-time AOP is accessing runtime services (logging, caching, user context) from aspects that are expanded at compile time. The GST framework solves this with the AspectServiceLocator pattern:

┌─────────────────────────────────────────────────┐
│ 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 Registration

// 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);
}
}

Graceful Degradation

All GST aspects are designed to work even when services are unavailable:

// 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 Catalog by Category

Logging Aspects

[Log] — Method Logging

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;
}
}
}

Usage:

[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] — Execution Time Logging

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

[LogException] — Exception-Only Logging

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

Validation Aspects

[NotNull] — Null Check

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

Usage:

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

[NotEmpty] — Empty String/Collection Check

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

[Range] — Numeric Range Validation

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

Caching Aspects

[Cache] — Method Result Caching

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;
}
}

Usage:

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

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

Exception Handling Aspects

[Retry] — Retry with Exponential Backoff

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()
}
}

Usage:

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

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

[CircuitBreaker] — Circuit Breaker Pattern

Implements the three-state circuit breaker:

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

Usage:

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

Performance Aspects

[Timing] — Execution Time Measurement

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

[Throttle] — Rate Limiting

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

[Debounce] — Debouncing

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

Audit Aspects (FDA Part 11)

[Audit] — Operation Audit Trail

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

Generates audit log:

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

[AuditDataChange] — Before/After Data Tracking

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

Generates:

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

[RequireChangeReason] — Mandatory Change Reason

[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
}

Authorization Aspects

[Authorize] — Authentication Check

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] — Permission Check

[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) { ... }

Threading Aspects

[Synchronized] — Thread-Safe Execution

[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 Thread Dispatch

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

Observability Aspects

[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";
}

Licensing Aspects

[LicenseRequired] — Feature Gating

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

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

Aspect Stacking in Practice

Real GST service methods often combine multiple aspects:

[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;
}

Execution order (outermost to innermost):

  1. [Authorize] → Check authentication
  2. [RequirePermission] → Check permission
  3. [NotNull]/[NotEmpty] → Validate parameters
  4. [Log] → Log entry with parameters
  5. [Audit] → Record operation
  6. [AuditDataChange] → Track before/after values
  7. [Retry] → Retry on failure
  8. [Timing] → Measure execution time
  9. Original method → Business logic executes

Summary

CategoryAspectsCount
Logging[Log], [LogPerformance], [LogException]3
Validation[NotNull], [NotEmpty], [Range]3
Caching[Cache], [CacheInvalidate]2
Exception[Retry], [CircuitBreaker], [HandleException]3
Performance[Timing], [Throttle], [Debounce]3
Threading[Synchronized], [RunOnUiThread]2
Audit[Audit], [AuditDataChange], [TrackChanges], [RequireChangeReason], [Part11Audit]5
Authorization[Authorize], [AllowAnonymous], [RequirePermission], [RequireAllPermissions], [RequireAnyPermission]5
Observability[NotifyPropertyChanged], [DependsOn]2
Localization[Localized], [NoLocalized]2
Licensing[LicenseRequired], [LicenseCheck]2
Total32

Next: Advanced Topics — Advising, member introduction, eligibility, and diagnostics.