Skip to main content

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 partial for 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

DimensionAttributesFabrics
GranularityPer-declarationBulk (pattern-based)
VisibilityVisible at target siteCentralized, separate from targets
ConfigurationPer-instanceCan apply different configs per pattern
RefactoringMust update each siteChange one query
DiscoveryObvious (reading the code)Less obvious (need to know fabric exists)
ReusabilityN/ATransitiveFabric crosses projects

When to Use Each

ScenarioRecommendation
Few specific methods need an aspectAttributes
All methods in a class/namespace/projectFabric
Different configuration per methodAttributes
Uniform policy across the projectFabric
Cross-project standardsTransitiveProjectFabric
Aspect library distributionBoth: 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:

ReasonExplanation
Explicit controlEach aspect application is visible at the target site
Fine-grained configurationDifferent methods need different aspect parameters
Application-layer flexibilityApplications 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 TypeScopeAutomatic?Use Case
ProjectFabricCurrent projectYesProject-wide policies
TransitiveProjectFabricReferencing projectsYesCross-project standards
NamespaceFabricOne namespaceYesNamespace-level rules
TypeFabricOne type (nested)YesType-level automation

Next: Pattern Libraries — Built-in patterns for contracts, caching, and observability.