Fabrics: Bulk Aspect Application
Fabrics are compile-time entry points that let you apply aspects, configure settings, and enforce architecture rules without placing attributes on individual declarations. They execute automatically during compilation.
Why Fabrics?
Consider a project with 200 service methods. Adding [Log] to each one is:
- Tedious: 200 attributes to add manually
- Error-prone: Easy to forget one
- Noisy: Clutters the code with repetitive attributes
- Unmaintainable: Changing the strategy requires modifying 200 files
Fabrics solve this by selecting targets programmatically:
// Apply [Log] to ALL public methods in ALL services — one line of code
public class LoggingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Service"))
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.AddAspectIfEligible<LogAttribute>();
}
}
Fabric Types
ProjectFabric
Scope: The entire project it's defined in.
using Metalama.Framework.Fabrics;
// Must be in a top-level class (not nested)
public class MyProjectFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Apply aspects to any declaration in the project
}
}
Use cases:
- Apply aspects to all methods matching a pattern
- Configure aspect library options
- Define architecture rules (e.g., "controllers must not depend on repositories directly")
TransitiveProjectFabric
Scope: Projects that reference the assembly containing this fabric.
// In a library project (e.g., MyCompany.Aspects)
public class MyTransitiveFabric : TransitiveProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// This runs in EVERY project that references MyCompany.Aspects
amender
.SelectMany(p => p.Types)
.Where(t => t.Attributes.Any(a => a.Type.Name == "ServiceAttribute"))
.SelectMany(t => t.Methods)
.AddAspectIfEligible<LogAttribute>();
}
}
Use cases:
- Enforce company-wide standards across all projects
- Auto-apply aspects from a shared library
- Architecture validation across dependent projects
NamespaceFabric
Scope: The namespace it's defined in.
namespace MyApp.Services;
// Applies to all types in MyApp.Services
public class ServicesFabric : NamespaceFabric
{
public override void AmendNamespace(INamespaceAmender amender)
{
amender
.SelectMany(ns => ns.Types)
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.AddAspectIfEligible<LogAttribute>();
}
}
TypeFabric
Scope: The type it's defined in (as a nested class).
public partial class OrderService
{
// Must be a nested private class
private class Fabric : TypeFabric
{
public override void AmendType(ITypeAmender amender)
{
amender
.SelectMany(t => t.Methods)
.Where(m => m.Name.StartsWith("Get"))
.AddAspectIfEligible<CacheAttribute>();
}
}
public Order GetOrder(int id) { /* automatically cached */ }
public List<Order> GetOrders() { /* automatically cached */ }
public void DeleteOrder(int id) { /* NOT cached */ }
}
Note: The containing class must be
partialfor TypeFabric to work.
Fabric Query API
Fabrics use a LINQ-like fluent API to select targets:
Selecting Types
amender
.SelectMany(p => p.Types) // All types
.Where(t => t.Name.EndsWith("Service")) // Filter by name
.Where(t => t.Accessibility == Accessibility.Public) // Filter by visibility
.Where(t => t.BaseType?.Name == "BaseService") // Filter by base class
.Where(t => t.ImplementedInterfaces.Any( // Filter by interface
i => i.Name == "IRepository"))
Selecting Methods
amender
.SelectMany(p => p.Types)
.SelectMany(t => t.Methods) // All methods
.Where(m => m.Accessibility == Accessibility.Public) // Public only
.Where(m => !m.IsStatic) // Instance only
.Where(m => m.ReturnType.Is(typeof(Task<>))) // Async only
.Where(m => m.Parameters.Count > 0) // Has parameters
Selecting Properties
amender
.SelectMany(p => p.Types)
.SelectMany(t => t.Properties)
.Where(p => p.Writeability == Writeability.All) // Read-write
.Where(p => p.Type.Is(typeof(string))) // String type
Applying Aspects
// Apply with default settings
.AddAspectIfEligible<LogAttribute>();
// Apply with configuration
.AddAspectIfEligible(m => new CacheAttribute
{
AbsoluteExpirationSeconds = 300
});
// Apply only if the aspect is eligible for the target
.AddAspectIfEligible<RetryAttribute>(); // Won't apply to void methods if not eligible
Fabrics vs. Attributes
| Dimension | Attributes | Fabrics |
|---|---|---|
| Granularity | Per-declaration | Bulk (pattern-based) |
| Visibility | Visible at target site | Centralized, separate from targets |
| Configuration | Per-instance | Can apply different configs per pattern |
| Refactoring | Must update each site | Change one query |
| Discovery | Obvious (reading the code) | Less obvious (need to know fabric exists) |
| Reusability | N/A | TransitiveFabric crosses projects |
When to Use Each
| Scenario | Recommendation |
|---|---|
| Few specific methods need an aspect | Attributes |
| All methods in a class/namespace/project | Fabric |
| Different configuration per method | Attributes |
| Uniform policy across the project | Fabric |
| Cross-project standards | TransitiveProjectFabric |
| Aspect library distribution | Both: Fabric for defaults, attributes for overrides |
Practical Examples
Example 1: Auto-Log All Controller Methods
public class ControllerLoggingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.BaseType?.Name == "ControllerBase" ||
t.Attributes.Any(a => a.Type.Name == "ApiControllerAttribute"))
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public &&
!m.IsStatic)
.AddAspectIfEligible(m => new LogAttribute
{
LogParameters = true,
LogReturnValue = true
});
}
}
Example 2: Enforce Not-Null on All Parameters
public class NullCheckFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.SelectMany(p => p.Types)
.Where(t => t.Accessibility == Accessibility.Public)
.SelectMany(t => t.Methods)
.Where(m => m.Accessibility == Accessibility.Public)
.SelectMany(m => m.Parameters)
.Where(p => p.Type.IsNullable != true &&
p.Type.IsReferenceType == true)
.AddAspectIfEligible<NotNullAttribute>();
}
}
Example 3: Configure Caching for Repository Pattern
public class CachingFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Cache all "Get" methods in repositories for 5 minutes
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Repository"))
.SelectMany(t => t.Methods)
.Where(m => m.Name.StartsWith("Get") &&
m.ReturnType.SpecialType != SpecialType.Void)
.AddAspectIfEligible(m => new CacheAttribute
{
AbsoluteExpirationSeconds = 300
});
}
}
Architecture Validation with Fabrics
Fabrics can also enforce architecture rules (without Metalama.Extensions.Architecture):
public class ArchitectureFabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// Warn if a Controller directly references a Repository
amender
.SelectMany(p => p.Types)
.Where(t => t.Name.EndsWith("Controller"))
.SelectMany(t => t.Fields)
.Where(f => f.Type.Name.EndsWith("Repository"))
.ReportDiagnostic(f =>
DiagnosticDescriptor.Create(
"ARCH001",
"Controllers should not directly reference repositories",
DiagnosticSeverity.Warning)
.WithMessage($"Controller uses {f.Type.Name} directly. Use a service layer."));
}
}
GST Framework: Current State
The GST framework currently uses attributes exclusively (no fabrics). This is a conscious design choice:
| Reason | Explanation |
|---|---|
| Explicit control | Each aspect application is visible at the target site |
| Fine-grained configuration | Different methods need different aspect parameters |
| Application-layer flexibility | Applications choose which aspects to apply |
Potential improvement: A TransitiveProjectFabric could auto-apply [NotNull] to all public method parameters, reducing boilerplate in application projects.
Summary
| Fabric Type | Scope | Automatic? | Use Case |
|---|---|---|---|
ProjectFabric | Current project | Yes | Project-wide policies |
TransitiveProjectFabric | Referencing projects | Yes | Cross-project standards |
NamespaceFabric | One namespace | Yes | Namespace-level rules |
TypeFabric | One type (nested) | Yes | Type-level automation |
Next: Pattern Libraries — Built-in patterns for contracts, caching, and observability.