Skip to main content

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

PackagePurposeKey Aspects
Metalama.Patterns.ContractsInput validation[NotNull], [NotEmpty], [Range], [Positive], [Regex], [Url], [Email]
Metalama.Patterns.CachingMethod result caching[Cache], [InvalidateCache]
Metalama.Patterns.ObservabilityMVVM property notification[Observable]
Metalama.Patterns.MemoizationPure function memoization[Memoize]

Note: The GST framework implements its own aspects in GST.Core.Aspects rather 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

ContractValidatesExample
[NotNull]Value is not nullvoid Process([NotNull] string input)
[NotEmpty]String/collection is not null or emptyvoid Save([NotEmpty] string name)
[Required]Value is not null/empty/whitespace[Required] string Title { get; set; }
[Range(min, max)]Numeric value within rangevoid SetTemp([Range(0, 100)] int temp)
[Positive]Value > 0void Resize([Positive] int width)
[StrictlyPositive]Value > 0 (same as Positive)Alias for [Positive]
[NonNegative]Value >= 0void SetCount([NonNegative] int count)
[StrictlyNegative]Value < 0void 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

ManualContract-Based
if (name == null) throw new ArgumentNullException(nameof(name));[NotNull] string name
Easy to forgetCannot be forgotten (applied declaratively)
Inconsistent error messagesStandard, consistent messages
Not inheritedContracts inherit from interfaces
VerboseConcise

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:

  1. The method name (fully qualified)
  2. All parameter values (using ToString() or custom ICacheKey)

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

BackendPackageUse Case
In-MemoryBuilt-in (MemoryCache)Single-server applications
RedisMetalama.Patterns.Caching.Backends.RedisDistributed, multi-server
L1+L2Layered configurationLocal 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:

  1. Implements INotifyPropertyChanged interface
  2. Adds PropertyChanged event
  3. Wraps each property setter with change notification
  4. Detects computed property dependencies (FullName depends on FirstName and LastName)
  5. Notifies FullName when FirstName or LastName changes
// 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):

PatternIssue
External method callsCannot trace dependencies through external methods
Complex LINQ expressionsCannot statically analyze all LINQ operations
Reflection-based accessCannot 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]
ParametersNone (parameterless only)Any
StorageInstance fieldExternal cache service
LifetimeObject lifetimeConfigurable expiration
InvalidationNone (immutable)[InvalidateCache]
Use casePure computed propertiesService method results

GST vs. Official Patterns

The GST framework has its own implementations instead of using the official packages:

FeatureOfficial PackageGST ImplementationReason for Custom
ValidationMetalama.Patterns.ContractsGST.Core.Aspects.ValidationCustom error handling integration
CachingMetalama.Patterns.CachingGST.Core.Aspects.CachingCustom ICacheService abstraction
ObservabilityMetalama.Patterns.ObservabilityGST.Core.Aspects.ObservabilityCustom logging + DependsOn
LoggingNone (not provided)GST.Core.Aspects.LoggingCore need, custom ILoggerService
RetryNoneGST.Core.Aspects.ExceptionDomain-specific (SECS/GEM, Modbus)
AuditNoneGST.Core.Aspects.AuditFDA 21 CFR Part 11 compliance
AuthorizationNoneGST.Core.Aspects.AuthorizationCustom permission model

Understanding the official patterns helps you:

  1. Learn Metalama best practices from the framework authors
  2. Compare approaches when building custom aspects
  3. 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.