Pattern Libraries
Metalama ships with official pattern libraries — production-ready aspects for common cross-cutting concerns. These are available as separate NuGet packages and provide well-tested implementations that you can use directly or learn from.
Overview
| Package | Purpose | Key Aspects |
|---|---|---|
Metalama.Patterns.Contracts | Input validation | [NotNull], [NotEmpty], [Range], [Positive], [Regex], [Url], [Email] |
Metalama.Patterns.Caching | Method result caching | [Cache], [InvalidateCache] |
Metalama.Patterns.Observability | MVVM property notification | [Observable] |
Metalama.Patterns.Memoization | Pure function memoization | [Memoize] |
Note: The GST framework implements its own aspects in
GST.Core.Aspectsrather than using these official packages. This is because GST needs custom integrations (AspectServiceLocator, ILoggerService, FDA compliance). However, understanding the official patterns is valuable for learning best practices.
Metalama.Patterns.Contracts
Purpose
Contract-based programming: enforce preconditions on parameters, postconditions on return values, and invariants on properties.
Installation
dotnet add package Metalama.Patterns.Contracts
Available Contracts
| Contract | Validates | Example |
|---|---|---|
[NotNull] | Value is not null | void Process([NotNull] string input) |
[NotEmpty] | String/collection is not null or empty | void Save([NotEmpty] string name) |
[Required] | Value is not null/empty/whitespace | [Required] string Title { get; set; } |
[Range(min, max)] | Numeric value within range | void SetTemp([Range(0, 100)] int temp) |
[Positive] | Value > 0 | void Resize([Positive] int width) |
[StrictlyPositive] | Value > 0 (same as Positive) | Alias for [Positive] |
[NonNegative] | Value >= 0 | void SetCount([NonNegative] int count) |
[StrictlyNegative] | Value < 0 | void SetOffset([StrictlyNegative] int offset) |
[Regex(pattern)] | String matches regex | [Regex(@"^\d{3}$")] string Code |
[Url] | Valid URL format | [Url] string Endpoint { get; set; } |
[Email] | Valid email format | [Email] string Contact { get; set; } |
[Phone] | Valid phone format | [Phone] string PhoneNumber { get; set; } |
[CreditCard] | Valid credit card number | [CreditCard] string CardNumber |
[EnumDataType] | Valid enum value | [EnumDataType] MyEnum Value |
[StringLength(min, max)] | String length within range | [StringLength(1, 50)] string Name |
Usage Patterns
Parameter Validation (Preconditions)
using Metalama.Patterns.Contracts;
public class OrderService
{
public Order CreateOrder(
[NotNull] Customer customer,
[NotEmpty] List<OrderItem> items,
[Range(0.01, 999999.99)] decimal total)
{
// All parameters are validated before this line executes
// If validation fails, an appropriate exception is thrown
return new Order(customer, items, total);
}
}
Property Validation (Invariants)
public class Configuration
{
[Range(1, 65535)]
public int Port { get; set; }
[NotEmpty]
public string Host { get; set; }
[Url]
public string Endpoint { get; set; }
}
Return Value Validation (Postconditions)
public class UserRepository
{
[return: NotNull]
public User GetById(int id)
{
var user = _db.Users.Find(id);
return user; // Throws if result is null
}
}
Benefits Over Manual Validation
| Manual | Contract-Based |
|---|---|
if (name == null) throw new ArgumentNullException(nameof(name)); | [NotNull] string name |
| Easy to forget | Cannot be forgotten (applied declaratively) |
| Inconsistent error messages | Standard, consistent messages |
| Not inherited | Contracts inherit from interfaces |
| Verbose | Concise |
Contract Inheritance
Contracts applied on interface methods are automatically inherited by implementing classes:
public interface IRepository<T>
{
T GetById([Positive] int id);
void Save([NotNull] T entity);
}
public class OrderRepository : IRepository<Order>
{
// [Positive] and [NotNull] are automatically enforced here!
public Order GetById(int id) { ... }
public void Save(Order entity) { ... }
}
Metalama.Patterns.Caching
Purpose
Automatically cache method return values based on their arguments. Provides cache invalidation to keep cached data fresh.
Installation
dotnet add package Metalama.Patterns.Caching
Basic Caching
using Metalama.Patterns.Caching.Aspects;
public class ProductService
{
[Cache]
public Product GetProduct(int id)
{
// This will only execute once per unique 'id'
// Subsequent calls return the cached result
return _repository.FindById(id);
}
[Cache]
public async Task<List<Product>> GetProductsByCategoryAsync(string category)
{
// Async methods are fully supported
return await _repository.FindByCategoryAsync(category);
}
}
Cache Invalidation
public class ProductService
{
[Cache]
public Product GetProduct(int id) => _repository.FindById(id);
[InvalidateCache(nameof(GetProduct))]
public void UpdateProduct(int id, Product product)
{
_repository.Update(product);
// Cache for GetProduct(id) is automatically invalidated
}
[InvalidateCache(nameof(GetProduct))]
public void DeleteProduct(int id)
{
_repository.Delete(id);
// Cache for GetProduct(id) is automatically invalidated
}
}
Cache Key Generation
Metalama automatically generates cache keys from:
- The method name (fully qualified)
- All parameter values (using
ToString()or customICacheKey)
For custom objects as parameters, implement ICacheKey:
public class ProductFilter : ICacheKey
{
public string Category { get; set; }
public decimal? MinPrice { get; set; }
public string ToCacheKey() => $"{Category}_{MinPrice}";
}
Cache Backends
| Backend | Package | Use Case |
|---|---|---|
| In-Memory | Built-in (MemoryCache) | Single-server applications |
| Redis | Metalama.Patterns.Caching.Backends.Redis | Distributed, multi-server |
| L1+L2 | Layered configuration | Local memory + Redis fallback |
Configuration
// In Startup.cs / Program.cs
services.AddMetalamaCaching(options =>
{
options.DefaultExpiration = TimeSpan.FromMinutes(10);
options.Profiles.Add("short", new CachingProfile
{
AbsoluteExpiration = TimeSpan.FromMinutes(1)
});
options.Profiles.Add("long", new CachingProfile
{
AbsoluteExpiration = TimeSpan.FromHours(1)
});
});
[Cache(ProfileName = "short")]
public Product GetProduct(int id) { ... }
[Cache(ProfileName = "long")]
public List<Category> GetCategories() { ... }
Metalama.Patterns.Observability
Purpose
Automatically implement INotifyPropertyChanged for WPF/MVVM view models, eliminating boilerplate property notification code.
Installation
dotnet add package Metalama.Patterns.Observability
Basic Usage
using Metalama.Patterns.Observability;
[Observable]
public partial class PersonViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
Important: The class must be
partial.
What Gets Generated
The [Observable] attribute automatically:
- Implements
INotifyPropertyChangedinterface - Adds
PropertyChangedevent - Wraps each property setter with change notification
- Detects computed property dependencies (
FullNamedepends onFirstNameandLastName) - Notifies
FullNamewhenFirstNameorLastNamechanges
// Conceptual generated code:
public partial class PersonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _firstName;
public string FirstName
{
get => _firstName;
set
{
if (!Equals(_firstName, value))
{
_firstName = value;
OnPropertyChanged(nameof(FirstName));
OnPropertyChanged(nameof(FullName)); // Dependency detected!
}
}
}
// ...similar for LastName
}
Handling Complex Dependencies
Child Object Dependencies
[Observable]
public partial class OrderViewModel
{
public Customer Customer { get; set; }
// Depends on Customer.Name — if Customer implements INPC,
// OrderViewModel will forward the notification
public string CustomerName => Customer?.Name ?? "Unknown";
}
Method Dependencies
[Observable]
public partial class CalculatorViewModel
{
public double Price { get; set; }
public int Quantity { get; set; }
// Metalama analyzes GetTotal() and detects it reads Price and Quantity
public double Total => GetTotal();
private double GetTotal() => Price * Quantity;
}
Unsupported Patterns
Some patterns cannot be analyzed. Metalama reports warnings (LAMA51xx):
| Pattern | Issue |
|---|---|
| External method calls | Cannot trace dependencies through external methods |
| Complex LINQ expressions | Cannot statically analyze all LINQ operations |
| Reflection-based access | Cannot trace at compile time |
Metalama.Patterns.Memoization
Purpose
Cache the result of pure, parameterless properties or methods. Simpler than full caching — values are computed once and stored for the lifetime of the object.
Usage
using Metalama.Patterns.Memoization;
public partial class ExpensiveComputation
{
[Memoize]
public double Result => ComputeExpensiveResult();
private double ComputeExpensiveResult()
{
// This only runs once, result is cached in a backing field
Thread.Sleep(5000);
return Math.PI * Math.E;
}
}
Memoize vs. Cache
| Feature | [Memoize] | [Cache] |
|---|---|---|
| Parameters | None (parameterless only) | Any |
| Storage | Instance field | External cache service |
| Lifetime | Object lifetime | Configurable expiration |
| Invalidation | None (immutable) | [InvalidateCache] |
| Use case | Pure computed properties | Service method results |
GST vs. Official Patterns
The GST framework has its own implementations instead of using the official packages:
| Feature | Official Package | GST Implementation | Reason for Custom |
|---|---|---|---|
| Validation | Metalama.Patterns.Contracts | GST.Core.Aspects.Validation | Custom error handling integration |
| Caching | Metalama.Patterns.Caching | GST.Core.Aspects.Caching | Custom ICacheService abstraction |
| Observability | Metalama.Patterns.Observability | GST.Core.Aspects.Observability | Custom logging + DependsOn |
| Logging | None (not provided) | GST.Core.Aspects.Logging | Core need, custom ILoggerService |
| Retry | None | GST.Core.Aspects.Exception | Domain-specific (SECS/GEM, Modbus) |
| Audit | None | GST.Core.Aspects.Audit | FDA 21 CFR Part 11 compliance |
| Authorization | None | GST.Core.Aspects.Authorization | Custom permission model |
Understanding the official patterns helps you:
- Learn Metalama best practices from the framework authors
- Compare approaches when building custom aspects
- Use the official packages for projects that don't need GST's custom integrations
Next: GST Real-World Examples — How the GST framework uses Metalama in production.