In Part 1, we saw the problem Capability Composition solves. In Part 2, we learned the core API - Scope, Composer, and Composition.
Now comes the fun part.
My Production Use Case: Cocoar.Configuration
Before we explore possibilities, let's ground this in reality. Here's what I actually built and use in production:
The Problem: In Cocoar.Configuration, users configure types with a fluent API across multiple assemblies:
services.AddCocoarConfiguration(rules, configure =>
{
configure.ConcreteType<AppSettings>()
.AsSingleton() // ← Extension from Cocoar.Configuration.DI
.ExposeAs<IAppSettings>() // ← Core method from Cocoar.Configuration
.DisableAutoRegistration(); // ← Another DI extension
});
The Challenge:
- Core library provides
ConcreteConfigBuilder<T>(doesn't know about DI) - DI assembly adds extension methods that attach metadata (lifetimes, interface mappings)
- Later, the DI registration process needs to read ALL metadata from ALL assemblies
- Builder instances are created dynamically at runtime
- Can't use inheritance, can't add fields to builders, can't use simple dictionaries
The Solution with Capabilities:
// 1. Core builder attaches primary capability (the type being configured)
public sealed class ConcreteConfigBuilder<T> : ConfigBuilder
{
internal ConcreteConfigBuilder(CapabilityScope scope) : base(scope)
{
scope.For(this).WithPrimary(new ConcreteTypePrimary<ConfigBuilder>(typeof(T)));
}
}
// 2. DI extension methods (different assembly!) attach secondary capabilities
public static ConcreteConfigBuilder<T> AsSingleton<T>(this ConcreteConfigBuilder<T> builder)
{
ConfigBuilder.GetComposer(builder)
.Add(new ServiceLifetimeCapability<ConfigBuilder>(ServiceLifetime.Singleton, null));
return builder;
}
public static ConcreteConfigBuilder<T> ExposeAs<TInterface>(this ConcreteConfigBuilder<T> builder)
{
ConfigBuilder.GetComposer(builder)
.Add(new ExposeAsCapability<ConfigBuilder>(typeof(TInterface)));
return builder;
}
// 3. DI registration retrieves and processes all capabilities
public static IServiceCollection AddCocoarConfiguration(
this IServiceCollection services,
ConfigManager configManager)
{
foreach (var spec in configManager.Exposures)
{
if (!configManager.CapabilityScope.Compositions.TryGet(spec, out var composition))
continue;
// Get the primary type
if (composition.TryGetPrimaryAs<ConcreteTypePrimary<ConfigBuilder>>(out var typeCapability))
{
var concreteType = typeCapability.SelectedType;
// Check for flags
if (composition.Has<DisableAutoRegistrationCapability<ConfigBuilder>>())
continue; // Skip this one
// Get all lifetimes and exposures
var lifetimes = composition.GetAll<ServiceLifetimeCapability<ConfigBuilder>>();
var exposures = composition.GetAll<ExposeAsCapability<ConfigBuilder>>();
// Register services with DI container
ProcessServiceRegistration(services, concreteType, lifetimes, exposures);
}
}
}
This is real production code. It solves the cross-assembly metadata propagation problem that was blocking the Configure API implementation.
Everything else in this article explores what else becomes possible with this same pattern.
🎯 What This Article Is (And Isn't)
My production experience: Cocoar.Configuration - enriching configuration builders with metadata across assemblies (shown above).
This article: Thought experiments showing what else becomes possible with this pattern.
What you'll find here:
- ✅ Showcase architectural possibilities
- ✅ Stretch your mental model
- ✅ Inspire solutions for future problems
- ✅ Patterns that show the flexibility of the approach
What you WON'T find:
- ❌ "Best practices from production"
- ❌ "You should use this approach"
- ❌ Fully-vetted production code for every example
- ❌ Prescriptive solutions
Many of these could be solved more simply with other approaches. The goal is to open your mind to new architectural options, not prescribe solutions.
When you hit a problem where you need to attach metadata across assemblies, compose behavior dynamically, or extend types you don't own - you'll think: "Hey, capabilities could work here!"
Don't cargo-cult these patterns. Understand them, then use what makes sense for your context.
We'll explore:
- Framework-free middleware pipelines
- Enriching enums and primitives with metadata
- Ordered execution without framework coupling
- Extension fields for game development
- Multi-tenant isolation and plugin architectures
Let's dive in.
Pattern 1: Framework-Free Ordered Pipelines 🔥
The Idea: What if you want ASP.NET Core-style middleware pipelines, but:
- You're in a class library (no HTTP context)
- You're building a background service
- You don't want framework dependencies
The Traditional Approach:
// Tightly coupled to ASP.NET Core
app.Use(async (context, next) =>
{
await LogRequest(context);
await next();
});
A Capability Composition Approach:
public record RequestContext(string UserId, Dictionary<string, object> Data);
// Create a pipeline definition object
public class RequestPipeline
{
public string Name { get; init; } = "Standard Request Pipeline";
}
public class PipelineService
{
private readonly CapabilityScope _scope = new();
private readonly RequestPipeline _pipelineDefinition = new();
public void Initialize()
{
// Define your pipeline once at startup with explicit ordering
_scope.For(_pipelineDefinition)
.Add<Func<RequestContext, Task>>(
async ctx => await AuthenticateUser(ctx),
order: 1)
.Add<Func<RequestContext, Task>>(
async ctx => await ValidateInput(ctx),
order: 2)
.Add<Func<RequestContext, Task>>(
async ctx => await ExecuteBusinessLogic(ctx),
order: 3)
.Add<Func<RequestContext, Task>>(
async ctx => await AuditAction(ctx),
order: 4)
.Build();
}
// Execute the pipeline for each request
public async Task ProcessRequest(RequestContext context)
{
var pipeline = _scope.Compositions.GetRequired(_pipelineDefinition)
.GetAll<Func<RequestContext, Task>>();
foreach (var step in pipeline) // Guaranteed order!
{
await step(context);
}
}
}
Why use an object as subject? Because the pipeline definition is set up once and reused many times. The object acts as the identifier for this pipeline configuration. When the object is garbage collected (e.g., when the service is disposed), the registry entry is automatically cleaned up.
What This Would Give You:
✅ Zero framework dependencies - Pure .NET, works anywhere
✅ Guaranteed execution order - Lower numbers run first
✅ Type-safe - Full compile-time checking
✅ Composable - Different pipelines for different scenarios
✅ Testable - Each step is independently testable
Different Pipelines for Different Scenarios:
public class MessageProcessingService
{
private readonly CapabilityScope _scope = new();
private readonly object _pipelineDefinition = new();
public void Initialize()
{
// Message processing pipeline
_scope.For(_pipelineDefinition)
.Add<Func<Message, Task>>(msg => ValidateSchema(msg), order: 1)
.Add<Func<Message, Task>>(msg => EnrichMetadata(msg), order: 2)
.Add<Func<Message, Task>>(msg => RouteToHandler(msg), order: 3)
.Build();
}
public async Task ProcessMessage(Message message)
{
var pipeline = _scope.Compositions.GetRequired(_pipelineDefinition)
.GetAll<Func<Message, Task>>();
foreach (var step in pipeline)
{
await step(message);
}
}
}
// Or use a dedicated pipeline configuration class for clarity
public class ValidationPipelineConfig { }
public class UserValidator
{
private readonly CapabilityScope _scope = new();
private readonly ValidationPipelineConfig _config = new();
public void Initialize()
{
// Validation pipeline (cheap validators first, expensive last!)
_scope.For(_config)
.Add<Func<User, ValidationResult>>(u => ValidateRequired(u), order: 1)
.Add<Func<User, ValidationResult>>(u => ValidateFormat(u), order: 2)
.Add<Func<User, ValidationResult>>(u => ValidateBusinessRules(u), order: 3)
.Add<Func<User, ValidationResult>>(u => CheckDatabase(u), order: 4)
.Build();
}
}
The Key Insight: Functions are capabilities too! When combined with ordering, you get pipeline orchestration without any framework.
Note on subject choice: Use an object instance as the subject (not a string) because:
- The pipeline definition is set up once and reused
- Reference types automatically clean up from the registry when no longer needed
- It's clearer what the subject represents (a pipeline configuration, not arbitrary metadata)
Pattern 2: Enriching Enums with Metadata 🏷️
The Idea: You have error codes or status enums, and you need to attach display messages, severity levels, and handlers - but you can't modify the enum (it's from a third-party library, or it's domain code that shouldn't know about UI).
The Traditional Approach:
// Giant switch statement scattered everywhere
string GetErrorMessage(ErrorCode code)
{
switch (code)
{
case ErrorCode.InvalidInput: return "Invalid input provided";
case ErrorCode.Timeout: return "Request timed out";
// ... 50 more cases
}
}
// Repeated in multiple places with slightly different variations
A Capability Composition Approach:
public enum ErrorCode { InvalidInput, Timeout, Unauthorized, NotFound }
public record DisplayCapability(string Message);
public record SeverityCapability(LogLevel Level);
public record ResponseCapability(int HttpStatus);
// Initialize once, during application startup
public void InitializeErrorMetadata()
{
_scope.For(ErrorCode.InvalidInput)
.Add(new DisplayCapability("The input provided was invalid"))
.Add(new SeverityCapability(LogLevel.Warning))
.Add(new ResponseCapability(400))
.Build();
_scope.For(ErrorCode.Timeout)
.Add(new DisplayCapability("The request timed out"))
.Add(new SeverityCapability(LogLevel.Error))
.Add(new ResponseCapability(504))
.Build();
_scope.For(ErrorCode.Unauthorized)
.Add(new DisplayCapability("You are not authorized"))
.Add(new SeverityCapability(LogLevel.Warning))
.Add(new ResponseCapability(401))
.Build();
}
// Use anywhere in your application
public void HandleError(ErrorCode code)
{
var composition = _scope.Compositions.Get(code);
if (composition == null) return;
var display = composition.GetFirstOrDefault<DisplayCapability>();
var severity = composition.GetFirstOrDefault<SeverityCapability>();
var response = composition.GetFirstOrDefault<ResponseCapability>();
Logger.Log(severity.Level, display.Message);
HttpContext.Response.StatusCode = response.HttpStatus;
}
What This Would Give You:
✅ Centralized metadata - One place to define all error information
✅ No switch statements - Metadata lookup instead
✅ Type-safe - Each capability is strongly typed
✅ Extensible - Add new capability types without changing existing code
✅ Domain separation - Domain enums stay clean, UI concerns live elsewhere
Remember from Part 2: Enum compositions persist in the registry for the application lifetime. This is a feature! You initialize once at startup, and the metadata is always available.
Advanced Pattern - Action Capabilities:
// Attach behavior directly to enum values
_scope.For(ErrorCode.Timeout)
.Add(new DisplayCapability("Request timed out"))
.Add<Action<HttpContext>>(ctx => RenderTimeoutPage(ctx))
.Build();
_scope.For(ErrorCode.NotFound)
.Add(new DisplayCapability("Not found"))
.Add<Action<HttpContext>>(ctx => RenderNotFoundPage(ctx))
.Build();
// Execute enum-specific behavior
var composition = _scope.Compositions.Get(errorCode);
var handler = composition?.GetFirstOrDefault<Action<HttpContext>>();
handler?.Invoke(httpContext);
Pattern 3: Capability Ordering Beyond Pipelines ⬆️
Ordering isn't just for pipelines - it's useful whenever you have multiple capabilities of the same type and execution order matters.
Event Handlers with Priority:
scope.For("OrderPlaced")
// Critical - must succeed
.Add<Func<Order, Task>>(ValidateOrder, order: 100)
.Add<Func<Order, Task>>(ChargePayment, order: 100)
// Important - should succeed
.Add<Func<Order, Task>>(UpdateInventory, order: 200)
.Add<Func<Order, Task>>(ReserveShipment, order: 200)
// Nice to have - can fail gracefully
.Add<Func<Order, Task>>(SendConfirmationEmail, order: 300)
.Add<Func<Order, Task>>(UpdateAnalytics, order: 300)
.Build();
Validation with Fail-Fast:
// Order: cheap validations first, expensive last
scope.For(formData)
.Add(new ValidationCapability("Required", ValidateRequired), order: 1)
.Add(new ValidationCapability("Format", ValidateFormat), order: 2)
.Add(new ValidationCapability("Business", ValidateBusinessRules), order: 3)
.Add(new ValidationCapability("Database", CheckDatabase), order: 4)
.Build();
// Execute until first failure
var validators = composition.GetAll<ValidationCapability>();
foreach (var validator in validators) // Returns in order
{
if (!validator.IsValid(data))
{
return ValidationResult.Fail(validator.Name);
}
}
Property-Based Ordering:
public record TaskCapability(string Name, int Priority);
scope.For(taskManager)
.Add(new TaskCapability("Critical", 1), cap => cap.Priority)
.Add(new TaskCapability("High", 5), cap => cap.Priority)
.Add(new TaskCapability("Normal", 10), cap => cap.Priority)
.Build();
// Tasks returned in priority order
var tasks = composition.GetAll<TaskCapability>();
Pattern 4: Multi-Tenant Context Isolation 🏢
The Idea: Different tenants need different metadata for the same identifiers.
The Solution: Each tenant gets its own scope!
public class MultiTenantService
{
private readonly Dictionary<string, CapabilityScope> _tenantScopes = new();
public void RegisterTenant(string tenantId)
{
var scope = new CapabilityScope();
_tenantScopes[tenantId] = scope;
// Same enum = different meaning per tenant!
scope.For(ErrorCode.InvalidInput)
.Add(new DisplayCapability(GetTenantMessage(tenantId)))
.Add(new SeverityCapability(GetTenantSeverity(tenantId)))
.Build();
// Tenant-specific configuration
scope.For("MaxUploadSize")
.Add(new LimitCapability(GetTenantLimit(tenantId)))
.Build();
scope.For("BrandColors")
.Add(new ThemeCapability(GetTenantTheme(tenantId)))
.Build();
}
public void ProcessForTenant(string tenantId, ErrorCode error)
{
var scope = _tenantScopes[tenantId];
var composition = scope.Compositions.Get(error);
// Tenant-specific error handling
var display = composition?.GetFirst<DisplayCapability>();
Console.WriteLine(display?.Message); // Tenant-specific message!
}
}
What This Would Give You:
✅ Complete isolation - Tenants can't interfere with each other
✅ Same API - No special multi-tenant handling in business logic
✅ Value type power - Enums and strings work perfectly as stable identifiers
Pattern 5: Plugin Architecture with Self-Description 🔌
The Idea: Build a plugin system where plugins can describe their own capabilities.
public interface IPlugin
{
void RegisterCapabilities(CapabilityScope scope);
}
public class EmailPlugin : IPlugin
{
public void RegisterCapabilities(CapabilityScope scope)
{
scope.For(this)
.WithPrimary(new PluginMetadata(
name: "Email Plugin",
version: "1.2.0",
author: "Acme Corp"))
.AddAs<(IEmailSender, INotificationProvider)>(new EmailService())
.Add(new DependencyCapability("SMTP", "1.0+"))
.Add(new FeatureCapability("Templates"))
.Add(new FeatureCapability("Attachments"))
.Build();
}
}
// Plugin host discovers capabilities
public class PluginHost
{
private readonly CapabilityScope _scope = new();
private readonly List<IPlugin> _plugins = new();
public void LoadPlugin(IPlugin plugin)
{
plugin.RegisterCapabilities(_scope);
_plugins.Add(plugin);
}
// Query plugins by capability
public IEnumerable<T> GetCapabilities<T>()
{
return _plugins
.Select(p => _scope.Compositions.Get(p))
.Where(c => c != null)
.SelectMany(c => c.GetAll<T>());
}
// Find plugins with specific features
public IEnumerable<IPlugin> GetPluginsWithFeature(string feature)
{
return _plugins
.Where(p => {
var comp = _scope.Compositions.Get(p);
var features = comp?.GetAll<FeatureCapability>() ?? [];
return features.Any(f => f.Name == feature);
});
}
// Check dependencies
public bool ValidatePlugin(IPlugin plugin)
{
var comp = _scope.Compositions.Get(plugin);
var deps = comp?.GetAll<DependencyCapability>() ?? [];
return deps.All(dep => IsDepSatisfied(dep));
}
}
What Makes This Interesting:
✅ Self-describing - Plugins declare their own capabilities
✅ Discoverable - Query plugins by any capability
✅ Type-safe - No casting or reflection needed
✅ Flexible - Easy to add new capability types
Pattern 6: Extension Fields for Game Development 🎮
The Idea: You have objects you can't modify (third-party classes, framework types) and you need to attach mutable state - data that changes over time, not just readonly metadata.
Key Insight: Capabilities aren't just data bags - they can have mutable state AND methods! This is essentially Entity Component System (ECS) without the framework.
Example: Adding Combat Stats to a Third-Party Enemy Class
// Third-party class you can't modify
public class Enemy
{
public string Name { get; }
public int AttackPower { get; }
public void Attack()
{
Console.WriteLine($"{Name} attacks for {AttackPower} damage!");
}
}
// Capability with MUTABLE state and methods
public class CombatModifiers
{
public float AttackMultiplier { get; set; } = 1.0f;
public List<string> ActiveEffects { get; } = new();
public void ApplyBuff(float boost, string effectName)
{
AttackMultiplier += boost;
ActiveEffects.Add(effectName);
}
public int CalculateModifiedDamage(int baseDamage)
=> (int)(baseDamage * AttackMultiplier);
}
// Extension method for ergonomic usage
public static class EnemyExtensions
{
private static readonly CapabilityScope _scope = new();
public static void EnhancedAttack(this Enemy enemy, Enemy target)
{
// Get or create capability
var composition = _scope.Compositions.Get(enemy)
?? _scope.For(enemy).Add(new CombatModifiers()).Build();
var modifiers = composition.GetFirst<CombatModifiers>();
int damage = modifiers.CalculateModifiedDamage(enemy.AttackPower);
Console.WriteLine($"{enemy.Name} attacks for {damage} damage!");
}
public static void ApplyBuff(this Enemy enemy, float boost)
{
var composition = _scope.Compositions.Get(enemy)
?? _scope.For(enemy).Add(new CombatModifiers()).Build();
var modifiers = composition.GetFirst<CombatModifiers>();
modifiers.ApplyBuff(boost, "Buffed");
Console.WriteLine($"{enemy.Name} buffed! Multiplier: {modifiers.AttackMultiplier:F2}");
}
}
var orc = new Enemy("Orc", 10);
orc.Attack(); // Orc attacks for 10 damage!
orc.ApplyBuff(0.5f); // Orc buffed! Multiplier: 1.50
orc.EnhancedAttack(goblin); // Orc attacks for 15 damage!
This is ECS without the framework:
- Entity = The subject (Enemy instance)
- Components = Capabilities (CombatModifiers, StatusEffects, etc.)
- Systems = Your code that operates on capabilities
What This Would Give You:
✅ No source code modification - Extend third-party types freely
✅ Mutable state - Data changes over time (buffs, stats, effects)
✅ Methods on capabilities - Encapsulated behavior (ApplyBuff, CalculateModifiedDamage)
✅ Automatic memory management - When enemy is GC'd, capabilities are too (ConditionalWeakTable)
✅ No inheritance hierarchy - Compose behavior dynamically
✅ Multiple systems - Different systems can add different capabilities independently
Game Development Use Cases:
- RPG Stats: Health, mana, buffs, debuffs, equipment modifiers
- AI State: Behavior trees, blackboards, decision-making data
- Physics Extensions: Custom collision data, particle effects, force modifiers
- Rendering Metadata: LOD info, culling data, shader parameters
- Audio Context: 3D positioning, reverb zones, sound profiles
- Networking State: Sync flags, ownership, replication metadata
General Patterns: Scope Lifetime & Error Handling
These patterns apply whether you use capabilities for configuration builders (like I do) or for any of the other scenarios.
Pattern: Scope Lifetime Strategy
// ✅ Application-wide scope (singleton)
public class Application
{
private readonly CapabilityScope _appScope = new();
// Use for:
// - Enum metadata
// - Configuration strings
// - Application constants
}
// ✅ Per-tenant scope
public class TenantManager
{
private readonly Dictionary<string, CapabilityScope> _tenantScopes = new();
// Use for:
// - Tenant-specific metadata
// - Context isolation
}
// ✅ Per-request scope
public void HandleRequest(Request request)
{
using var requestScope = new CapabilityScope();
// Use for:
// - Request-specific capabilities
// - Automatic cleanup
}
// ✅ Per-test scope
[Test]
public void MyTest()
{
using var testScope = new CapabilityScope();
// Use for:
// - Test isolation
// - Clean slate per test
}
Pattern: Registry Decision Matrix
| Scenario | UseCompositionRegistry | When |
|---|---|---|
| Build once, use many places | ✅ Enabled (default) | Most common |
| Build & use in same method | ❌ Disabled | Performance-critical |
| Pass composition directly | ❌ Disabled | Explicit data flow |
| Lookup by subject needed | ✅ Enabled | Convenience |
Pattern: Error Handling
// Use GetRequired when capability MUST exist
var editor = composition.GetRequiredFirst<EditCapability>();
// Use TryGet when capability is optional
if (composition.TryGetFirst<PrintCapability>(out var printer))
{
printer.Print(document);
}
// Use Get with null check for registries
var comp = scope.Compositions.Get(document);
if (comp == null)
{
// Handle missing composition
throw new InvalidOperationException("Document not registered");
}
Summary: When to Consider Each Pattern
| Pattern | Could Work For | Key Characteristic |
|---|---|---|
| Ordered Pipelines | Middleware, validation, processing | Framework-free orchestration |
| Enum Enrichment | Error codes, status values | Centralized metadata |
| Capability Ordering | Priority execution, event handlers | Guaranteed order |
| Multi-Tenant | SaaS applications | Complete isolation |
| Plugin Architecture | Extensibility systems | Self-describing capabilities |
| Extension Fields | Game dev, extending third-party types | ECS without framework |
What Makes This Pattern Different
After using Capability Composition in production (for Cocoar.Configuration), here's what makes it unique:
It's not just one thing - it's the combination:
- Works with any type (objects, enums, strings, functions)
- Ordered execution built-in
- Cross-assembly extensibility
- Type-safe throughout
- Zero dependencies
- Immutable by default
- Thread-safe without locks
No other pattern gives you all of these together.
Final Thoughts
I extracted this pattern from Cocoar.Configuration to solve a specific problem: attaching metadata to configuration builders across assembly boundaries. Extension methods from the DI assembly needed to add lifetime information, interface mappings, and flags - all without the core library knowing about DI.
That's my real production use case. Configuration builders with cross-assembly metadata.
But once you have that foundation - once you can attach typed metadata to any object and retrieve it later - you start seeing other possibilities:
- Could this work for framework-free pipelines?
- What about multi-tenant isolation with separate scopes?
- Extension fields for game development?
- Plugin systems with self-describing capabilities?
I haven't built most of these patterns in production. They're explorations based on what the core mechanism enables. Some might be perfect for your scenario. Some might be overkill. Some definitely have simpler alternatives.
The real value isn't that you should use all these patterns. It's that when you hit a problem where you need to:
- Attach metadata across assemblies
- Compose behavior dynamically
- Extend types you don't own
- Build plugin architectures
...you have this pattern available. Not a silver bullet, just another tool in your architectural toolkit.
Try It Yourself
GitHub: github.com/cocoar-dev/Cocoar.Capabilities
NuGet: Cocoar.Capabilities
Real-world usage: Cocoar.Configuration (the production use case)
Have a scenario where this might fit? I'd love to hear about your architectural challenges and whether Capability Composition could help solve them.
This is Part 3 (final) in the Capability Composition series.
← Part 1: The Cross-Assembly Metadata Problem
← Part 2: Building with Capabilities
Top comments (0)