Cocoar.Capabilities 0.7.0

dotnet add package Cocoar.Capabilities --version 0.7.0
                    
NuGet\Install-Package Cocoar.Capabilities -Version 0.7.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cocoar.Capabilities" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cocoar.Capabilities" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="Cocoar.Capabilities" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Cocoar.Capabilities --version 0.7.0
                    
#r "nuget: Cocoar.Capabilities, 0.7.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Cocoar.Capabilities@0.7.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Cocoar.Capabilities&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=Cocoar.Capabilities&version=0.7.0
                    
Install as a Cake Tool

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.

Build (develop) PR Validation License: Apache-2.0 NuGet Downloads

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.

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 or TagIndexing=None
  • Medium bags (21-100): Use default TagIndexing=Auto
  • Large bags (≥100): Use TagIndexing=Eager for read-heavy scenarios

📖 Documentation

Getting Started

Examples

Guides

Reference

🚨 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 vs Auto vs Eager)
  • 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:

Implementation Examples:

Advanced Guides:

Reference:

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.