Cocoar.Capabilities
0.7.0
dotnet add package Cocoar.Capabilities --version 0.7.0
NuGet\Install-Package Cocoar.Capabilities -Version 0.7.0
<PackageReference Include="Cocoar.Capabilities" Version="0.7.0" />
<PackageVersion Include="Cocoar.Capabilities" Version="0.7.0" />
<PackageReference Include="Cocoar.Capabilities" />
paket add Cocoar.Capabilities --version 0.7.0
#r "nuget: Cocoar.Capabilities, 0.7.0"
#:package Cocoar.Capabilities@0.7.0
#addin nuget:?package=Cocoar.Capabilities&version=0.7.0
#tool nuget:?package=Cocoar.Capabilities&version=0.7.0
Cocoar.Capabilities
A general-purpose capabilities system for .NET that enables type-safe, composable capability attachment to any object. Perfect for cross-project extensibility without circular dependencies.
What is it?
Cocoar.Capabilities implements the Capability Composition pattern - a type-safe, high-performance approach to object extensibility that eliminates circular dependencies and enables cross-project collaboration.
Think of it as a strongly-typed property bag where any library can attach behavior to any object, and consumers can discover and use these capabilities in a predictable, compile-time safe manner.
🌟 Key Benefits
- 🔒 Type Safe: Compile-time guarantees for capability-subject relationships
- ⚡ Zero Allocation: Optimized hot paths with configurable tag indexing
- 🧵 Thread Safe: Immutable by design - no locks needed
- 🔌 Extensible: Cross-library capability tagging and discovery
- 📦 Lightweight: Zero dependencies, AOT-friendly
- 🏷️ Tagged: Group and query capabilities by tags for advanced scenarios
- ⚙️ Configurable: Performance tuning for different bag sizes and usage patterns
Install
dotnet add package Cocoar.Capabilities
Quickstart
using Cocoar.Capabilities;
// 1. Define capabilities for your domain
public record LoggingCapability<T>(LogLevel Level) : ICapability<T>;
public record CachingCapability<T>(TimeSpan Duration) : ICapability<T>;
// 2. Attach capabilities to any object
var userService = new UserService();
var bag = Composer.For(userService)
.Add(new LoggingCapability<UserService>(LogLevel.Debug))
.Add(new CachingCapability<UserService>(TimeSpan.FromMinutes(5)))
.Build();
// 3. Consume capabilities anywhere
if (bag.TryGet<LoggingCapability<UserService>>(out var logging))
{
logger.SetLevel(logging.Level);
}
if (bag.TryGet<CachingCapability<UserService>>(out var caching))
{
ConfigureCache(caching.Duration);
}
📚 Core Concepts
Capabilities
A capability represents a piece of functionality or configuration that can be attached to a subject:
// Generic capability - works with any subject type T
public record MyCapability<T>(string Value) : ICapability<T>;
// Specific capability - only works with UserService
public record UserCapability(int UserId) : ICapability<UserService>;
Subjects
A subject is any object that can have capabilities attached. No special interfaces or base classes required:
// Any class can be a subject
public class UserService { }
public class DatabaseConfig { }
public class PaymentProcessor { }
Capability Bags
A capability bag is an immutable container that stores capabilities for a specific subject:
var bag = Composer.For(myObject)
.Add(new SomeCapability<MyObject>("value"))
.Add(new AnotherCapability<MyObject>(42))
.Build(); // Immutable from this point
Tagged Capabilities
Tagged capabilities enable grouping, filtering, and cross-library identification:
// Define tagged capability
public record LoggingCapability<T>(LogLevel Level) : ITaggedCapability<T>
{
public IReadOnlyCollection<object> Tags => ["Logging", "CrossCutting", typeof(MyLibrary)];
}
// Query by tags
var loggingCaps = bag.GetByTag("Logging");
var librarySpecific = bag.GetByAllTags(["Logging", typeof(MyLibrary)]);
Benefits of Tagging:
- Cross-library coordination - Libraries can find each other's capabilities
- Batch processing - Process all capabilities with specific tags
- Conflict resolution - Group competing capabilities by tags
- Plugin discovery - Find capabilities from specific assemblies/modules
🔧 Essential API
Building Capability Bags
// Create a builder for any object
var builder = Composer.For(myObject);
// Add capabilities by concrete type
builder.Add(new MyCapability<MyObject>("value"));
// Add capabilities by interface/contract (for exact-type retrieval)
builder.AddAs<IMyCapability<MyObject>>(new ConcreteCapability<MyObject>());
// Build immutable bag (one-shot operation)
var bag = builder.Build();
// Convenience: build AND auto-register in the global weak registry
var registered = builder.BuildAndRegister();
Retrieving Capabilities
// Try to get a capability (safe)
if (bag.TryGet<MyCapability<MyObject>>(out var capability))
{
Console.WriteLine(capability.Value);
}
// Get required capability (throws if missing)
var required = bag.GetRequired<MyCapability<MyObject>>();
// Get all capabilities of a type
var allCapabilities = bag.GetAll<MyCapability<MyObject>>();
// Process all capabilities efficiently (zero-allocation iteration)
allCapabilities.ForEach(cap => ProcessCapability(cap));
// Check if capability exists
bool exists = bag.Contains<MyCapability<MyObject>>();
// Tagged capability queries
var taggedCaps = bag.GetAllByTag<MyCapability<MyObject>>("MyTag");
var multiTagCaps = bag.GetByAllTags(["Tag1", "Tag2"]);
// NEW: Cross-type capability queries (any capability type with a tag)
var crossTypeCaps = bag.GetAllWithTag("DI");
crossTypeCaps.ForEach(cap => ProcessForDI(cap)); // Works across capability types
New (alpha): First-class Primary support + DX helpers
This release adds optional, first-class support for “Primary Capabilities” and a few small DX helpers.
- Primary contracts:
IPrimaryCapability<T>
,IPrimaryTypeCapability<T>
,IPrimaryFactoryCapability<T>
- Builder helpers (pre-build):
SetPrimary(...)
,HasPrimary()
,RemovePrimary()
- Bag helpers (read-only):
HasPrimary<T>()
,TryGetPrimary<T>(out ...)
,GetPrimaryOrNull<T>()
,GetRequiredPrimary<T>()
(alias:GetPrimary<T>()
) - Registry convenience:
HasCapability<TSubject,TCap>(subject)
,TryGetCapability<TSubject,TCap>(subject, out cap)
,GetOrDefault<TSubject,TCap>(subject, default)
,GetRequired<TSubject,TCap>(subject)
- Builder batch add:
AddRange(params ICapability<T>[] caps)
Primary example (Cocoar.Configuration-style):
// Define a primary kind (see docs/examples/primary-capabilities.md for full pattern)
public sealed record ConcreteTypePrimary<T>(Type Concrete) : IPrimaryTypeCapability<T>
{
public Type SelectedType => Concrete;
}
// Build with exactly one primary (enforced):
var bag = Composer.For(spec)
.SetPrimary(new ConcreteTypePrimary<ConfigureSpec>(typeof(MyService)))
.AddRange(
new ExposeAsCapability<ConfigureSpec>(typeof(IMyService)),
new SingletonCapability<ConfigureSpec>()
)
.Build();
// Read the primary (throws if none or many):
var primary = bag.GetRequiredPrimary<ConfigureSpec>();
// NEW: Type-specific primary capability retrieval (no casting needed!)
if (bag.TryGetPrimaryAs<IPrimaryTypeCapability<ConfigureSpec>>(out var typeCapability))
{
var implType = typeCapability.SelectedType; // Direct access, no casting
// ... consume
}
// Or get directly with null handling
var typeCap = bag.GetPrimaryAsOrNull<IPrimaryTypeCapability<ConfigureSpec>>();
if (typeCap != null)
{
var implType = typeCap.SelectedType;
}
DX query helpers (when you only have the subject instance):
// Remove TryGet + bag.TryGet boilerplate
var lifetimeCap = CapabilityRegistry.GetOrDefault<ConfigureSpec, ServiceLifetimeCapability<ConfigureSpec>>(
spec, defaultValue: null);
See the dedicated guide: docs/guides/first-class-primary-and-dx-helpers.md
🆕 New APIs (Latest Release)
Enhanced Primary Capability Support
Get strongly-typed primary capabilities without casting:
// OLD: Required type checking and casting
if (bag.TryGetPrimary(out var primary) && primary is IPrimaryTypeCapability<ConfigureSpec> typeCapability)
{
services.AddSingleton(typeCapability.Type, bag.Subject);
}
// NEW: Direct type-specific access
if (bag.TryGetPrimaryAs<IPrimaryTypeCapability<ConfigureSpec>>(out var typeCapability))
{
services.AddSingleton(typeCapability.Type, bag.Subject); // No casting needed!
}
// Additional convenience methods
var typeCap = bag.GetPrimaryAsOrNull<IPrimaryTypeCapability<ConfigureSpec>>();
var requiredTypeCap = bag.GetRequiredPrimaryAs<IPrimaryTypeCapability<ConfigureSpec>>();
Cross-Type Capability Querying
Query capabilities across different types using shared tags - perfect for cross-cutting concerns:
// Get ALL capabilities (any type) that have a specific tag
var diRelevantCaps = bag.GetAllWithTag("DI");
// Process DI-related capabilities regardless of their specific type
diRelevantCaps.ForEach(cap =>
{
switch (cap)
{
case ILifetimeCapability<T> lifetime:
ConfigureLifetime(lifetime);
break;
case IExposeAsCapability<T> exposeAs:
ConfigureContract(exposeAs);
break;
// Handle any capability type that affects DI
}
});
Universal ForEach Extension
Efficient iteration without API explosion or allocations:
// Works with ALL Get* methods that return IReadOnlyList<T>
bag.GetAll<ILifetimeCapability>().ForEach(cap => services.Configure(cap));
bag.GetAllByTag<IServiceCapability>("DI").ForEach(cap => ProcessDI(cap));
bag.GetAllWithTag("Configuration").ForEach(cap => ProcessConfig(cap));
// Zero allocations - uses indexed access instead of .ToList().ForEach()
// One method instead of ForEach, ForEachByTag, ForEachWithTag, etc.
Recommended Pattern: Interface-Based Capability Grouping
For the cleanest API when working with related capabilities, use interface-based registration:
// Define a contract interface
public interface IDIRelevantCapability<T> : ICapability<T> { }
// Implement on your capabilities
public record SingletonCapability<T> : IDIRelevantCapability<T>;
public record ExposeAsCapability<T>(Type ContractType) : IDIRelevantCapability<T>;
// Register using the interface contract
builder.AddAs<IDIRelevantCapability<ConfigureSpec>>(new SingletonCapability<ConfigureSpec>());
builder.AddAs<IDIRelevantCapability<ConfigureSpec>>(new ExposeAsCapability<ConfigureSpec>(typeof(IMyService)));
// Query by interface - clean and type-safe
bag.GetAll<IDIRelevantCapability<ConfigureSpec>>().ForEach(cap => ProcessForDI(cap));
Performance Optimization
Build Options control performance characteristics for different scenarios:
// Small bags - minimize build cost
var smallBag = Composer.For(config)
.Add(capability1)
.Add(capability2)
.Build(CapabilityBagBuildOptions.SmallBag);
// Large bags - optimize for fast queries
var largeBag = Composer.For(service)
.Add(/* many capabilities */)
.Build(new CapabilityBagBuildOptions
{
TagIndexing = TagIndexingMode.Eager, // Pre-build tag indices
IndexMinFrequency = 3 // Only index frequently used tags
});
// Auto mode - let the library decide (default)
var autoBag = builder.Build(); // Uses TagIndexingMode.Auto
Tag Index Modes:
None
- No indexing, fast build (ideal for <20 capabilities)Eager
- Pre-build indices, fast queries (ideal for >100 capabilities)Auto
- Automatic selection based on bag size (default, threshold: 64)
Optional: Global Registry (no signature changes) ✅
If your existing code only returns the subject instance and you can’t thread an ICapabilityBag<T>
through your APIs, enable the optional registry. It weakly associates subjects with their bags via ConditionalWeakTable
, so entries vanish automatically when the subject is GC’d.
Register on build:
var userService = new UserService();
// One-liner: build and auto-register
var bag = Composer.For(userService)
.Add(new LoggingCapability<UserService>(LogLevel.Debug))
.Add(new CachingCapability<UserService>(TimeSpan.FromMinutes(5)))
.BuildAndRegister();
// Or opt-in via build options
var options = new CapabilityBagBuildOptions { AutoRegisterInRegistry = true };
var bag2 = Composer.For(userService)
.Add(new LoggingCapability<UserService>(LogLevel.Info))
.Build(options);
Lookup later from only the subject:
if (CapabilityRegistry.TryGet(userService, out ICapabilityBag<UserService> cached))
{
cached.Use<LoggingCapability<UserService>>(cap => logger.SetLevel(cap.Level));
}
// When subject type is not known at compile time
if (CapabilityService.TryGet(someObject, out ICapabilityBag anyBag))
{
// anyBag.Subject == someObject
}
Notes:
- Reference types only (by reference identity). Value-type subjects are ignored.
- TryGet is O(1) and allocation-free; registration allocates once per subject.
- Default is off to avoid retaining bags you’ll never query—opt-in when you need it.
Plugin/ALC scenarios:
- In standard apps, there’s exactly one global registry (single assembly load = single static).
- In plugin isolation (separate AssemblyLoadContexts or duplicated package versions), each isolated context gets its own static.
- Hosts can unify registries by providing a bridge:
// Early in host startup
CapabilityRegistry.Provider = new MyCrossContextRegistryProvider();
Implement ICapabilityRegistryProvider to forward Register/TryGet/Remove into your host’s shared storage or IPC.
🎯 Real-World Example
// Define configuration-specific capabilities
public record ExposeAsCapability<T>(Type ContractType) : ICapability<T>;
public record SingletonLifetimeCapability<T> : ICapability<T>;
public record HealthCheckCapability<T>(string Name) : ICapability<T>;
// Create configuration with capabilities
var dbConfig = new DatabaseConfig { ConnectionString = "..." };
var configBag = Composer.For(dbConfig)
.Add(new ExposeAsCapability<DatabaseConfig>(typeof(IDbConfig)))
.Add(new SingletonLifetimeCapability<DatabaseConfig>())
.Add(new HealthCheckCapability<DatabaseConfig>("database"))
.Build();
// Process capabilities in your DI registration code
if (configBag.Contains<SingletonLifetimeCapability<DatabaseConfig>>())
{
services.AddSingleton(configBag.Subject);
}
foreach (var expose in configBag.GetAll<ExposeAsCapability<DatabaseConfig>>())
{
services.AddSingleton(expose.ContractType, _ => configBag.Subject);
}
⚡ Performance & Benchmarks
High-Performance Design:
- Zero allocations in hot paths (
Array.Empty<T>()
, type-safe casting) - Optimized tag indexing with configurable strategies
- Thread-safe immutability without locks
- AOT-friendly - no reflection in performance-critical code
Benchmark Results (see src/Cocoar.Capabilities.Benchmarks/
):
Bag Size | Index Mode | Build Time | Single Tag Query | Multi-Tag Query |
---|---|---|---|---|
~20 capabilities | None |
~10 µs | ~120 ns | ~200 ns |
~100 capabilities | Auto |
~45 µs | ~80 ns | ~150 ns |
~1000 capabilities | Eager |
~800 µs | ~25 ns | ~60 ns |
Run Benchmarks:
cd src/Cocoar.Capabilities.Benchmarks
dotnet run -c Release
Performance Guidelines:
- Small bags (≤20): Use
CapabilityBagBuildOptions.SmallBag
orTagIndexing=None
- Medium bags (21-100): Use default
TagIndexing=Auto
- Large bags (≥100): Use
TagIndexing=Eager
for read-heavy scenarios
📖 Documentation
Getting Started
- Getting Started Guide - Your first capability bag in 5 minutes
- Core Concepts - Understanding the architecture and design patterns
- API Reference - Complete API documentation
Examples
- Primary Capabilities - Extensible "exactly one" constraint system
- Plugin Architecture - Cross-assembly capability discovery
- Configuration System - DI integration with validation and health checks
- Web Framework - Declarative routing, auth, and caching
- Event-Driven Architecture - Handler orchestration with ordering and error handling
Guides
- Building Libraries - Using Cocoar.Capabilities as a foundation
- Tagged Capabilities - Processing capabilities in grouped order
- Performance Tuning - Optimization best practices
- First-class Primary + DX helpers - What’s new and how to use it
Reference
- Performance Results - Comprehensive performance analysis
🚨 Important Notes
Exact Type Matching
The system uses exact type matching - it only finds capabilities registered under the exact same type:
// ❌ This won't work
builder.Add(new ConcreteCapability<T>());
bag.TryGet<ICapability<T>>(out _); // Returns false!
// ✅ Use AddAs<T> for interface retrieval
builder.AddAs<ICapability<T>>(new ConcreteCapability<T>());
bag.TryGet<ICapability<T>>(out _); // Returns true!
Builder Lifecycle
Builders are single-use - they become unusable after Build()
:
var builder = Composer.For(myObject);
var bag1 = builder.Build(); // ✅ Works
var bag2 = builder.Build(); // ❌ Throws InvalidOperationException
Thread Safety
- Capability Bags: Thread-safe (immutable)
- Builders: NOT thread-safe (single-threaded use only)
🧪 Testing & Benchmarks
The library includes comprehensive tests and performance benchmarks:
Tests
- Comprehensive unit tests covering core functionality, edge cases, and performance
- 100% coverage on core types
- Thread safety tests for concurrent access
- Tagged capability tests for advanced querying scenarios
dotnet test
Benchmarks
- BenchmarkDotNet suite measuring tag indexing strategies
- Real-world scenarios with different bag sizes and access patterns
- Cross-platform validation on ARM and x64 architectures
cd src/Cocoar.Capabilities.Benchmarks
dotnet run -c Release
Benchmark Focus Areas:
- Tag indexing performance (
None
vsAuto
vsEager
) - Build time vs query time trade-offs
- Memory allocation patterns
- Concurrent access performance
Quality & Reliability
Cocoar.Capabilities is backed by comprehensive testing and performance validation:
• 170+ automated tests covering core functionality, edge cases, and performance scenarios
• 100% coverage on critical paths with thread safety validation
• Continuous integration validates every PR and commit on multiple platforms
• BenchmarkDotNet suite for performance regression detection
• Real-world usage in production systems (Cocoar.Configuration)
Testing Categories:
- Core capability composition and retrieval
- Tagged capability querying and indexing
- Thread safety under concurrent access
- Performance characteristics across different bag sizes
- Edge cases and error handling
� Documentation
Core Concepts:
- Getting Started Guide - Your first capability bag in 5 minutes
- Core Concepts - Understanding the architecture and design patterns
- API Reference - Complete API documentation
Implementation Examples:
- Primary Capabilities - Extensible "exactly one" constraint system
- Plugin Architecture - Cross-assembly capability discovery
- Configuration System - DI integration with validation
- Web Framework - Declarative routing and middleware
- Event-Driven Architecture - Handler orchestration with ordering
- Cross-Context Registry Provider - Unify registry across plugin ALCs
- Host + Plugin (ALC) Sample - Realistic wiring without a separate repo
Advanced Guides:
- Building Libraries - Integration patterns for library authors
- Tagged Capabilities - Processing capabilities in groups
- Performance Tuning - Optimization strategies
Reference:
- Performance Results - Comprehensive benchmarking analysis
Contributing & Versioning
• SemVer versioning - additive MINOR releases, breaking changes in MAJOR
• PRs & issues welcome - Community contributions encouraged
• Apache License 2.0 - Use anywhere, commercial or personal
License & Trademark
This project is licensed under the Apache License, Version 2.0. See NOTICE for attribution.
"Cocoar" and related marks are trademarks of COCOAR e.U. Use of the name in forks or derivatives should preserve attribution and avoid implying official endorsement. See TRADEMARKS for permitted and restricted uses.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
0.7.0 | 35 | 9/30/2025 |
0.6.0 | 81 | 9/29/2025 |
0.5.0 | 84 | 9/29/2025 |
0.4.0 | 78 | 9/29/2025 |
0.4.0-alpha.0 | 39 | 9/28/2025 |
0.2.0-alpha.0 | 42 | 9/28/2025 |
See CHANGELOG.md for detailed release notes.