From 944eaafd253eed04bd2eff48a9db76d28dee104e Mon Sep 17 00:00:00 2001 From: Bernhard Windisch Date: Wed, 8 Oct 2025 18:43:15 +0200 Subject: [PATCH 1/2] change to non generic --- CHANGELOG.md | 128 --- README-SIMPLE.md | 170 ---- README.md | 330 ++------ docs/DOCUMENTATION-REVIEW-SUMMARY.md | 119 --- docs/RELEASE-NOTES-v0.11.0.md | 284 ------- docs/api-reference.md | 591 ++++++-------- docs/boolean-flag-usage-examples.md | 145 ---- docs/complete-public-api-reference.md | 326 -------- docs/core-concepts.md | 301 -------- docs/examples.md | 729 ++++++++++++++++++ docs/examples/configuration-system.md | 539 ------------- docs/getting-started.md | 250 ------ docs/guides/capability-ordering.md | 473 ------------ docs/guides/memory-management.md | 264 ------- docs/guides/pattern-cookbook.md | 377 --------- docs/guides/performance-optimization.md | 410 ---------- docs/guides/primary-capabilities.md | 462 ----------- docs/guides/tuple-contracts.md | 215 ------ docs/hybrid-initialization-optimization.md | 218 ------ docs/lazy-initialization-analysis.md | 181 ----- docs/lifecycle-and-disposal.md | 49 -- docs/performance-analysis.md | 160 ---- docs/registration-and-querying.md | 204 ----- docs/static-api-migration-strategy.md | 241 ------ .../CanonicalizationBenchmarks.cs | 4 +- .../CapabilityBenchmarks.cs | 34 +- .../CoreVsRegistryBenchmarks.cs | 38 +- .../OrderingBenchmarks.cs | 34 +- .../RecompositionBenchmarks.cs | 12 +- .../CapabilityEntryTests.cs | 6 +- .../ComposerPrimaryNegativeTests.cs | 14 +- .../ConfigTests/ConcreteConfigBuilder.cs | 36 + .../ConfigTests/ConfigurationTests.cs | 44 ++ .../ConfigTests/ConfigureBuilder.cs | 29 + .../ConfigTests/ConfigureSpec.cs | 21 + .../CustomStringMapperTests.cs | 2 +- .../FuncOrderingTests.cs | 126 +++ .../NegativeInvariantTests.cs | 2 +- .../OrderingTests.cs | 34 +- .../PrimaryCapabilityTests.cs | 16 +- .../RegistryApiTests.cs | 2 +- .../SingleTestApproach.cs | 2 +- .../StringSubjectValueSemanticsTests.cs | 2 +- .../SubjectKeyCanonicalizerTests.cs | 2 +- src/Cocoar.Capabilities.Tests/TestHelpers.cs | 46 +- .../TupleTypeExtractorNegativeTests.cs | 6 +- .../ValueTypeRegistryTests.cs | 2 +- .../CapabilityArrayBuilder.cs | 12 +- src/Cocoar.Capabilities/CapabilityEntry.cs | 30 +- src/Cocoar.Capabilities/CapabilityOrdering.cs | 22 +- src/Cocoar.Capabilities/CapabilityScope.cs | 8 +- src/Cocoar.Capabilities/CapabilityStore.cs | 42 +- src/Cocoar.Capabilities/Composer.cs | 156 ++-- .../ComposerRegistryApi.cs | 8 +- src/Cocoar.Capabilities/Composition.cs | 68 +- .../CompositionRegistryApi.cs | 8 +- .../DefaultCapabilityRegistry.cs | 55 +- src/Cocoar.Capabilities/ICapability.cs | 15 +- .../ICapabilityRegistry.cs | 13 +- src/Cocoar.Capabilities/IComposerRegistry.cs | 4 +- src/Cocoar.Capabilities/IComposition.cs | 29 +- .../ICompositionRegistry.cs | 4 +- src/Cocoar.Capabilities/TupleTypeExtractor.cs | 14 +- 63 files changed, 1658 insertions(+), 6510 deletions(-) delete mode 100644 README-SIMPLE.md delete mode 100644 docs/DOCUMENTATION-REVIEW-SUMMARY.md delete mode 100644 docs/RELEASE-NOTES-v0.11.0.md delete mode 100644 docs/boolean-flag-usage-examples.md delete mode 100644 docs/complete-public-api-reference.md delete mode 100644 docs/core-concepts.md create mode 100644 docs/examples.md delete mode 100644 docs/examples/configuration-system.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/guides/capability-ordering.md delete mode 100644 docs/guides/memory-management.md delete mode 100644 docs/guides/pattern-cookbook.md delete mode 100644 docs/guides/performance-optimization.md delete mode 100644 docs/guides/primary-capabilities.md delete mode 100644 docs/guides/tuple-contracts.md delete mode 100644 docs/hybrid-initialization-optimization.md delete mode 100644 docs/lazy-initialization-analysis.md delete mode 100644 docs/lifecycle-and-disposal.md delete mode 100644 docs/performance-analysis.md delete mode 100644 docs/registration-and-querying.md delete mode 100644 docs/static-api-migration-strategy.md create mode 100644 src/Cocoar.Capabilities.Tests/ConfigTests/ConcreteConfigBuilder.cs create mode 100644 src/Cocoar.Capabilities.Tests/ConfigTests/ConfigurationTests.cs create mode 100644 src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureBuilder.cs create mode 100644 src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureSpec.cs create mode 100644 src/Cocoar.Capabilities.Tests/FuncOrderingTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4fd52..6187ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,131 +5,3 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.11.0] - 2024-10-04 - -### 🚨 BREAKING CHANGES - -**Major Architecture Refactor**: Replaced static `Composer.For()` API with scoped `CapabilityScope` pattern for better resource management and testability. - -**Before:** -```csharp -var composition = Composer.For(subject).Add(capability).Build(); -``` - -**After:** -```csharp -using var scope = new CapabilityScope(); -var composition = scope.For(subject).Add(capability).Build(); -``` - -**Package Changes:** -- Removed `Cocoar.Capabilities.Core` package -- Consolidated all functionality into `Cocoar.Capabilities` (~28KB) - -**Migration:** See `docs/static-api-migration-strategy.md` for upgrade guide. - -### Added -- `CapabilityScope` - Central scoped container with `IDisposable` support -- `CapabilityScopeOptions` - Configuration options for scope behavior -- Enhanced registry APIs for composer and composition operations -- Comprehensive documentation and migration guides - -### Changed -- Improved performance: ~25ns registry lookups, ~51ns feature queries, ~4.5ΞΌs builds -- Enhanced memory management with automatic cleanup for reference types -- Better type safety and error handling - -### Removed -- `Cocoar.Capabilities.Core.Composer` static class -- `Cocoar.Capabilities.ComposerExtensions` -- `Cocoar.Capabilities.CompositionRegistry` - -## [0.10.0] - 2025-10-03 - -### πŸŽ‰ First Public Release - -**Cocoar.Capabilities** is a general-purpose capabilities system for .NET that enables type-safe, composable capability attachment to any object without requiring interface implementations or inheritance. - -### Added - -#### Core Features -- **Capability Composition Pattern**: Type-safe capability attachment to any object without requiring interfaces -- **High Performance Architecture**: Sub-microsecond queries (135 ns), efficient build operations (8.55 ΞΌs for 50 capabilities) -- **Thread Safety**: Immutable compositions with lock-free operations -- **Type Safety**: Compile-time guarantees for capability-subject relationships -- **Zero Dependencies**: 25KB assembly, AOT-friendly, no external dependencies - -#### Core API (Cocoar.Capabilities.Core) -- **Composer API**: Fluent builder pattern for capability composition - - `Composer.ForSubject(subject)` - Create composer for any object - - `Add(capability)` - Add capability with automatic contract detection - - `AddAs(capability)` - Explicit contract registration - - `WithPrimary(capability)` - Single identity enforcement - - `RemoveWhere(predicate)` - Conditional capability removal - - `Build()` - Create immutable composition -- **ICapability**: Generic capability interface for type-safe attachment -- **IPrimaryCapability**: Marker interface for primary capabilities -- **IOrderedCapability**: Deterministic processing with configurable ordering -- **IComposition**: Immutable capability container with query methods - -#### Registry API (Cocoar.Capabilities) -- **Global Registry**: Optional discovery system - - `Composition.FindOrDefault(subject)` - Global capability lookup - - `CompositionRegistry` - Pluggable registry provider interface - - Automatic registration and cleanup for reference/value types - -#### Memory Management -- **Smart References**: Automatic weak references for reference types -- **Explicit Control**: Manual lifecycle management for value types via `ClearValueTypes()` -- **Minimal Allocations**: Optimized for production workloads - -#### Performance Characteristics -- **Build Time**: ~171 ns per capability (linear scaling) -- **Query Time**: ~135 ns per lookup (constant time, scale-independent) -- **Memory Usage**: 11-102 KB build allocations, 320B-1.2KB runtime allocations -- **Thread Model**: Lock-free through immutability - -#### Documentation & Examples -- Complete API reference and core concepts guide -- Real-world examples including cross-project configuration systems -- Performance analysis and optimization guides -- Pattern cookbook for advanced usage scenarios -- Integration examples with dependency injection containers - -#### Quality Assurance -- Comprehensive test suite with 95%+ code coverage -- Performance benchmarks with BenchmarkDotNet -- CI/CD pipeline with automated testing and packaging -- Production-ready error handling and edge cases -- Thread safety validation and concurrent access testing -- Value type and reference type lifecycle testing - -### Technical Details - -#### Packages -- **Cocoar.Capabilities.Core**: Core capability system (25KB, zero dependencies) - - `Composer` - Fluent builder API - - `IComposition` - Immutable capability containers - - `ICapability` - Type-safe capability contracts - - Core algorithms and performance optimizations -- **Cocoar.Capabilities**: Registry system for global capability discovery - - `CompositionRegistry` - Global capability lookup - - `Composition.FindOrDefault()` - Discovery extensions - - Depends on Cocoar.Capabilities.Core - -#### Target Frameworks -- .NET Standard 2.0 (broad compatibility) -- .NET 8.0+ optimizations -- AOT compilation ready - -#### Breaking Changes -- N/A (Initial release) - -### Migration Guide -- N/A (Initial release) - ---- - -## Future Releases - -Future versions will follow semantic versioning and document all changes in this changelog. \ No newline at end of file diff --git a/README-SIMPLE.md b/README-SIMPLE.md deleted file mode 100644 index 20d894f..0000000 --- a/README-SIMPLE.md +++ /dev/null @@ -1,170 +0,0 @@ -# Cocoar.Capabilities - Simple Explanation - -**If you're a C# developer who's never heard of "capabilities" before, this guide is for you.** - -*Written by someone who struggled to understand this concept at first, despite being a Senior Software Architect.* - -## The Problem (Why This Exists) - -Have you ever wanted to: -- **Add behavior to objects you don't own** (third-party libraries, framework types)? -- **Let multiple libraries enhance the same object** without them knowing about each other? -- **Avoid circular dependencies** between projects but still share functionality? -- **Attach complex logic, not just simple properties** to any object? - -If yes, keep reading. If no, you probably don't need this library (yet). - -## What Is This? (In Simple Words) - -Think of **Cocoar.Capabilities** as a **smart dictionary** where: - -- **Keys** = Any object you want to enhance (we call this the "Subject") -- **Values** = Behaviors, data, or logic you want to attach (we call these "Capabilities") - -But unlike a regular dictionary, it's: -- βœ… **Type-safe** (compile-time checking) -- βœ… **High-performance** (sub-microsecond lookups) -- βœ… **Cross-project friendly** (no circular dependencies) - -## Real Example: The Journey - -### Before (Traditional Approach) -```csharp -public class UserService -{ - // Problem: To add logging, caching, validation, etc. - // I need to modify this class or inherit from it - // But what if this is from a third-party library? - // What if multiple libraries want to add different behaviors? -} -``` - -### After (Capabilities Approach) -```csharp -using var scope = new CapabilityScope(); -var userService = new UserService(); - -// Attach capabilities via the scope -var enhanced = scope.For(userService) - .Add(new LoggingCapability("Debug mode")) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Add(new ValidationCapability(user => user.IsValid())) - .Build(); - -// Now use those capabilities -var loggers = enhanced.GetAll(); -var caches = enhanced.GetAll(); -``` - -## Key Mental Shifts - -### 1. **"Subject" β‰  Email Subject** -- A **Subject** is just **any object** you want to enhance -- Think: "The subject of our enhancement" = the target object - -### 2. **"Capability" β‰  Application Feature** -- A **Capability** is **anything you attach** to a subject -- Think: "Adding capabilities (possibilities) to an object" -- German speakers: Think "MΓΆglichkeiten" - -### 3. **Objects as Dictionary Keys** -- **Any object** can be a "key" that unlocks attached behaviors -- **Multiple libraries** can attach their own capabilities -- **No modification** of the original object needed - -## When Would You Use This? - -### Scenario 1: Cross-Project Extensions -```csharp -// Project A: Core library defines a config class -public class DatabaseConfig { public string ConnectionString { get; set; } } - -// Project B: Validation library adds validation (without knowing about Project C) -composer.Add(new ValidationCapability(cfg => !string.IsNullOrEmpty(cfg.ConnectionString))); - -// Project C: DI library adds registration info (without knowing about Project B) -composer.Add(new DIRegistrationCapability(ServiceLifetime.Singleton)); - -// Consumer: Gets both capabilities without circular dependencies -var config = new DatabaseConfig(); -var enhanced = scope.For(config) - .Add(validationFromProjectB) - .Add(diInfoFromProjectC) - .Build(); -``` - -### Scenario 2: Third-Party Object Enhancement -```csharp -// You can't modify HttpClient, but you can enhance it -var httpClient = new HttpClient(); -var enhanced = scope.For(httpClient) - .Add(new RetryCapability(maxRetries: 3)) - .Add(new CircuitBreakerCapability(threshold: 5)) - .Add(new LoggingCapability("HTTP")) - .Build(); - -// Now httpClient has all these behaviors attached -``` - -## Simple API Overview - -```csharp -// 1. Start with any object (the "Subject") -var myObject = new AnyClass(); - -// 2. Attach capabilities (behaviors/data) -var composition = scope.For(myObject) - .Add(new SomeCapability()) // Add behavior - .Add(new AnotherCapability()) // Add more behavior - .Build(useRegistry: true); // Optionally register in scope registry - -// 3. Use the capabilities -var behaviors = composition.GetAll(); -if (composition.Has()) { - // Do something with that capability -} - -// 4. Find it globally later (if using Registry package) -var found = scope.Compositions.FindOrDefault(myObject); -``` - -## Two Packages = Two Approaches - -### Install -```bash -dotnet add package Cocoar.Capabilities -``` - -Use `CapabilityScopeOptions` to enable/disable registry tracking per scope: -```csharp -var scope = new CapabilityScope(new CapabilityScopeOptions{ UseCompositionRegistry = true }); -``` - -## Is This Like...? - -### Dependency Injection? -**No.** DI gives you instances. Capabilities attach behaviors to existing instances. - -### Extension Methods? -**Kind of,** but much more powerful: -- Extension methods are compile-time, capabilities are runtime -- Capabilities can carry state and complex logic -- Multiple libraries can add capabilities without conflicts - -### Attributes/Metadata? -**Similar concept,** but: -- Capabilities can contain actual logic, not just data -- They're attached at runtime, not compile-time -- Much more flexible and powerful - -## Next Steps - -1. **Try the examples** in this README -2. **Read the full documentation** (it will make sense now!) -3. **Check out real-world usage** in Cocoar.Configuration - -The journey from "What is this?" to "This is powerful!" is worth it. Trust the process! - ---- - -*This explanation was written after struggling to understand capabilities for days. If it still doesn't click, that's normal - the concept is genuinely different from traditional OOP patterns. See `docs/static-api-migration-strategy.md` if you're reading older examples with `BuildAndRegister()`.* \ No newline at end of file diff --git a/README.md b/README.md index cc4b619..bae5ede 100644 --- a/README.md +++ b/README.md @@ -1,316 +1,114 @@ # 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. - -> **New to capabilities?** Start with our [Simple Explanation](README-SIMPLE.md) for a beginner-friendly introduction. - -[![Build (develop)](https://github.com/cocoar-dev/cocoar.capabilities/actions/workflows/develop-prerelease.yml/badge.svg)](https://github.com/cocoar-dev/cocoar.capabilities/actions/workflows/develop-prerelease.yml) -[![PR Validation](https://github.com/cocoar-dev/cocoar.capabilities/actions/workflows/pr-validation.yml/badge.svg)](https://github.com/cocoar-dev/cocoar.capabilities/actions/workflows/pr-validation.yml) -[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) [![NuGet](https://img.shields.io/nuget/v/Cocoar.Capabilities.svg)](https://www.nuget.org/packages/Cocoar.Capabilities/) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![.NET](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/) [![Downloads](https://img.shields.io/nuget/dt/Cocoar.Capabilities.svg)](https://www.nuget.org/packages/Cocoar.Capabilities/) -## 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. - -```csharp -// Any object can have capabilities attached -using var scope = new CapabilityScope(); -var userService = new UserService(); -var composition = scope.For(userService) - .Add(new LoggingCapability(LogLevel.Info)) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Build(); // Immutable snapshot - -// Capabilities are discoverable and type-safe -var cache = composition.GetAll>().FirstOrDefault(); -var loggers = composition.GetAll>(); -``` +A high-performance, low-allocation capability composition library for .NET that implements the Capability Composition pattern for building extensible, type-safe systems. -## 🌟 Key Benefits +## πŸš€ Features -- **πŸ”’ Type Safe**: Compile-time guarantees for capability-subject relationships -- **⚑ High Performance**: ~25 ns registry lookups, ~51 ns feature queries, ~4.5 ΞΌs builds (50 caps) -- **🧡 Thread Safe**: Immutable by design - no locks needed -- **πŸ”Œ Extensible**: Cross-library capability attachment and discovery -- **πŸ“¦ Lightweight**: Single package ~28 KB (no dependencies, AOT-friendly) -- **🎯 Contract-Based**: Explicit registration semantics - subjects need no interfaces -- **πŸ’Ύ Smart Memory**: Reference types use ConditionalWeakTable for automatic cleanup; value types stored in ConcurrentDictionary +- **🎯 Type-Safe Composition** - Attach multiple capabilities to any object with full type safety +- **⚑ High Performance** - Zero-allocation lookups and minimal overhead +- **πŸ”’ Immutable Compositions** - Thread-safe by design with immutable capability collections +- **🎨 Flexible Registration** - Support for multiple contract types per capability +- **πŸ“¦ Primary Capabilities** - Enforce single "primary" capability per subject +- **πŸ”„ Recomposition** - Modify existing compositions safely +- **🎭 Custom Ordering** - Control capability resolution order with flexible ordering strategies +- **πŸ—‚οΈ Registry Support** - Optional registries for managing compositions across your application -## Install - -Install the single package: +## πŸ“¦ Installation ```bash dotnet add package Cocoar.Capabilities ``` -Registry behavior (global lookup) and composer tracking are now configured per `CapabilityScope` via `CapabilityScopeOptions`; no separate "Core" package is required. - -## Quick Start +## πŸŽ“ Quick Start -### 1. Define Capabilities ```csharp -using Cocoar.Capabilities; // Available in both packages +using Cocoar.Capabilities; -// Capabilities are just records/classes implementing ICapability -public record LoggingCapability(LogLevel Level, string Category) : ICapability; -public record CachingCapability(TimeSpan Duration) : ICapability; -public record ValidationCapability(Func Validator) : ICapability; -``` - -### 2. Attach Capabilities to Objects - -```csharp +// Create a scope to manage capabilities using var scope = new CapabilityScope(); -var userService = new UserService(); - -// Build immutable composition (store it yourself or rely on scope registries if enabled) -var composition = scope.For(userService) - .Add(new LoggingCapability(LogLevel.Debug, "UserManagement")) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Add(new ValidationCapability(user => user.IsValid())) - .Build(); - -// Enable registration explicitly at build time -var registered = scope.For(userService) - .Add(new LoggingCapability(LogLevel.Info, "UserManagement")) - .Build(useRegistry: true); // discoverable via scope.Compositions -``` - -### 3. Query and Use Capabilities - -**Basic Querying** (both packages): -```csharp -// Type-safe capability discovery -var loggers = composition.GetAll>(); -foreach (var logger in loggers) -{ - Logger.Log(logger.Level, $"[{logger.Category}] Processing user request"); -} - -// Optional capability usage -if (composition.Has>()) -{ - var cache = composition.GetAll>().First(); - // Use caching with cache.Duration -} -``` - -**Global Discovery (per-scope)** -```csharp -using var scope = new CapabilityScope(new CapabilityScopeOptions { UseCompositionRegistry = true }); -scope.For(userService).Add(new LoggingCapability(LogLevel.Debug, "UserManagement")).Build(); -var again = scope.Compositions.FindOrDefault(userService); -``` -## Core Concepts +// Define your subject +var document = new Document("README.md"); -### Subjects -Any object can be a subject - no special interfaces required: -```csharp -// Reference types -var service = new UserService(); -var config = new DatabaseConfig(); - -// Value types -var userId = 12345; -var status = OrderStatus.Pending; -var point = new Point(10, 20); - -// Even reflection objects -var method = typeof(UserService).GetMethod("CreateUser"); -``` - -### Capabilities -Behaviors, policies, or metadata attachable to subjects: -```csharp -// Generic capabilities work with any subject -public record MetricsCapability(string MetricName) : ICapability; - -// Specific capabilities for particular subjects -public record DatabaseConnectionCapability(string ConnectionString) : ICapability; +// Compose capabilities +var composition = scope.For(document) + .Add(new EditCapability()) + .Add(new PrintCapability()) + .Add(new ShareCapability()) + .Build(); -// Interface-based capabilities for contracts -public interface IValidationCapability : ICapability +// Retrieve and use capabilities +var capabilities = composition.GetAll(); +foreach (var cap in capabilities) { - bool IsValid(T subject); + cap.Edit(document); } ``` -### Contract Registration -Explicit control over how capabilities are queryable: -```csharp -var validator = new EmailValidator(); // implements IValidationCapability - -// Concrete registration - only queryable as EmailValidator -composer.Add(validator); - -// Contract registration - only queryable as IValidationCapability -composer.AddAs>(validator); - -// Multiple registration - queryable as both -composer.AddAs<(IValidationCapability, EmailValidator)>(validator); -``` - -## Real-World Example: Cross-Project Configuration System - -See how Cocoar.Capabilities enables sophisticated cross-project architectures: - -```csharp -// Core project defines base capabilities -configure.ConcreteType() - .AddValidation(config => ValidateConnectionString(config.ConnectionString)) - .AddHealthCheck("database", config => TestConnection(config)) - .ExposeAs(typeof(IDbConfig)); - -// DI project extends with new strategies - same API! -configure.ExposedType() - .AddValidation(config => ValidateRedisConnection(config)) // Same extension method - .AddHealthCheck("cache", config => TestRedisConnection(config)) // Same extension method - .AsSingleton() // DI-specific - .WithDependencyInjection(services => ConfigureServices(services)); // DI-specific -``` - -This demonstrates the **Primary Capability Strategy Pattern** - using capabilities to enable unified APIs across different project strategies. [See full example β†’](docs/examples/configuration-system.md) - -## Advanced Features - ### Primary Capabilities -Enforce single "core identity" per subject: -```csharp -// Only one primary capability allowed per subject -composer.WithPrimary(new DatabasePrimaryCapability()); -if (composition.TryGetPrimary(out var primary)) -{ - // Use primary behavior -} -``` +Enforce a single "primary" capability per subject: -### Capability Ordering -Deterministic processing sequences: ```csharp -public record OrderedMiddleware(int Priority) : ICapability, IOrderedCapability -{ - public int Order => Priority; // Lower values execute first -} - -// GetAll() automatically sorts by Order -var middleware = composition.GetAll>(); // Pre-sorted -``` - -Ordering is entirely opt-in: if no capability implements `IOrderedCapability`, no ordering scan or sort occurs (zero overhead path). When ordering is present, a single stable sort is performed at build/recompose time; enumerations reuse the pre-ordered array (no per-call sorting). See Ordering benchmarks in `Cocoar.Capabilities.Benchmarks` for measured build deltas across random, sorted, reverse, and duplicate-priority scenarios. +public record UserPrimaryCapability(string UserId, string Name) : IPrimaryCapability; -### Cross-Project Extension Methods -Enable clean separation without circular dependencies: -```csharp -// Core project -public static Composer AddLogging(this Composer composer, LogLevel level) - => composer.Add(new LoggingCapability(level)); - -// DI project - no circular dependency -public static Composer AsSingleton(this Composer composer) - => composer.Add(new SingletonLifetimeCapability()); +var composition = scope.For(user) + .Add(new UserPrimaryCapability("user123", "John Doe")) + .Add(new AdminCapability("Level2")) + .Build(); -// Usage: both work together seamlessly -using var scope2 = new CapabilityScope(); -scope2.For(service).AddLogging(LogLevel.Info).AsSingleton().Build(); +// Retrieve the primary capability +var primary = composition.GetPrimary(); +Console.WriteLine($"User: {primary.Name}"); ``` -## Performance & Architecture +### Multiple Contracts -### Registry Participation - -Each `CapabilityScope` can optionally track composers and/or compositions. Both options default to `true` but can be disabled for minimal overhead: +Register a single capability under multiple contract types: ```csharp -var scope = new CapabilityScope(new CapabilityScopeOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -// Force registration for a single build even if disabled globally -var composition = scope.For(subject) - .Add(new SomeCapability()) - .Build(useRegistry: true); +var composition = scope.For(myObject) + .AddAs<(IValidator, IFormatter)>(new DataProcessor()) + .Build(); -// Discover later if registered -var found = scope.Compositions.FindOrDefault(subject); +// Access via either contract +var validator = composition.GetAll().First(); +var formatter = composition.GetAll().First(); ``` -### Performance Summary (High-Level) - -Instead of embedding raw microsecond/nanosecond figures (easy to misinterpret without hardware & runtime context), we summarize behavior qualitatively: - -- **Build Time**: Core is the baseline; Registry adds a small mostly fixed overhead (registration + key canonicalization). Relative overhead shrinks as capability count grows. -- **Feature Queries (Get/Has for a specific capability type)**: Essentially the same for both; overhead is usually within typical measurement noise. -- **Full Enumeration (GetAll for large sets)**: Registry incurs extra indirection; absolute cost remains in the microsecond range but can look large as a percentage because the Core path is extremely small. -- **Memory**: Registry adds only a few dozen bytes of bookkeeping per registered composition; capability object sizes dominate actual usage. -- **Scaling Characteristics**: Build scales linearly with number of capabilities; feature queries are near-constant; enumeration scales with result size. - -For reproducible, versioned benchmark data (including exact numbers, environment, .NET version, and methodology) see the separate document: - -[Detailed performance analysis & methodology β†’](docs/performance-analysis.md) - -### Choosing Configuration - -| Disable Registry When | Enable Registry When | -| --------------------- | ------------------- | -| You already manage object lifetimes (DI/caches) | You want discovery without passing references | -| Maximum raw performance matters | Convenience outweighs small overhead | -| Very frequent queries or large compositions | Occasional queries / simpler compositions | -| Tight memory / allocation budgets | Simplicity of central lookup | - -### Interpreting Overhead - -Any system that lets you retrieve compositions later must store them somewhere. The Registry just makes this explicit and integrated; its overhead is the inherent cost of that convenience, not a library inefficiency. - -### Technical Notes - -- Lock-free via immutable compositions -- Subject key canonicalization for consistent string value semantics -- Per-scope customization of key mapping (no global mutable state) -- Works with reference & value type subjects (automatic cleanup for references) -- AOT-friendly: no runtime code generation, no external dependencies - -If you need concrete numbers for capacity planning, consult the dedicated performance document rather than relying on simplified README summaries. - -### Migration Note +## πŸ“š Documentation -Older examples referencing separate `Cocoar.Capabilities.Core` vs `Cocoar.Capabilities` packages, static `Composer.For`, `BuildAndRegister()`, or `Composition.FindOrDefault()` should be updated to use `CapabilityScope` and `CapabilityScopeOptions`. See `docs/static-api-migration-strategy.md` for detailed guidance. +- **[Examples](docs/Examples.md)** - Detailed examples and use cases +- **[API Reference](docs/API-Reference.md)** - Complete API documentation -## Documentation +## 🎯 Key Concepts -### Getting Started -- [Core Concepts & Architecture](docs/core-concepts.md) - Understand the capability composition pattern -- [API Reference](docs/api-reference.md) - Complete API documentation -- [Registration & Querying](docs/registration-and-querying.md) - How the type system works +- **CapabilityScope** - Entry point for all capability operations +- **Composer** - Fluent builder for creating compositions +- **Composition** - Immutable collection of capabilities attached to a subject +- **Primary Capability** - Single "main" capability per subject (via `IPrimaryCapability`) +- **Registry** - Optional centralized management of compositions -### Advanced Topics -- [Primary Capabilities](docs/guides/primary-capabilities.md) - Single identity enforcement and strategy patterns -- [Capability Ordering](docs/guides/capability-ordering.md) - Deterministic processing sequences -- [Tuple Contract Syntax](docs/guides/tuple-contracts.md) - Multiple contract registration -- [Memory Management](docs/guides/memory-management.md) - Lifecycle and performance optimization -- [Lifecycle & Disposal](docs/lifecycle-and-disposal.md) - Scope disposal effects and composition survivability +## 🀝 Contributing -### Examples & Patterns -- [Configuration System](docs/examples/configuration-system.md) - Real-world cross-project architecture -- [Pattern Cookbook](docs/guides/pattern-cookbook.md) - Creative capability usage patterns -- [Performance Optimization](docs/guides/performance-optimization.md) - Best practices for high-performance usage +Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md). -## Contributing +## πŸ“„ License -We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. -## License +## πŸ”— Links -This project is licensed under the [Apache License 2.0](LICENSE). +- [NuGet Package](https://www.nuget.org/packages/Cocoar.Capabilities/) +- [GitHub Repository](https://github.com/cocoar-dev/Cocoar.Capabilities) +- [Issue Tracker](https://github.com/cocoar-dev/Cocoar.Capabilities/issues) +- [Changelog](CHANGELOG.md) --- -**Built with ❀️ by the Cocoar team** \ No newline at end of file +Built with ❀️ by the Cocoar Development Team diff --git a/docs/DOCUMENTATION-REVIEW-SUMMARY.md b/docs/DOCUMENTATION-REVIEW-SUMMARY.md deleted file mode 100644 index d8364d5..0000000 --- a/docs/DOCUMENTATION-REVIEW-SUMMARY.md +++ /dev/null @@ -1,119 +0,0 @@ -# Documentation Review & Corrections Summary - -## βœ… **Review Completed** - -I've conducted a comprehensive review of all README and documentation files in the Cocoar.Capabilities project and identified several critical issues that have been **FIXED**. - -## πŸ”§ **Major Issues Fixed** - -### 1. **Outdated API References** -**Problem**: Many documentation files still referenced the old static API (`Composer.For()`, `BuildAndRegister()`, `Composition.FindOrDefault()`) - -**Files Fixed**: -- `docs/getting-started.md` - Updated all examples to use `CapabilityScope` -- `docs/api-reference.md` - Completely rewrote API documentation for current architecture -- `docs/registration-and-querying.md` - Updated registration examples - -**Changes Made**: -- Replaced `Composer.For(subject)` with `scope.For(subject)` -- Replaced `BuildAndRegister()` with `Build()` + scope registry -- Replaced static `Composition.FindOrDefault()` with `scope.Compositions.FindOrDefault()` -- Added proper `using var scope = new CapabilityScope()` patterns - -### 2. **Package Information Inconsistencies** -**Problem**: Documentation mentioned separate \"Core\" vs \"Registry\" packages - -**Solution**: -- Clarified that there is only **one package**: `Cocoar.Capabilities` -- Explained that registry behavior is controlled via `CapabilityScopeOptions` -- Removed all references to the non-existent `Cocoar.Capabilities.Core` package - -### 3. **API Documentation Gaps** -**Problem**: No complete public API reference existed - -**Solution**: -- **Created new file**: `docs/complete-public-api-reference.md` -- Comprehensive listing of all public interfaces, classes, and methods -- Proper documentation of the current scope-based architecture -- Performance characteristics and usage examples - -## πŸ“„ **Files Updated** - -### βœ… **Major Updates** -1. **`README.md`** - βœ… Already current (no changes needed) -2. **`README-SIMPLE.md`** - βœ… Already current (no changes needed) -3. **`docs/getting-started.md`** - πŸ”§ **FIXED** - Updated all API examples -4. **`docs/api-reference.md`** - πŸ”§ **FIXED** - Complete rewrite for current API -5. **`docs/registration-and-querying.md`** - πŸ”§ **FIXED** - Updated examples - -### βœ… **New Files Created** -6. **`docs/complete-public-api-reference.md`** - πŸ†• **NEW** - Comprehensive API listing - -### βœ… **Files Verified as Current** -- `docs/core-concepts.md` - βœ… Current -- `docs/examples/configuration-system.md` - βœ… Current -- `docs/guides/` - βœ… Current (all files) -- `docs/performance-analysis.md` - βœ… Current -- `docs/lifecycle-and-disposal.md` - βœ… Current - -## 🎯 **Complete Public API List** - -### **Core Interfaces** -- `ICapability` -- `ICapability` -- `IPrimaryCapability` -- `IOrderedCapability` -- `IComposition` -- `IComposition` - -### **Main Classes** -- `CapabilityScope` - Main entry point -- `Composer` - Fluent builder -- `ComposerRegistryApi` - Scope-level composer registry -- `CompositionRegistryApi` - Scope-level composition registry - -### **Configuration** -- `CapabilityScopeOptions` - Scope configuration -- `ISubjectKeyMapper` - Custom key mapping - -### **Extension Methods** -- `ReadOnlyListExtensions` - Utility methods - -### **Advanced Interfaces** (for custom implementations) -- `ICapabilityRegistry` -- `IComposerRegistry` -- `ICompositionRegistry` - -## πŸ“Š **Documentation Status** - -| Document | Status | Notes | -|----------|--------|-------| -| `README.md` | βœ… **Current** | No changes needed | -| `README-SIMPLE.md` | βœ… **Current** | No changes needed | -| `docs/getting-started.md` | πŸ”§ **FIXED** | Updated API examples | -| `docs/api-reference.md` | πŸ”§ **FIXED** | Complete rewrite | -| `docs/complete-public-api-reference.md` | πŸ†• **NEW** | Comprehensive API list | -| `docs/registration-and-querying.md` | πŸ”§ **FIXED** | Updated examples | -| `docs/core-concepts.md` | βœ… **Current** | No changes needed | -| `docs/examples/configuration-system.md` | βœ… **Current** | No changes needed | -| All `docs/guides/*.md` | βœ… **Current** | No changes needed | -| `docs/performance-analysis.md` | βœ… **Current** | No changes needed | - -## πŸŽ‰ **Result** - -**All documentation is now ACCURATE and CURRENT** with the actual implementation. The documentation correctly reflects: - -1. **Single Package Architecture** - Only `Cocoar.Capabilities` package -2. **Scope-Based API** - All examples use `CapabilityScope` -3. **Current Method Names** - No outdated static API references -4. **Complete API Coverage** - All public APIs documented -5. **Consistent Examples** - All code samples work with current API - -## πŸ“š **Recommendations** - -1. **Use the new `complete-public-api-reference.md`** for comprehensive API lookup -2. **Review `getting-started.md`** for updated examples that reflect current best practices -3. **The main `README.md` remains the best starting point** for new users -4. **All guides in `docs/guides/`** contain advanced patterns and are current - -The project documentation is now **feature complete** and **accurate**! 🎯 \ No newline at end of file diff --git a/docs/RELEASE-NOTES-v0.11.0.md b/docs/RELEASE-NOTES-v0.11.0.md deleted file mode 100644 index eff29e7..0000000 --- a/docs/RELEASE-NOTES-v0.11.0.md +++ /dev/null @@ -1,284 +0,0 @@ -# Release Notes v0.11.0 - Major Architecture Refactor - -**Release Date**: October 4, 2024 -**Breaking Changes**: Yes - Major API changes -**Migration Required**: Yes - See migration guide below - -## 🎯 What's New - -### Major Architectural Improvement: CapabilityScope - -This release introduces **CapabilityScope** - a fundamental improvement that eliminates static dependencies and provides proper lifetime management for capability compositions. - -## 🚨 Breaking Changes Summary - -### 1. API Changes - Before vs After - -**❌ Old Static API (v1.x):** -```csharp -// Static methods - no longer available -var composition = Composer.For(userService) - .Add(new LoggingCapability()) - .Build(); - -var updated = Composer.Recompose(composition) - .Add(new CachingCapability()) - .Build(); -``` - -**βœ… New Scoped API (v0.11.0):** -```csharp -// Scoped approach with proper resource management -using var scope = new CapabilityScope(); - -var composition = scope.For(userService) - .Add(new LoggingCapability()) - .Build(); - -var updated = scope.Recompose(composition) - .Add(new CachingCapability()) - .Build(); -``` - -### 2. Package Structure Changes - -| Change Type | v1.x | v0.11.0 | -|-------------|------|------| -| **Packages** | `Cocoar.Capabilities.Core` + `Cocoar.Capabilities` | `Cocoar.Capabilities` only | -| **Total Size** | ~37KB (21KB + 16KB) | ~28KB | -| **Dependencies** | Zero | Zero (maintained) | - -### 3. Namespace Changes - -```csharp -// Remove these imports -using Cocoar.Capabilities.Core; - -// Use this instead -using Cocoar.Capabilities; -``` - -## πŸš€ Quick Migration Guide - -### Step 1: Update Package References - -```xml - - - - - - - -``` - -### Step 2: Update Code Patterns - -#### Pattern 1: Basic Composition -```csharp -// Before -var composition = Composer.For(subject).Add(capability).Build(); - -// After -using var scope = new CapabilityScope(); -var composition = scope.For(subject).Add(capability).Build(); -``` - -#### Pattern 2: Recomposition -```csharp -// Before -var updated = Composer.Recompose(existing).Add(newCapability).Build(); - -// After -using var scope = new CapabilityScope(); -var updated = scope.Recompose(existing).Add(newCapability).Build(); -``` - -#### Pattern 3: Dependency Injection -```csharp -// Before - Global static access -public class MyService -{ - public void DoSomething(MyObject obj) - { - var composition = Composer.For(obj).Add(capability).Build(); - // use composition - } -} - -// After - Inject scope or create as needed -public class MyService -{ - private readonly CapabilityScope _scope; - - public MyService(CapabilityScope scope) // Or create new scope - { - _scope = scope; - } - - public void DoSomething(MyObject obj) - { - var composition = _scope.For(obj).Add(capability).Build(); - // use composition - } -} -``` - -### Step 3: Handle Scope Lifetime - -Choose one of these patterns based on your needs: - -#### Option A: Short-lived Scope (Recommended for most cases) -```csharp -public void ProcessRequest() -{ - using var scope = new CapabilityScope(); - var composition = scope.For(subject).Add(capability).Build(); - // Use composition within this method - // Scope automatically disposed at end -} -``` - -#### Option B: Longer-lived Scope (DI Container) -```csharp -// In DI registration -services.AddSingleton(); -// Or -services.AddScoped(); - -// In your class -public class MyService -{ - private readonly CapabilityScope _scope; - - public MyService(CapabilityScope scope) - { - _scope = scope; - } - - // Use _scope.For(...) throughout the service -} -``` - -#### Option C: Application-wide Scope -```csharp -public class Application -{ - private readonly CapabilityScope _globalScope; - - public Application() - { - _globalScope = new CapabilityScope(); - } - - public CapabilityScope Capabilities => _globalScope; - - // Remember to dispose in application shutdown - public void Shutdown() - { - _globalScope?.Dispose(); - } -} -``` - -## ✨ New Features & Improvements - -### 1. Enhanced Resource Management -- **Automatic Cleanup**: `CapabilityScope` implements `IDisposable` for proper resource management -- **Memory Efficiency**: Advanced weak reference handling for automatic cleanup -- **Type-Specific Storage**: Optimized storage patterns for reference vs value types - -### 2. Performance Improvements -| Operation | v0.11.0 Performance | Notes | -|-----------|------------------|-------| -| Registry Lookups | ~25ns | Measured via benchmarks | -| Feature Queries | ~51ns | Measured via benchmarks | -| Composition Builds | ~4.5ΞΌs (50 caps) | Measured via benchmarks | - -### 3. New Configuration Options -```csharp -var options = new CapabilityScopeOptions -{ - SubjectKeyMappers = new List - { - new CustomStringMapper(), - // Add your custom mappers - } -}; - -using var scope = new CapabilityScope(options); -``` - -### 4. Enhanced Registry APIs -- `ComposerRegistryApi` - Registry-backed composer operations -- `CompositionRegistryApi` - Registry-backed composition operations -- Full registry integration while maintaining high performance - -## πŸ” Why This Change? - -### Problems Solved -1. **❌ Static Dependencies**: Old static API made testing and isolation difficult -2. **❌ Resource Leaks**: No explicit cleanup mechanism for internal resources -3. **❌ Package Complexity**: Two packages with overlapping concerns -4. **❌ Limited Flexibility**: Static configuration couldn't be scoped per use case - -### Benefits Gained -1. **βœ… Better Testability**: Scoped dependencies enable easier unit testing -2. **βœ… Resource Management**: Explicit disposal and lifetime control -3. **βœ… Simplified Deployment**: Single package with all functionality -4. **βœ… Enhanced Flexibility**: Per-scope configuration and isolation - -## 🎯 Upgrade Strategy - -### Low-Risk Migration Approach - -1. **Start Small**: Migrate one service/component at a time -2. **Test Thoroughly**: Each migrated component should have tests covering the new API -3. **Bridge Pattern**: Temporarily wrap old code with scope creation until fully migrated - -### Example Bridge Pattern -```csharp -// Temporary wrapper to ease migration -public static class ComposerBridge -{ - [Obsolete("Use CapabilityScope directly")] - public static Composer For(T subject) where T : notnull - { - // Create temporary scope - should be replaced with proper scope management - var scope = new CapabilityScope(); - return scope.For(subject); - } -} -``` - -## πŸ“š Additional Resources - -- **Complete Migration Guide**: `docs/static-api-migration-strategy.md` -- **API Reference**: `docs/complete-public-api-reference.md` -- **Performance Guide**: `docs/guides/performance-optimization.md` -- **Lifecycle Management**: `docs/lifecycle-and-disposal.md` - -## ❓ Common Questions - -### Q: Do I need to change my capability implementations? -**A**: No! Your existing capability classes that implement `ICapability` remain unchanged. - -### Q: What about performance - is the new API slower? -**A**: No! Performance is actually improved. We have real benchmark data showing faster operations across all scenarios. - -### Q: Can I gradually migrate or must I do it all at once? -**A**: You can migrate gradually. Consider using a bridge pattern during transition. - -### Q: Why eliminate the Core package? -**A**: Simplified deployment and reduced confusion. The functionality is now consolidated into a single, well-organized package. - -### Q: How do I handle dependency injection? -**A**: Register `CapabilityScope` with your DI container at the appropriate lifetime (singleton, scoped, or transient based on your needs). - -## 🏁 Conclusion - -This release represents a **major quality improvement** that enhances testability, resource management, and long-term maintainability while improving performance. The migration effort is moderate and can be done incrementally. - -**We recommend upgrading** as this architecture provides a more robust foundation for capability-based development. - -For questions or migration assistance, please refer to the documentation or create an issue in the repository. \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index 656c48a..e2e385a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,453 +1,372 @@ -# API Reference +# Cocoar.Capabilities - API Reference -Complete reference for all public APIs in Cocoar.Capabilities. +Complete API reference for the Cocoar.Capabilities library. -## Package Architecture +## Table of Contents -Distributed as a single package: **`Cocoar.Capabilities`**. +- [CapabilityScope](#capabilityscope) +- [CapabilityScopeOptions](#capabilityscopeoptions) +- [Composer](#composer) +- [IComposition](#icomposition) +- [IPrimaryCapability](#iprimarycapability) +- [ComposerRegistryApi](#composerregistryapi) +- [CompositionRegistryApi](#compositionregistryapi) -Scope-level options (`CapabilityScopeOptions`) enable or disable composer and composition registry tracking. No separate *Core* vs *Registry* packages exist anymore. Historical references to dual packaging and static helpers (like `BuildAndRegister()` / `Composition.FindOrDefault`) should be migrated to the `CapabilityScope` model. - -## Core Interfaces - -### ICapability +--- -Base marker interface for all capabilities. +## CapabilityScope -```csharp -public interface ICapability { } -``` +Entry point for all capability operations. Manages the lifecycle of composers and compositions. -### ICapability<in TSubject> +### Constructors -Generic capability interface that defines a capability for a specific subject type. +| Constructor | Description | +|------------|-------------| +| `CapabilityScope(CapabilityScopeOptions? options = null)` | Creates a new capability scope with the specified options | -```csharp -public interface ICapability : ICapability { } -``` +### Methods -**Usage**: -```csharp -public record LoggingCapability(LogLevel Level) : ICapability; -public record CachingCapability(TimeSpan Duration) : ICapability; -``` +| Method | Returns | Description | +|--------|---------|-------------| +| `For(object subject, bool? useRegistry = null)` | `Composer` | Creates a new composer for the specified subject | +| `Recompose(IComposition composition, bool? useRegistry = null)` | `Composer` | Creates a new composer based on an existing composition | +| `Dispose()` | `void` | Releases all resources used by the scope | -### IPrimaryCapability<in T> +### Properties -Marker interface for primary capabilities. Exactly one primary capability may exist per subject at any time. -Adding rules: +| Property | Type | Description | +|----------|------|-------------| +| `Composers` | `ComposerRegistryApi` | Provides access to the composer registry | +| `Compositions` | `CompositionRegistryApi` | Provides access to the composition registry | -- First registration may use Add, AddAs, tuple AddAs, or WithPrimary -- Replacement MUST use WithPrimary(newPrimary) -- Add / AddAs / tuple AddAs will THROW if a primary already exists -- WithPrimary(null) removes the current primary -- Tuples may not contain more than one IPrimaryCapability<> contract - - TryAdd / TryAddAs of another primary silently no-op (they never replace) +### Example ```csharp -public interface IPrimaryCapability : ICapability { } +using var scope = new CapabilityScope(); +var composer = scope.For(myObject); +var composition = composer.Add(new MyCapability()).Build(); ``` -**Usage**: -```csharp -public record DatabasePrimaryCapability : IPrimaryCapability; +--- -// First time -composer.Add(new DatabasePrimaryCapability()); +## CapabilityScopeOptions -// Replacing (must use WithPrimary) -composer.WithPrimary(new DatabasePrimaryCapability()); +Configuration options for a capability scope. -// Removing -composer.WithPrimary(null); -``` +### Properties -## Registry Participation +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `UseComposerRegistry` | `bool` | `false` | Controls whether composers are automatically registered when created | +| `UseCompositionRegistry` | `bool` | `false` | Controls whether compositions are automatically registered when built | +| `SubjectKeyMappers` | `List>?` | `null` | Optional list of functions to canonicalize subject keys for registry lookups | -Registration is controlled per scope and optionally per build call: +### Example ```csharp -using var scope = new CapabilityScope(new CapabilityScopeOptions +var options = new CapabilityScopeOptions { - UseCompositionRegistry = true // default -}); + UseComposerRegistry = true, + UseCompositionRegistry = true, + SubjectKeyMappers = new List> + { + obj => obj is string s ? s.ToLowerInvariant() : obj + } +}; +``` -var composition = scope.For(user) - .Add(new LoggingCapability(LogLevel.Info)) - .Build(); // automatically registered because UseCompositionRegistry=true -// Explicit override (force register even if disabled in options) -var forced = scope.For(user) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Build(useRegistry: true); +--- -// Lookup -var found = scope.Compositions.FindOrDefault(user); -``` +## Composer -To migrate from legacy `BuildAndRegister()` + static global lookup, see `static-api-migration-strategy.md`. -``` +Fluent builder for creating capability compositions. -### IOrderedCapability +### Properties -Interface for capabilities that need specific ordering within their type group. +| Property | Type | Description | +|----------|------|-------------| +| `Subject` | `object` | Gets the subject this composer is building capabilities for | -```csharp -public interface IOrderedCapability -{ - int Order { get; } -} -``` +### Methods -**Usage**: -```csharp -public record OrderedMiddleware(int Priority) : ICapability, IOrderedCapability -{ - public int Order => Priority; // Lower values execute first -} -``` +| Method | Returns | Description | +|--------|---------|-------------| +| `Add(object capability, int? order = null)` | `Composer` | Adds a capability to the composition | +| `Add(object capability, Func orderSelector)` | `Composer` | Adds a capability with order determined by a selector function | +| `AddAs(object capability, int? order = null)` | `Composer` | Adds a capability and registers it under specific contract type(s) | +| `AddAs(object capability, Func orderSelector)` | `Composer` | Adds a capability under specific contract(s) with order selector | +| `TryAdd(TCapability capability, int? order = null)` | `Composer` | Adds a capability only if it doesn't already exist | +| `TryAdd(TCapability capability, Func orderSelector)` | `Composer` | Try-add variant with order selector | +| `Has()` | `bool` | Checks if a capability of the specified type has been added | +| `HasPrimary()` | `bool` | Checks if a primary capability has been added | +| `Build(bool? useRegistry = null)` | `IComposition` | Builds the final immutable composition | -## Core Types +### Exceptions -### IComposition +| Method | Exception | Condition | +|--------|-----------|-----------| +| `Add` | `ArgumentNullException` | If capability is null | +| `Add` | `InvalidOperationException` | If adding a second primary capability or if already built | +| `Build` | `InvalidOperationException` | If already built | -Non-generic interface for accessing basic composition information. +### Example ```csharp -public interface IComposition -{ - object Subject { get; } - int TotalCapabilityCount { get; } -} +var composition = scope.For(subject) + .Add(new Capability1(), order: 10) + .Add(new Capability2(), order: 5) + .AddAs<(IContract1, IContract2)>(new MultiContract()) + .TryAdd(new OptionalCapability()) + .Build(); ``` -### IComposition<TSubject> +--- -Generic interface for typed access to capabilities attached to a subject. +## IComposition -```csharp -public interface IComposition : IComposition -{ - new TSubject Subject { get; } - - // Primary capability methods - bool HasPrimary(); - bool HasPrimary() where TPrimaryCapability : class, IPrimaryCapability; - bool TryGetPrimary(out IPrimaryCapability primary); - IPrimaryCapability? GetPrimaryOrDefault(); - IPrimaryCapability GetPrimary(); - bool TryGetPrimaryAs(out TPrimaryCapability primary) where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability? GetPrimaryOrDefaultAs() where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability GetRequiredPrimaryAs() where TPrimaryCapability : class, IPrimaryCapability; - - // Capability query methods - IReadOnlyList GetAll() where TCapability : class, ICapability; - IReadOnlyList> GetAll(); - bool Has() where TCapability : class, ICapability; - int Count() where TCapability : class, ICapability; -} -``` +Immutable collection of capabilities attached to a subject. Thread-safe. -## Builder API +### Properties -### CapabilityScope +| Property | Type | Description | +|----------|------|-------------| +| `Subject` | `object` | Gets the subject this composition is attached to | +| `TotalCapabilityCount` | `int` | Gets the total number of capabilities in the composition | -Entry point for creating capability scopes and managing compositions. +### Capability Query Methods -```csharp -public sealed class CapabilityScope : IDisposable -{ - // Constructor - public CapabilityScope(CapabilityScopeOptions? options = null); - - // Create composer for subject - public Composer For(TSubject subject, bool? useRegistry = null) where TSubject : notnull; - - // Recomposition from existing composition - public Composer Recompose(IComposition composition, bool? useRegistry = null) where TSubject : notnull; - - // Registry access - public ComposerRegistryApi Composers { get; } - public CompositionRegistryApi Compositions { get; } - - // Disposal - public void Dispose(); -} -``` +| Method | Returns | Description | +|--------|---------|-------------| +| `GetAll()` | `IReadOnlyList` | Retrieves all capabilities of the specified type in order | +| `Has()` | `bool` | Checks if any capability of the specified type exists | -### CapabilityScopeOptions +### Primary Capability Methods -Configuration options for capability scopes. +| Method | Returns | Description | +|--------|---------|-------------| +| `HasPrimary()` | `bool` | Checks if a primary capability exists | +| `HasPrimary()` | `bool` | Checks if a primary capability of specific type exists | +| `GetPrimary()` | `IPrimaryCapability` | Gets the primary capability (throws if not found) | +| `GetPrimaryOrDefault()` | `IPrimaryCapability?` | Gets the primary capability or null | +| `TryGetPrimary(out IPrimaryCapability primary)` | `bool` | Tries to get the primary capability | +| `GetPrimaryOrDefaultAs()` | `TPrimaryCapability?` | Gets the primary capability cast to a specific type, or null | +| `GetRequiredPrimaryAs()` | `TPrimaryCapability` | Gets the primary capability cast to a specific type (throws if not found) | +| `TryGetPrimaryAs(out TPrimaryCapability primary)` | `bool` | Tries to get the primary capability as a specific type | -```csharp -public record CapabilityScopeOptions -{ - public bool UseComposerRegistry { get; init; } = true; - public bool UseCompositionRegistry { get; init; } = true; - public IReadOnlyList SubjectKeyMappers { get; init; } = Array.Empty(); -} -``` +### Exceptions -### Composer<TSubject> +| Method | Exception | Condition | +|--------|-----------|-----------| +| `GetPrimary()` | `InvalidOperationException` | If no primary capability exists | +| `GetRequiredPrimaryAs()` | `InvalidOperationException` | If primary capability doesn't exist or isn't of the specified type | -Fluent builder for capability registration (created via `CapabilityScope.For()`). +### Example ```csharp -public sealed class Composer where TSubject : notnull +// Query capabilities +var validators = composition.GetAll(); +var hasLogging = composition.Has(); + +// Work with primary +if (composition.TryGetPrimary(out var primary)) { - public TSubject Subject { get; } - - // Basic registration - public Composer Add(ICapability capability); - - // Contract registration - public Composer AddAs(ICapability capability) where TContract : class, ICapability; - - // Tuple contract registration - public Composer AddAs(ICapability capability) where TContracts : ITuple; - - // Conditional registration - public Composer TryAdd(TCapability capability) where TCapability : class, ICapability; - public Composer TryAddAs(ICapability capability) where TContract : class, ICapability; - - // Capability removal - public Composer RemoveWhere(Func, bool> predicate); - - // Primary capability management - public Composer WithPrimary(IPrimaryCapability? primary); - - // Query builder state - public bool HasPrimary(); - public bool Has() where TCapability : class, ICapability; - - // Build immutable composition - public IComposition Build(bool? useRegistry = null); + Console.WriteLine($"Primary: {primary}"); } + +// Type-safe primary access +var userPrimary = composition.GetRequiredPrimaryAs(); ``` -## Registry APIs +--- + +## IPrimaryCapability -### ComposerRegistryApi +Marker interface indicating a capability that should be the primary capability for an instance. -Scope-level registry for composer lookup and management. +### Interface Definition ```csharp -public class ComposerRegistryApi : IDisposable -{ - // Find existing composer by subject - public bool TryFind(TSubject subject, out Composer composer) where TSubject : notnull; - public Composer? FindOrDefault(TSubject subject) where TSubject : notnull; - public Composer FindRequired(TSubject subject) where TSubject : notnull; - - // Remove composer - public bool Remove(TSubject subject) where TSubject : notnull; - public bool Remove(object subject); - - public void Dispose(); -} +public interface IPrimaryCapability { } ``` -### CompositionRegistryApi +### Rules -Scope-level registry for composition lookup and management. +| Rule | Description | +|------|-------------| +| **Single Primary** | Only one primary capability is allowed per composition | +| **Exception on Duplicate** | Attempting to add a second primary capability throws `InvalidOperationException` | +| **Specialized Retrieval** | Primary capabilities have dedicated retrieval methods on `IComposition` | + +### Example ```csharp -public class CompositionRegistryApi : IDisposable -{ - // Generic subject lookup - public bool TryFind(TSubject subject, out IComposition composition) where TSubject : notnull; - public IComposition? FindOrDefault(TSubject subject) where TSubject : notnull; - public IComposition FindRequired(TSubject subject) where TSubject : notnull; - - // Non-generic subject lookup - public bool TryFind(object subject, out IComposition composition); - public IComposition? FindOrDefault(object subject); - public IComposition FindRequired(object subject); - - // Composition removal - public bool Remove(TSubject subject) where TSubject : notnull; - public bool Remove(object subject); - - public void Dispose(); -} +public record UserPrimaryCapability(string UserId, string Name) : IPrimaryCapability; + +public record DocumentPrimaryCapability(string Id, string Title) : IPrimaryCapability; + +// Use in composition +var composition = scope.For(user) + .Add(new UserPrimaryCapability("user123", "John Doe")) + .Add(new AdminCapability()) // Non-primary, OK + .Build(); ``` -## Extension Methods +--- + +## ComposerRegistryApi -### ReadOnlyListExtensions +Provides access to the composer registry for managing active composers. -Utility extensions for capability collections. +### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `Has(object subject)` | `bool` | Checks if a composer exists for the subject | +| `Find(object subject)` | `Composer` | Finds the composer for the subject (throws if not found) | +| `FindOrDefault(object subject)` | `Composer?` | Finds the composer or returns null | +| `TryGet(object subject, out Composer composer)` | `bool` | Tries to get the composer for the subject | + +### Example ```csharp -public static class ReadOnlyListExtensions +if (scope.Composers.Has(document)) { - public static void ForEach(this IReadOnlyList list, Action action); + var composer = scope.Composers.Find(document); + // Composer is still being built } ``` -## Configuration +--- -### ISubjectKeyMapper +## CompositionRegistryApi -Interface for custom subject key mapping strategies. +Provides access to the composition registry for managing built compositions. -```csharp -public interface ISubjectKeyMapper -{ - bool CanMap(Type subjectType); - string MapToKey(object subject); -} -``` +### Methods -## Usage Patterns +| Method | Returns | Description | +|--------|---------|-------------| +| `Has(object subject)` | `bool` | Checks if a composition exists for the subject | +| `Find(object subject)` | `IComposition` | Finds the composition for the subject (throws if not found) | +| `FindOrDefault(object subject)` | `IComposition?` | Finds the composition or returns null | +| `TryFind(object subject, out IComposition composition)` | `bool` | Tries to find the composition for the subject | -### Basic Registration and Query +### Example ```csharp -// Create scope and composition -using var scope = new CapabilityScope(); -var composition = scope.For(subject) - .Add(new FirstCapability()) - .Add(new SecondCapability()) - .Build(); - -// Query capabilities -var capabilities = composition.GetAll>(); -if (composition.Has>()) +var composition = scope.Compositions.FindOrDefault(document); +if (composition != null) { - // Handle capability presence + var capabilities = composition.GetAll(); } ``` -### Contract-Based Registration +--- -```csharp -// Register under interface contract -using var scope = new CapabilityScope(); -var composer = scope.For(subject) - .AddAs>(new EmailValidator()); +## Performance Characteristics -// Register under multiple contracts (tuple syntax) -composer.AddAs<(IValidationCapability, EmailValidator)>(validator); -``` -composer.AddAs<(IValidationCapability, EmailValidator)>(validator); -``` +| Operation | Complexity | Notes | +|-----------|-----------|-------| +| Capability Lookup | O(1) | Dictionary-based lookup | +| Add Capability | O(1) amortized | List growth | +| Build Composition | O(n log n) | Sorting by order value | +| Memory (Post-Build) | Zero allocation | For capability lookups | +| Registry Lookup | O(1) | With canonical key | -### Primary Capability Usage +--- -```csharp -// Set primary capability -using var scope = new CapabilityScope(); -var composition = scope.For(subject) - .WithPrimary(new DatabasePrimaryCapability()) - .Build(); +## Thread Safety -// Query primary capability -if (composition.TryGetPrimary(out var primary)) -{ - // Use primary capability -} +| Type | Thread Safety | Description | +|------|--------------|-------------| +| `CapabilityScope` | ❌ Not thread-safe | Each thread should use its own scope or external synchronization | +| `Composer` | ❌ Not thread-safe | Should not be shared across threads | +| `IComposition` | βœ… Thread-safe | Immutable and fully thread-safe, can be safely shared | +| Registries | βœ… Thread-safe | Internal locking for concurrent access | -var typedPrimary = composition.GetPrimaryOrDefaultAs>(); -``` +--- -### Conditional Registration +## Best Practices -```csharp -// Only register if not already present -using var scope = new CapabilityScope(); -var composer = scope.For(subject) - .TryAdd(new LoggingCapability(LogLevel.Info)) - .TryAddAs>(new EmailValidator()); -``` +| Practice | Description | +|----------|-------------| +| **Always Dispose Scopes** | Use `using` statement to ensure proper cleanup | +| **Don't Reuse Composers** | Create new composers for each composition | +| **Enable Registries Sparingly** | Only enable when centralized management is needed | +| **Use Explicit Ordering** | Specify order values for predictable behavior | +| **Primary for Identity** | Use primary capabilities for the main "identity" of a subject | +| **Try-Add for Conditionals** | Use `TryAdd` when adding logic is complex | +| **Recompose for Modifications** | Never try to mutate compositions directly | -### Capability Removal +--- -```csharp -// Remove capabilities by predicate -using var scope = new CapabilityScope(); -var composition = scope.For(subject) - .Add(new LoggingCapability(LogLevel.Debug, "Debug")) - .Add(new LoggingCapability(LogLevel.Info, "Info")) - .RemoveWhere(cap => cap is LoggingCapability log && log.Level == LogLevel.Debug) - .Build(); -``` +## Common Design Patterns -### Scope Registry Usage +### Builder Pattern ```csharp -// Create scope with configuration -using var scope = new CapabilityScope(new CapabilityScopeOptions +public class DocumentBuilder { - UseCompositionRegistry = true, - UseComposerRegistry = true -}); + private readonly Composer _composer; -// Build and register -var composition = scope.For(subject) - .Add(new LoggingCapability(LogLevel.Info, "Test")) - .Build(); // Automatically registered due to UseCompositionRegistry=true + public DocumentBuilder(CapabilityScope scope, object subject) + { + _composer = scope.For(subject); + } -// Find composition by subject -var foundComposition = scope.Compositions.FindOrDefault(subject); + public DocumentBuilder WithEditing() + { + _composer.Add(new EditCapability()); + return this; + } -// Remove composition -scope.Compositions.Remove(subject); - -// Find composer (if still building) -var foundComposer = scope.Composers.FindOrDefault(subject); + public IComposition Build() => _composer.Build(); +} ``` -## Error Handling - -### Common Exceptions - -**InvalidOperationException**: -- Thrown when multiple primary capabilities are registered -- Thrown when required capabilities are not found -- Thrown when builder is used after `Build()` has been called +### Strategy Pattern -**ArgumentException**: -- Thrown when contract types don't implement `ICapability` -- Thrown when recomposing with invalid composition types +```csharp +// Add strategies as capabilities, execute in order +var composition = scope.For(processor) + .Add(new ValidationStrategy(), order: 1) + .Add(new TransformationStrategy(), order: 2) + .Add(new PersistenceStrategy(), order: 3) + .Build(); -**ArgumentNullException**: -- Thrown when null subjects or capabilities are provided +foreach (var strategy in composition.GetAll()) +{ + strategy.Execute(); +} +``` -### Exception Examples +### Chain of Responsibility ```csharp -// Multiple primary capabilities -try -{ - var composition = Composer.For(subject) - .WithPrimary(new FirstPrimary()) - .WithPrimary(new SecondPrimary()) // This will throw - .Build(); -} -catch (InvalidOperationException ex) -{ - // "Multiple primary capabilities registered for 'Subject'. Only one primary capability is allowed." -} +var composition = scope.For(request) + .Add(new AuthenticationHandler(), order: 1) + .Add(new AuthorizationHandler(), order: 2) + .Add(new ValidationHandler(), order: 3) + .Build(); -// Required capability not found -try +foreach (var handler in composition.GetAll()) { - var required = composition.GetRequiredPrimaryAs>(); -} -catch (InvalidOperationException ex) -{ - // "Primary capability of type 'MissingPrimary' not found for subject 'Subject'." + if (handler.CanHandle(request)) + { + await handler.HandleAsync(request); + break; + } } ``` -## Performance Notes - -- **Registration**: O(1) for single capabilities, O(k) for tuple registration where k = number of contracts -- **Query**: O(1) for capability lookup, O(n) for GetAll() where n = capabilities of that type -- **Memory**: Compositions use array-based storage for optimal performance -- **Threading**: All operations are thread-safe through immutability - ---- +### Decorator Pattern -This API reference covers all public interfaces and methods in Cocoar.Capabilities. For usage examples and patterns, see the [guides](guides/) and [examples](examples/) sections. \ No newline at end of file +```csharp +// Layer capabilities as decorators +var composition = scope.For(service) + .Add(new LoggingDecorator(), order: 1) + .Add(new CachingDecorator(), order: 2) + .Add(new ValidationDecorator(), order: 3) + .Build(); +``` diff --git a/docs/boolean-flag-usage-examples.md b/docs/boolean-flag-usage-examples.md deleted file mode 100644 index b912d90..0000000 --- a/docs/boolean-flag-usage-examples.md +++ /dev/null @@ -1,145 +0,0 @@ -## Boolean Flag System Usage Examples - -This document demonstrates the new boolean flag-based registry control system with method-level overrides. - -### 1. Default Behavior (Enabled by Default) - -```csharp -// Create context with default settings (both registries enabled by default) -var context = new CapabilityContext(); - -var document = new Document("example.txt"); - -// Auto-registration happens by default -var composer = context.For(document); -var composition = composer.Add(new MetadataCapability("author", "John")).Build(); - -// Both are automatically registered and accessible via API -context.Composers.TryGet(document, out var foundComposer); // Returns true -context.Compositions.TryGet(document, out var foundComposition); // Returns true -``` - -### 2. Explicitly Disable Registries - -```csharp -// Create context with both registries disabled -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -var document = new Document("example.txt"); - -// No auto-registration happens -var composer = context.For(document); -var composition = composer.Add(new MetadataCapability("author", "John")).Build(); - -// Nothing is registered -context.Composers.TryGet(document, out var foundComposer); // Returns false -context.Compositions.TryGet(document, out var foundComposition); // Returns false -``` - -### 3. Method-Level Override (Enable for Specific Operations) - -```csharp -// Create context with both registries disabled by default -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -var document = new Document("example.txt"); - -// Override the default behavior for this specific operation -var composer = context.For(document, useRegistry: true); -var composition = composer.Add(new MetadataCapability("author", "John")).Build(useRegistry: true); - -// Both are now registered despite default settings -context.Composers.TryGet(document, out var foundComposer); // Returns true -context.Compositions.TryGet(document, out var foundComposition); // Returns true -``` - -### 4. Method-Level Override (Disable for Specific Operations) - -```csharp -// Create context with both registries enabled by default -var context = new CapabilityContext(); // Default: UseComposerRegistry = true, UseCompositionRegistry = true - -var document1 = new Document("example1.txt"); -var document2 = new Document("example2.txt"); - -// Disable registration for specific operations -var composer1 = context.For(document1, useRegistry: false); -var composition1 = composer1.Add(new MetadataCapability("author", "John")).Build(useRegistry: false); - -// Use default behavior (enabled) -var composer2 = context.For(document2); -var composition2 = composer2.Add(new MetadataCapability("author", "Jane")).Build(); - -// Only document2 is registered -context.Composers.TryGet(document1, out var foundComposer1); // Returns false -context.Compositions.TryGet(document1, out var foundComposition1); // Returns false - -context.Composers.TryGet(document2, out var foundComposer2); // Returns true -context.Compositions.TryGet(document2, out var foundComposition2); // Returns true -``` - -### 5. Registry API with Overrides - -```csharp -// Context with registries disabled by default -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -var document = new Document("example.txt"); - -// Use registry API with override to enable registration -var composer = context.Composers.GetOrCreate(document, useRegistry: true); -var composition = context.Compositions.GetOrCreateEmpty(document, useRegistry: true); - -// Subsequent calls return the same instances -var sameComposer = context.Composers.GetOrCreate(document, useRegistry: true); -var sameComposition = context.Compositions.GetOrCreateEmpty(document, useRegistry: true); - -Assert.Same(composer, sameComposer); -Assert.Same(composition, sameComposition); -``` - -### 6. Mixed Scenarios - -```csharp -// Enable composer registry but disable composition registry -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = true, - UseCompositionRegistry = false -}); - -var document = new Document("example.txt"); - -// Only composer is auto-registered by default -var composer = context.For(document); -var composition = composer.Add(new MetadataCapability("author", "John")).Build(); - -context.Composers.TryGet(document, out var foundComposer); // Returns true -context.Compositions.TryGet(document, out var foundComposition); // Returns false - -// Override composition registration for this specific operation -var anotherComposition = composer.Add(new MetadataCapability("editor", "Jane")).Build(useRegistry: true); - -context.Compositions.TryGet(document, out var foundComposition2); // Returns true now -``` - -### Key Benefits - -1. **Explicit Control**: Boolean flags make registry behavior explicit and predictable -2. **Method-Level Granularity**: Per-operation control via method parameters -3. **Always Available**: Registries are always available, eliminating null checks -4. **Backward Compatibility**: Default settings maintain existing behavior -5. **Fine-Grained Control**: Mix and match settings per registry type and per operation -6. **Clear API**: `IsEnabledByDefault` property makes the configuration transparent \ No newline at end of file diff --git a/docs/complete-public-api-reference.md b/docs/complete-public-api-reference.md deleted file mode 100644 index 9a2ab47..0000000 --- a/docs/complete-public-api-reference.md +++ /dev/null @@ -1,326 +0,0 @@ -# Complete Public API Reference - -This document provides a comprehensive list of all public APIs available in **Cocoar.Capabilities v1.0.0**. - -## Package Information - -**Package**: `Cocoar.Capabilities` -**Namespace**: `Cocoar.Capabilities` -**Target Frameworks**: .NET 8.0+ -**Dependencies**: None - -## Core Interfaces - -### ICapability -```csharp -public interface ICapability { } -``` -Base marker interface for all capabilities. - -### ICapability<in TSubject> -```csharp -public interface ICapability : ICapability { } -``` -Generic capability interface that defines a capability for a specific subject type. - -### IPrimaryCapability<in T> -```csharp -public interface IPrimaryCapability : ICapability { } -``` -Marker interface for primary capabilities. Only one primary capability per subject allowed. - -### IOrderedCapability -```csharp -public interface IOrderedCapability -{ - int Order { get; } -} -``` -Interface for capabilities that need specific ordering within their type group. Lower values execute first. - -### IComposition -```csharp -public interface IComposition -{ - object Subject { get; } - int TotalCapabilityCount { get; } -} -``` -Non-generic interface for accessing basic composition information. - -### IComposition<TSubject> -```csharp -public interface IComposition : IComposition -{ - new TSubject Subject { get; } - - // Primary capability methods - bool HasPrimary(); - bool HasPrimary() where TPrimaryCapability : class, IPrimaryCapability; - bool TryGetPrimary(out IPrimaryCapability primary); - IPrimaryCapability? GetPrimaryOrDefault(); - IPrimaryCapability GetPrimary(); - bool TryGetPrimaryAs(out TPrimaryCapability primary) where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability? GetPrimaryOrDefaultAs() where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability GetRequiredPrimaryAs() where TPrimaryCapability : class, IPrimaryCapability; - - // Capability query methods - IReadOnlyList GetAll() where TCapability : class, ICapability; - IReadOnlyList> GetAll(); - bool Has() where TCapability : class, ICapability; - int Count() where TCapability : class, ICapability; -} -``` -Generic interface for typed access to capabilities attached to a subject. - -## Main Classes - -### CapabilityScope -```csharp -public sealed class CapabilityScope : IDisposable -{ - // Constructor - public CapabilityScope(CapabilityScopeOptions? options = null); - - // Properties - public ComposerRegistryApi Composers { get; } - public CompositionRegistryApi Compositions { get; } - internal bool IsDisposed { get; } - - // Methods - public Composer For(TSubject subject, bool? useRegistry = null) where TSubject : notnull; - public Composer Recompose(IComposition composition, bool? useRegistry = null) where TSubject : notnull; - public void Dispose(); -} -``` -Main entry point for creating and managing capability compositions within a scope. - -### Composer<TSubject> -```csharp -public sealed class Composer where TSubject : notnull -{ - // Property - public TSubject Subject { get; } - - // Basic registration - public Composer Add(ICapability capability); - - // Contract registration - public Composer AddAs(ICapability capability) where TContract : class, ICapability; - public Composer AddAs(ICapability capability) where TContracts : ITuple; - - // Conditional registration - public Composer TryAdd(TCapability capability) where TCapability : class, ICapability; - public Composer TryAddAs(ICapability capability) where TContract : class, ICapability; - - // Capability removal - public Composer RemoveWhere(Func, bool> predicate); - - // Primary capability management - public Composer WithPrimary(IPrimaryCapability? primary); - - // Query builder state - public bool HasPrimary(); - public bool Has() where TCapability : class, ICapability; - - // Build composition - public IComposition Build(bool? useRegistry = null); -} -``` -Fluent builder for capability registration and composition creation. - -## Configuration - -### CapabilityScopeOptions -```csharp -public record CapabilityScopeOptions -{ - public bool UseComposerRegistry { get; init; } = true; - public bool UseCompositionRegistry { get; init; } = true; - public IReadOnlyList SubjectKeyMappers { get; init; } = Array.Empty(); -} -``` -Configuration options for `CapabilityScope` behavior. - -### ISubjectKeyMapper -```csharp -public interface ISubjectKeyMapper -{ - bool CanMap(Type subjectType); - string MapToKey(object subject); -} -``` -Interface for custom subject key mapping strategies. - -## Registry APIs - -### ComposerRegistryApi -```csharp -public class ComposerRegistryApi : IDisposable -{ - // Find methods - public bool TryFind(TSubject subject, out Composer composer) where TSubject : notnull; - public Composer? FindOrDefault(TSubject subject) where TSubject : notnull; - public Composer FindRequired(TSubject subject) where TSubject : notnull; - - // Removal - public bool Remove(TSubject subject) where TSubject : notnull; - public bool Remove(object subject); - - // Disposal - public void Dispose(); -} -``` -Scope-level registry for composer lookup and management. - -### CompositionRegistryApi -```csharp -public class CompositionRegistryApi : IDisposable -{ - // Generic subject lookup - public bool TryFind(TSubject subject, out IComposition composition) where TSubject : notnull; - public IComposition? FindOrDefault(TSubject subject) where TSubject : notnull; - public IComposition FindRequired(TSubject subject) where TSubject : notnull; - - // Non-generic subject lookup - public bool TryFind(object subject, out IComposition composition); - public IComposition? FindOrDefault(object subject); - public IComposition FindRequired(object subject); - - // Removal - public bool Remove(TSubject subject) where TSubject : notnull; - public bool Remove(object subject); - - // Disposal - public void Dispose(); -} -``` -Scope-level registry for composition lookup and management. - -## Extension Methods - -### ReadOnlyListExtensions -```csharp -public static class ReadOnlyListExtensions -{ - public static void ForEach(this IReadOnlyList list, Action action); -} -``` -Utility extensions for capability collections. - -## Internal Interfaces (Advanced Usage) - -### ICapabilityRegistry -```csharp -public interface ICapabilityRegistry : IDisposable -{ - void RegisterComposer(Composer composer) where TSubject : notnull; - bool TryFindComposer(TSubject subject, out Composer composer) where TSubject : notnull; - bool RemoveComposer(TSubject subject) where TSubject : notnull; - bool RemoveComposer(object subject); - - void RegisterComposition(TSubject subject, IComposition composition) where TSubject : notnull; - bool TryFindComposition(TSubject subject, out IComposition composition) where TSubject : notnull; - bool TryFindComposition(object subject, out IComposition composition); - bool RemoveComposition(TSubject subject) where TSubject : notnull; - bool RemoveComposition(object subject); -} -``` - -### IComposerRegistry -```csharp -public interface IComposerRegistry : IDisposable -{ - void Register(Composer composer) where TSubject : notnull; - bool TryFind(TSubject subject, out Composer composer) where TSubject : notnull; - bool Remove(TSubject subject) where TSubject : notnull; - bool Remove(object subject); -} -``` - -### ICompositionRegistry -```csharp -public interface ICompositionRegistry : IDisposable -{ - void Register(TSubject subject, IComposition composition) where TSubject : notnull; - bool TryFind(TSubject subject, out IComposition composition) where TSubject : notnull; - bool TryFind(object subject, out IComposition composition); - bool Remove(TSubject subject) where TSubject : notnull; - bool Remove(object subject); -} -``` - -## Usage Examples - -### Basic Usage -```csharp -using var scope = new CapabilityScope(); -var subject = new MyClass(); - -var composition = scope.For(subject) - .Add(new LoggingCapability(LogLevel.Info)) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Build(); - -// Query capabilities -var hasLogging = composition.Has>(); -var allCapabilities = composition.GetAll>(); -``` - -### Contract Registration -```csharp -using var scope = new CapabilityScope(); -var validator = new EmailValidator(); - -var composition = scope.For(user) - .AddAs>(validator) - .Build(); - -// Query by contract -var validators = composition.GetAll>(); -``` - -### Primary Capabilities -```csharp -using var scope = new CapabilityScope(); -var composition = scope.For(service) - .WithPrimary(new DatabasePrimaryCapability()) - .Add(new LoggingCapability(LogLevel.Debug)) - .Build(); - -if (composition.TryGetPrimary(out var primary)) -{ - // Handle primary capability -} -``` - -### Scope Registry -```csharp -using var scope = new CapabilityScope(); -var composition = scope.For(subject) - .Add(new SomeCapability()) - .Build(); - -// Find later via scope -var found = scope.Compositions.FindOrDefault(subject); -``` - -## Exception Types - -- **InvalidOperationException**: Multiple primary capabilities, builder used after Build(), required capabilities not found -- **ArgumentException**: Invalid contract types, recomposition with invalid types -- **ArgumentNullException**: Null subjects or capabilities -- **ObjectDisposedException**: Using disposed scope - -## Performance Characteristics - -- **Registration**: O(1) for single capabilities, O(k) for tuple registration -- **Query**: O(1) for capability lookup, O(n) for GetAll() where n = capabilities of that type -- **Memory**: Array-based storage, minimal overhead per composition -- **Threading**: Thread-safe through immutability, no locks required - ---- - -**Version**: 1.0.0 -**Last Updated**: October 7, 2025 -**Documentation Status**: βœ… Current \ No newline at end of file diff --git a/docs/core-concepts.md b/docs/core-concepts.md deleted file mode 100644 index d9c5e07..0000000 --- a/docs/core-concepts.md +++ /dev/null @@ -1,301 +0,0 @@ -# Core Concepts & Architecture - -Understanding the foundational principles and architectural patterns behind Cocoar.Capabilities. - -## The Capability Composition Pattern - -**Cocoar.Capabilities implements Capability Composition**: An architectural pattern where objects carry typed, immutable collections of capabilities (behaviors/policies). This enables composition-over-inheritance with type safety and cross-project extensibility. - -### Core Philosophy - -> **"Any object can be extended with typed behaviors without modification"** - -Instead of changing classes or creating inheritance hierarchies, you **attach capabilities** that define how objects should behave in different contexts. - -## Architectural Principles - -1. **πŸ”§ Composition over Inheritance** - Extend behavior by attaching capabilities, not extending classes -2. **πŸ”’ Type Safety** - Compile-time guarantees for capability-subject relationships -3. **🧊 Immutability** - Thread-safe by design, no locks needed -4. **πŸ”Œ Cross-Project Extensibility** - Any library can define capabilities for any subject type -5. **⚑ High Performance** - ~140ns queries, ~4.6ΞΌs builds (Core), registry overhead available when needed -6. **πŸ“ Contract-Only Semantics** - Capabilities only queryable by explicitly registered types - -## Pattern Mapping - -| Concept | Role | Example | -|---------|------|---------| -| **Subject** | Host object | `DatabaseConfig`, `UserService`, `PaymentController` | -| **Capability** | Behavior/Policy | `LoggingCapability`, `CachingCapability`, `ValidationCapability` | -| **Composition** | Capability Container | Immutable container with type-safe lookup | -| **Composer** | Builder | Fluent API for capability registration | -| **Contract Registration** | Interface Binding | Register concrete under interface contracts | -| **Primary Capability** | Identity Marker | Exactly one "core identity" per subject | - -## Related Design Patterns - -**Cocoar.Capabilities** implements and enables several established patterns: - -- **🎭 Extension Object Pattern** - Dynamically extend objects with new interfaces -- **πŸŽͺ Role Object Pattern** - Objects play different roles in different contexts -- **🧩 Component-Based Architecture** - Compose behavior from reusable components -- **πŸ“‹ Strategy/Policy Pattern** - Encapsulate algorithms as attachable capabilities -- **🎨 Decorator Pattern** - Attach (not wrap) additional responsibilities -- **🏭 Registry Pattern** - Global capability discovery and lookup - -## Core Architecture - -### Subjects (Any Object) - -A **subject** is any object that can have capabilities attached. No special interfaces or inheritance required: - -```csharp -// Value types -var userId = 12345; -var status = OrderStatus.Pending; -var point = new Point(10, 20); - -// Reference types -var userService = new UserService(); -var config = new DatabaseConfig(); -var controller = new PaymentController(); - -// Even reflection objects -var method = typeof(UserService).GetMethod("CreateUser"); -var property = typeof(User).GetProperty("Email"); -``` - -**Design Decision**: Universal subject support maximizes compatibility and reduces coupling. - -### Capabilities (Behaviors/Policies) - -A **capability** represents functionality, configuration, or policy attachable to subjects: - -```csharp -// Generic capabilities - work with any subject type T -public record LoggingCapability(LogLevel Level, string Category) : ICapability; -public record CachingCapability(TimeSpan Duration) : ICapability; - -// Specific capabilities - work with specific subject types -public record UserPermissionCapability(string[] Roles) : ICapability; -public record ConnectionStringCapability(string ConnectionString) : ICapability; - -// Interface-based capabilities for contract enforcement -public interface IValidationCapability : ICapability -{ - bool IsValid(T subject); -} - -// Primary capabilities for identity -public record DatabasePrimaryCapability : IPrimaryCapability; -``` - -**Design Decision**: Generic type parameters ensure type safety and prevent incompatible capability attachment. - -### Compositions (Immutable Containers) - -A **composition** is an immutable container storing all capabilities for a specific subject. Each `CapabilityScope` owns an isolated in-memory registry; registries are no longer injectable or replaceable. This guarantees invariant behavior and prevents accidental divergence caused by custom registry implementations. - -```csharp -// Build composition -var composition = Composer.For(userService) - .Add(new LoggingCapability(LogLevel.Info, "UserManagement")) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .AddAs>(new UserValidationCapability()) - .Build(); // ← Immutable from this point - -// Query capabilities -var loggers = composition.GetAll>(); -var cache = composition.GetAll>().FirstOrDefault(); -var validators = composition.GetAll>(); -``` - -**Design Decision**: Immutability provides thread safety without locks and prevents accidental modification. - -### Subject Identity & Canonicalization - -Subjects are classified as value-like or reference-like for storage. Value-like subjects (value types and canonicalized references such as `string`) are stored in a strong keyed dictionary; reference-like subjects are stored via weak references (automatic cleanup when the subject is collected). - -`string` subjects receive **value semantics** through an internal canonicalization layer: two distinct string instances with identical content map to the same composition. This is implemented via a per-scope `SubjectKeyCanonicalizer`. - -You can customize string canonicalization (e.g. case-insensitive, trimming) per scope by providing one or more `ISubjectKeyMapper` implementations in `CapabilityScopeOptions.SubjectKeyMappers`: - -```csharp -public sealed class CaseInsensitiveTrimMapper : ISubjectKeyMapper -{ - public bool CanHandle(Type t) => t == typeof(string); - public object Map(object subject) => new StringSubjectKey(((string)subject).Trim().ToUpperInvariant()); -} - -var scope = new CapabilityScope(new CapabilityScopeOptions -{ - SubjectKeyMappers = new ISubjectKeyMapper[] { new CaseInsensitiveTrimMapper() } -}); - -// These refer to the same canonical subject -var a = scope.For(" foo ").Add(new LoggingCapability(LogLevel.Info)).Build(); -var b = scope.Compositions.FindOrDefault("FOO"); // same composition -``` - -Only string mappers are currently recognized; additional reference-type canonicalization may be added in the future if real use cases emerge. - -## Type System Design - -### Contract-Only Registration Semantics - -The system uses **contract-only semantics** - capabilities are only queryable by the exact types they were registered under: - -```csharp -public class EmailValidator : IValidationCapability -{ - public bool IsValid(User user) => IsValidEmail(user.Email); -} - -var validator = new EmailValidator(); - -// Scenario 1: Concrete registration -composer.Add(validator); // Registered as EmailValidator only -composition.GetAll(); // βœ… Found -composition.GetAll>(); // ❌ NOT Found - -// Scenario 2: Interface registration -composer.AddAs>(validator); // Registered as interface only -composition.GetAll(); // ❌ NOT Found -composition.GetAll>(); // βœ… Found - -// Scenario 3: Multiple registration (tuple syntax) -composer.AddAs<(IValidationCapability, EmailValidator)>(validator); -composition.GetAll(); // βœ… Found -composition.GetAll>(); // βœ… Found -``` - -**Benefits**: -- **🎯 Predictable**: You get exactly what you registered for -- **⚑ Performance**: No expensive type hierarchy walking -- **πŸ” Explicit**: Clear intent when using interfaces vs concrete types -- **🚫 No Interface Contamination**: Prevents accidental queryability - -### Memory Management Strategy - -The system implements **dual storage** based on subject type: - -```csharp -// Value types: Strong references (manual cleanup) -var number = 42; -var composition = Composer.For(number).Add(capability).Build(); -// Stays in memory until: Composition.Remove(number) - -// Reference types: Weak references (automatic cleanup) -var service = new UserService(); -var composition = Composer.For(service).Add(capability).Build(); -// Automatically cleaned up when service is garbage collected -``` - -**Design Decision**: Different strategies optimize for value type immutability and reference type lifecycle management. String subjects are canonicalized to behave like value types for lookup and removal consistency. - -## Advanced Concepts - -### Primary Capabilities - -**Primary capabilities** represent the core identity or main behavior of a subject. Exactly one primary capability may exist per subject. - -Registration rules (fail-fast enforced): - -1. First primary can be registered via any of: Add, AddAs>, AddAs<(contracts including IPrimaryCapability)>, or WithPrimary(primary) -2. To replace an existing primary you MUST call WithPrimary(newPrimary) -3. Add / AddAs attempts after a primary exists throw InvalidOperationException -4. WithPrimary(null) removes the existing primary -5. A tuple contract cannot contain more than one IPrimaryCapability marker (throws) -6. TryAdd / TryAddAs involving a new primary after one exists silently no-op (never replace) - -```csharp -// First time registration (any method OK) -composer.Add(new DatabasePrimaryCapability()); - -// Replacement (must use WithPrimary) -composer.WithPrimary(new DatabasePrimaryCapability()); - -// Removal -composer.WithPrimary(null); - -// Query primary capability -if (composition.TryGetPrimary(out var primary)) -{ - // Use primary behavior -} -``` - -Why WithPrimary for replacement? It makes intent explicit and prevents accidental overwrites hidden among many Add(...) calls. - -**Use Cases**: Configuration strategies, core behaviors, identity markers. - -### Capability Ordering - -**Ordered capabilities** enable deterministic processing sequences: - -```csharp -public record OrderedCapability(int Priority) : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -// Lower Order values execute first -composer.Add(new OrderedCapability(-10)); // First -composer.Add(new OrderedCapability(0)); // Second -composer.Add(new OrderedCapability(100)); // Third - -// GetAll() returns capabilities sorted by Order -var ordered = composition.GetAll>(); // Auto-sorted -``` - -**Use Cases**: Middleware pipelines, event handlers, processing chains. - -### Cross-Project Extensibility - -**Extension methods** enable cross-project capability registration: - -```csharp -// Core project -public static class CoreExtensions -{ - public static Composer AddLogging(this Composer composer, LogLevel level) - => composer.Add(new LoggingCapability(level)); -} - -// DI project (no circular dependency) -public static class DIExtensions -{ - public static Composer AsSingleton(this Composer composer) - => composer.Add(new SingletonLifetimeCapability()); -} - -// Usage: Both extensions work together -var composition = Composer.For(service) - .AddLogging(LogLevel.Info) // Core project extension - .AsSingleton() // DI project extension - .Build(); -``` - -**Benefits**: Clean separation, no circular dependencies, unified API. - -## Performance Characteristics - -### Registration Performance -- **Single registration**: O(1) -- **Tuple registration**: O(k) where k = number of contracts -- **Primary validation**: O(1) at build time - -### Query Performance -- **Single capability**: O(1) array lookup -- **Multiple capabilities**: O(n) where n = capabilities of requested type -- **Ordering**: O(n log n) when ordered capabilities are present - -### Memory Usage -- **Compositions**: One per subject (not per instance) -- **Capabilities**: Shared references, no duplication -- **Value types**: Strong references until explicit cleanup -- **Reference types**: Automatic cleanup via weak references - ---- - -**The capability composition pattern provides a powerful foundation for building extensible, type-safe systems without the complexity of traditional inheritance hierarchies.** \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..6700841 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,729 @@ +# Cocoar.Capabilities - Examples + +This document provides detailed examples and use cases for the Cocoar.Capabilities library. + +## Table of Contents + +- [Basic Examples](#basic-examples) +- [Primary Capabilities](#primary-capabilities) +- [Multiple Contracts](#multiple-contracts) +- [Capability Ordering](#capability-ordering) +- [Recomposition](#recomposition) +- [Registry Management](#registry-management) +- [Try-Add Pattern](#try-add-pattern) +- [Real-World Use Cases](#real-world-use-cases) + +## Basic Examples + +### Simple Composition + +```csharp +using Cocoar.Capabilities; + +// Create a scope +using var scope = new CapabilityScope(); + +// Define your subject +var document = new Document("README.md"); + +// Compose capabilities +var composition = scope.For(document) + .Add(new EditCapability()) + .Add(new PrintCapability()) + .Add(new ShareCapability()) + .Build(); + +// Retrieve and use capabilities +var editCapabilities = composition.GetAll(); +foreach (var cap in editCapabilities) +{ + cap.Edit(document); +} + +// Check if a capability exists +if (composition.Has()) +{ + Console.WriteLine("Document can be printed"); +} +``` + +### Working with Multiple Capabilities of Same Type + +```csharp +// Add multiple validation capabilities +var composition = scope.For(formData) + .Add(new ValidationCapability("Email", EmailValidator)) + .Add(new ValidationCapability("Phone", PhoneValidator)) + .Add(new ValidationCapability("ZipCode", ZipValidator)) + .Build(); + +// Process all validators +var validators = composition.GetAll(); +var allValid = validators.All(v => v.Validate()); +``` + +## Primary Capabilities + +### Basic Primary Capability + +```csharp +public record UserPrimaryCapability(string UserId, string Name) : IPrimaryCapability; +public record AdminCapability(string AdminLevel); +public record AuditCapability(DateTime LastAudit); + +var user = new User("user123"); + +var composition = scope.For(user) + .Add(new UserPrimaryCapability("user123", "John Doe")) + .Add(new AdminCapability("Level2")) + .Add(new AuditCapability(DateTime.Now)) + .Build(); + +// Retrieve the primary capability +var primary = composition.GetPrimary(); +Console.WriteLine($"User: {((UserPrimaryCapability)primary).Name}"); + +// Type-safe retrieval +if (composition.TryGetPrimaryAs(out var userPrimary)) +{ + Console.WriteLine($"User ID: {userPrimary.UserId}"); +} + +// Or use GetRequiredPrimaryAs for guaranteed non-null +var typedPrimary = composition.GetRequiredPrimaryAs(); +Console.WriteLine($"Name: {typedPrimary.Name}"); +``` + +### Primary Capability Validation + +```csharp +// Only one primary capability is allowed +var composition = scope.For(subject) + .Add(new PrimaryCapabilityA("A")); + +// This will throw InvalidOperationException +try +{ + composition.Add(new PrimaryCapabilityB("B")); +} +catch (InvalidOperationException ex) +{ + Console.WriteLine(ex.Message); // "A primary capability is already set..." +} +``` + +### Replacing Primary Capability via Recomposition + +```csharp +var initial = scope.For(user) + .Add(new GuestPrimaryCapability("guest123")) + .Build(); + +// Later, upgrade to registered user +var upgraded = scope.Recompose(initial) + .WithPrimary(new RegisteredUserPrimaryCapability("user123", "John Doe")) + .Add(new PreferencesCapability()) + .Build(); +``` + +## Multiple Contracts + +### Single Capability, Multiple Interfaces + +```csharp +public interface IValidator +{ + bool Validate(); +} + +public interface IFormatter +{ + string Format(); +} + +public class DataProcessor : IValidator, IFormatter +{ + public bool Validate() => true; + public string Format() => "Formatted Data"; +} + +var processor = new DataProcessor(); + +var composition = scope.For(myObject) + .AddAs<(IValidator, IFormatter)>(processor) + .Build(); + +// Access via either contract - both return the same instance +var validator = composition.GetAll().First(); +var formatter = composition.GetAll().First(); + +Console.WriteLine(ReferenceEquals(validator, formatter)); // True +``` + +### Multiple Contracts with Three or More Interfaces + +```csharp +public interface IReadable { } +public interface IWritable { } +public interface ISearchable { } + +public class DataStore : IReadable, IWritable, ISearchable +{ + // Implementation +} + +var store = new DataStore(); + +var composition = scope.For(database) + .AddAs<(IReadable, IWritable, ISearchable)>(store) + .Build(); + +// Available under all three contracts +var readers = composition.GetAll(); +var writers = composition.GetAll(); +var searchers = composition.GetAll(); +``` + +## Capability Ordering + +### Explicit Order Values + +```csharp +// Lower numbers = higher priority (returned first) +var composition = scope.For(subject) + .Add(new LoggingCapability(), order: 10) + .Add(new ValidationCapability(), order: 5) + .Add(new ProcessingCapability(), order: 20) + .Add(new AuditCapability(), order: 15) + .Build(); + +var all = composition.GetAll(); +// Order: ValidationCapability (5), LoggingCapability (10), +// AuditCapability (15), ProcessingCapability (20) +``` + +### Order Selectors + +```csharp +public record TaskCapability(string Name, int Priority); + +var composition = scope.For(taskList) + .Add(new TaskCapability("High Priority", 1), cap => cap.Priority) + .Add(new TaskCapability("Normal Priority", 5), cap => cap.Priority) + .Add(new TaskCapability("Low Priority", 10), cap => cap.Priority) + .Build(); + +// Tasks retrieved in priority order +var tasks = composition.GetAll(); +``` + +### Property-Based Ordering + +```csharp +public record OrderedCapability +{ + public string Name { get; init; } + public int ExecutionOrder { get; init; } +} + +var composition = scope.For(pipeline) + .Add(new OrderedCapability { Name = "Step1", ExecutionOrder = 10 }, + cap => cap.ExecutionOrder) + .Add(new OrderedCapability { Name = "Step2", ExecutionOrder = 5 }, + cap => cap.ExecutionOrder) + .Add(new OrderedCapability { Name = "Step3", ExecutionOrder = 15 }, + cap => cap.ExecutionOrder) + .Build(); +``` + +## Recomposition + +### Basic Recomposition + +```csharp +var initialComposition = scope.For(document) + .Add(new ReadCapability()) + .Build(); + +// Later, add more capabilities +var updatedComposition = scope.Recompose(initialComposition) + .Add(new WriteCapability()) + .Add(new DeleteCapability()) + .Build(); + +// Original remains unchanged (immutable) +Console.WriteLine(initialComposition.TotalCapabilityCount); // 1 +Console.WriteLine(updatedComposition.TotalCapabilityCount); // 3 +``` + +### Conditional Recomposition + +```csharp +var composition = scope.For(user).Add(new BasicUserCapability()).Build(); + +// Add admin capabilities if user is admin +if (userIsAdmin) +{ + composition = scope.Recompose(composition) + .Add(new AdminCapability()) + .Add(new ModeratorCapability()) + .Build(); +} + +// Add premium features if subscribed +if (hasPremiumSubscription) +{ + composition = scope.Recompose(composition) + .Add(new PremiumFeaturesCapability()) + .Build(); +} +``` + +### Recomposition with Ordering Changes + +```csharp +var initial = scope.For(pipeline) + .Add(new StepA(), 1) + .Add(new StepB(), 2) + .Build(); + +// Insert a new step with higher priority +var modified = scope.Recompose(initial) + .Add(new ValidationStep(), 0) // Runs first now + .Build(); +``` + +## Registry Management + +### Basic Registry Usage + +```csharp +// Enable registries +var options = new CapabilityScopeOptions +{ + UseComposerRegistry = true, + UseCompositionRegistry = true +}; + +using var scope = new CapabilityScope(options); + +// Build with registry +var composition = scope.For(document) + .Add(new EditCapability()) + .Build(); // Automatically registered + +// Find compositions later +var found = scope.Compositions.FindOrDefault(document); +if (found != null) +{ + var capabilities = found.GetAll(); +} +``` + +### Registry Lookup Patterns + +```csharp +// Check if composition exists +if (scope.Compositions.Has(document)) +{ + var comp = scope.Compositions.Find(document); + // Work with composition +} + +// Find or return null +var comp = scope.Compositions.FindOrDefault(document); +if (comp != null) +{ + // Use composition +} + +// Try pattern +if (scope.Compositions.TryFind(document, out var composition)) +{ + // Use composition +} +``` + +### Composer Registry + +```csharp +var options = new CapabilityScopeOptions { UseComposerRegistry = true }; +using var scope = new CapabilityScope(options); + +// Start composing +var composer = scope.For(document); // Registered automatically + +// Check if a composer is active +if (scope.Composers.Has(document)) +{ + Console.WriteLine("Document is currently being composed"); +} + +// Retrieve the composer +if (scope.Composers.TryGet(document, out var activeComposer)) +{ + // Can inspect or modify the active composer +} + +// Building removes from composer registry, adds to composition registry +var composition = composer.Add(new Capability()).Build(); +``` + +### Per-Operation Registry Override + +```csharp +var options = new CapabilityScopeOptions +{ + UseComposerRegistry = false, + UseCompositionRegistry = false +}; + +using var scope = new CapabilityScope(options); + +// Override: use registry for this specific operation +var composition = scope.For(subject, useRegistry: true) + .Add(capability) + .Build(useRegistry: true); + +// This composition IS registered despite global settings +var found = scope.Compositions.FindOrDefault(subject); +Console.WriteLine(found != null); // True +``` + +## Try-Add Pattern + +### Basic Try-Add + +```csharp +var composition = scope.For(document) + .Add(new LoggingCapability()) + .TryAdd(new LoggingCapability()) // Won't add duplicate + .TryAdd(new MetricsCapability()) // Will add (doesn't exist yet) + .Build(); + +Console.WriteLine(composition.GetAll().Count); // 1 +Console.WriteLine(composition.GetAll().Count); // 1 +``` + +### Conditional Capability Addition + +```csharp +var composer = scope.For(user); + +// Add base capabilities +composer.Add(new UserProfileCapability()); + +// Try to add premium features (won't duplicate if already present) +composer.TryAdd(new PremiumFeatureCapability()); + +// Try to add admin capabilities +if (userIsAdmin) +{ + composer.TryAdd(new AdminCapability()); +} + +var composition = composer.Build(); +``` + +### Try-Add with Ordering + +```csharp +var composition = scope.For(pipeline) + .Add(new StepA(), 10) + .TryAdd(new StepA(), 5) // Won't add, already exists + .TryAdd(new StepB(), 20) // Will add + .Build(); +``` + +## Real-World Use Cases + +### Plugin Architecture + +```csharp +public interface IPlugin +{ + string Name { get; } + void Initialize(); +} + +public record EditorPlugin(string Name) : IPlugin +{ + public void Initialize() => Console.WriteLine($"Editor: {Name}"); +} + +public record ExporterPlugin(string Name, string Format) : IPlugin +{ + public void Initialize() => Console.WriteLine($"Exporter: {Name} ({Format})"); +} + +// Application setup +using var scope = new CapabilityScope(); +var app = new Application(); + +var composition = scope.For(app) + .Add(new EditorPlugin("TextEditor")) + .Add(new EditorPlugin("CodeEditor")) + .Add(new EditorPlugin("MarkdownEditor")) + .Add(new ExporterPlugin("PDFExporter", "PDF")) + .Add(new ExporterPlugin("HTMLExporter", "HTML")) + .Build(); + +// Initialize all plugins +var allPlugins = composition.GetAll(); +foreach (var plugin in allPlugins) +{ + plugin.Initialize(); +} + +// Work with specific plugin types +var editors = composition.GetAll(); +var exporters = composition.GetAll(); +``` + +### Role-Based Access Control + +```csharp +public record UserRole(string RoleName) : IPrimaryCapability; +public record Permission(string Resource, string Action); + +var user = new User("john@example.com"); + +var composition = scope.For(user) + .Add(new UserRole("Admin")) + .Add(new Permission("Users", "Read")) + .Add(new Permission("Users", "Write")) + .Add(new Permission("Users", "Delete")) + .Add(new Permission("Settings", "Read")) + .Add(new Permission("Settings", "Write")) + .Build(); + +// Check specific permission +bool CanPerform(IComposition comp, string resource, string action) +{ + var permissions = comp.GetAll(); + return permissions.Any(p => p.Resource == resource && p.Action == action); +} + +if (CanPerform(composition, "Users", "Delete")) +{ + Console.WriteLine("User can delete users"); +} + +// Get role +var role = composition.GetPrimaryAs(); +Console.WriteLine($"Role: {role?.RoleName}"); +``` + +### Event Processing Pipeline + +```csharp +public interface IEventHandler +{ + Task HandleAsync(object @event); +} + +public record AuditLogger() : IEventHandler +{ + public Task HandleAsync(object @event) + { + Console.WriteLine($"Audit: {@event}"); + return Task.CompletedTask; + } +} + +public record NotificationSender() : IEventHandler +{ + public Task HandleAsync(object @event) + { + Console.WriteLine($"Notify: {@event}"); + return Task.CompletedTask; + } +} + +public record MetricsCollector() : IEventHandler +{ + public Task HandleAsync(object @event) + { + Console.WriteLine($"Metrics: {@event}"); + return Task.CompletedTask; + } +} + +// Setup event processing +var order = new Order("ORD-001"); + +var composition = scope.For(order) + .Add(new AuditLogger(), order: 1) + .Add(new NotificationSender(), order: 2) + .Add(new MetricsCollector(), order: 3) + .Build(); + +// Process event through all handlers in order +async Task ProcessEvent(IComposition comp, object @event) +{ + var handlers = comp.GetAll(); + foreach (var handler in handlers) + { + await handler.HandleAsync(@event); + } +} + +await ProcessEvent(composition, new OrderCreatedEvent(order)); +``` + +### Dynamic Feature Flags + +```csharp +public interface IFeature +{ + string FeatureName { get; } + bool IsEnabled { get; } +} + +public record ExperimentalFeature(string FeatureName, bool IsEnabled) : IFeature; +public record BetaFeature(string FeatureName, bool IsEnabled) : IFeature; + +var app = new Application(); + +var composer = scope.For(app); + +// Load features from configuration +var featureConfig = LoadFeatureConfiguration(); + +foreach (var (name, enabled) in featureConfig.ExperimentalFeatures) +{ + composer.Add(new ExperimentalFeature(name, enabled)); +} + +foreach (var (name, enabled) in featureConfig.BetaFeatures) +{ + composer.Add(new BetaFeature(name, enabled)); +} + +var composition = composer.Build(); + +// Check if feature is enabled +bool IsFeatureEnabled(IComposition comp, string featureName) +{ + var features = comp.GetAll(); + return features.FirstOrDefault(f => f.FeatureName == featureName)?.IsEnabled ?? false; +} + +if (IsFeatureEnabled(composition, "NewUI")) +{ + // Enable new UI +} +``` + +### Document Processing with Middleware + +```csharp +public interface IDocumentMiddleware +{ + Task ProcessAsync(Document doc); +} + +public record ValidationMiddleware() : IDocumentMiddleware +{ + public Task ProcessAsync(Document doc) + { + Console.WriteLine("Validating document..."); + return Task.CompletedTask; + } +} + +public record TransformationMiddleware() : IDocumentMiddleware +{ + public Task ProcessAsync(Document doc) + { + Console.WriteLine("Transforming document..."); + return Task.CompletedTask; + } +} + +public record PersistenceMiddleware() : IDocumentMiddleware +{ + public Task ProcessAsync(Document doc) + { + Console.WriteLine("Saving document..."); + return Task.CompletedTask; + } +} + +// Setup processing pipeline +var document = new Document("report.pdf"); + +var composition = scope.For(document) + .Add(new ValidationMiddleware(), order: 1) + .Add(new TransformationMiddleware(), order: 2) + .Add(new PersistenceMiddleware(), order: 3) + .Build(); + +// Process through pipeline +var middlewares = composition.GetAll(); +foreach (var middleware in middlewares) +{ + await middleware.ProcessAsync(document); +} +``` + +### Service Decorators + +```csharp +public interface INotificationService +{ + Task SendAsync(string message); +} + +public record EmailNotification() : INotificationService +{ + public Task SendAsync(string message) + { + Console.WriteLine($"Email: {message}"); + return Task.CompletedTask; + } +} + +public record SMSNotification() : INotificationService +{ + public Task SendAsync(string message) + { + Console.WriteLine($"SMS: {message}"); + return Task.CompletedTask; + } +} + +public record PushNotification() : INotificationService +{ + public Task SendAsync(string message) + { + Console.WriteLine($"Push: {message}"); + return Task.CompletedTask; + } +} + +// User preferences determine notification methods +var user = new User("user@example.com"); + +var composer = scope.For(user); + +if (preferences.EmailEnabled) + composer.Add(new EmailNotification()); + +if (preferences.SMSEnabled) + composer.Add(new SMSNotification()); + +if (preferences.PushEnabled) + composer.Add(new PushNotification()); + +var composition = composer.Build(); + +// Send via all enabled channels +async Task NotifyUser(IComposition comp, string message) +{ + var services = comp.GetAll(); + await Task.WhenAll(services.Select(s => s.SendAsync(message))); +} + +await NotifyUser(composition, "Your order has shipped!"); +``` diff --git a/docs/examples/configuration-system.md b/docs/examples/configuration-system.md deleted file mode 100644 index a8f8705..0000000 --- a/docs/examples/configuration-system.md +++ /dev/null @@ -1,539 +0,0 @@ -# Configuration System Example - -A comprehensive example demonstrating how to build a modern configuration system using Cocoar.Capabilities patterns. - -## Overview - -This example shows how to create a flexible, extensible configuration system that supports: -- Multiple configuration sources (files, environment, cloud) -- Environment-specific behaviors -- Feature flags and settings -- Type-safe configuration access -- Real-time configuration updates - -## Core Configuration Model - -```csharp -using Cocoar.Capabilities; - -// Unified configuration key for cross-project extensibility -public readonly record struct ConfigurationKey(string Section, string Key) -{ - public override string ToString() => $"{Section}:{Key}"; -} - -// Configuration value wrapper -public readonly record struct ConfigurationValue(object Value, Type ValueType) -{ - public T GetValue() => (T)Value; - public bool TryGetValue(out T value) - { - if (ValueType == typeof(T) && Value is T typedValue) - { - value = typedValue; - return true; - } - value = default(T); - return false; - } -} -``` - -## Primary Capability Strategies - -```csharp -// Environment-based primary capabilities -public record DevelopmentConfigPrimary : IPrimaryCapability; -public record StagingConfigPrimary : IPrimaryCapability; -public record ProductionConfigPrimary : IPrimaryCapability; - -// Source-based primary capabilities -public record FileConfigPrimary(string FilePath) : IPrimaryCapability; -public record EnvironmentConfigPrimary : IPrimaryCapability; -public record CloudConfigPrimary(string Endpoint) : IPrimaryCapability; -public record DatabaseConfigPrimary(string ConnectionString) : IPrimaryCapability; -``` - -## Configuration Capabilities - -```csharp -// Core configuration capabilities -public record ConfigurationValueCapability(ConfigurationValue Value) : ICapability; - -public record DefaultValueCapability(object DefaultValue) : ICapability; - -public record ValidationCapability(Func Validator, string ErrorMessage) : ICapability; - -public record TypeMappingCapability(Type TargetType, Func Converter) : ICapability; - -// Feature flag capability -public record FeatureFlagCapability(bool IsEnabled, DateTime? ExpiresAt = null) : ICapability; - -// Monitoring and observability -public record ConfigurationMonitoringCapability( - DateTime LastAccessed, - DateTime LastModified, - int AccessCount -) : ICapability; - -// Caching capability with ordering -public record ConfigurationCacheCapability( - TimeSpan CacheDuration, - int Priority -) : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -// File watching capability -public record FileWatcherCapability(string FilePath, DateTime LastChanged) : ICapability; - -// Environment-specific overrides -public record EnvironmentOverrideCapability(string Environment, object OverrideValue) : ICapability; -``` - -## Configuration Service Implementation - -```csharp -public class ConfigurationService -{ - private readonly ILogger _logger; - - public ConfigurationService(ILogger logger) - { - _logger = logger; - } - - // Initialize configuration for a key - public void ConfigureKey(string section, string key, object value, string environment = "Development") - { - var configKey = new ConfigurationKey(section, key); - - var composer = Composer.For(configKey); - - // Set primary capability based on environment - composer = environment switch - { - "Development" => composer.WithPrimary(new DevelopmentConfigPrimary()), - "Staging" => composer.WithPrimary(new StagingConfigPrimary()), - "Production" => composer.WithPrimary(new ProductionConfigPrimary()), - _ => composer.WithPrimary(new DevelopmentConfigPrimary()) - }; - - // Add base configuration value - composer = composer.Add(new ConfigurationValueCapability( - new ConfigurationValue(value, value.GetType()) - )); - - // Add environment-specific capabilities - composer = AddEnvironmentCapabilities(composer, configKey, environment); - - // Add monitoring - composer = composer.Add(new ConfigurationMonitoringCapability( - LastAccessed: DateTime.UtcNow, - LastModified: DateTime.UtcNow, - AccessCount: 0 - )); - - composer.Build(); - - _logger.LogInformation("Configured {Key} for environment {Environment}", configKey, environment); - } - - private Composer AddEnvironmentCapabilities( - Composer composer, - ConfigurationKey key, - string environment) - { - return environment switch - { - "Development" => composer - .Add(new FileWatcherCapability("appsettings.Development.json", DateTime.UtcNow)) - .Add(new ConfigurationCacheCapability(TimeSpan.FromMinutes(1), 300)), // Low priority cache - - "Staging" => composer - .Add(new ConfigurationCacheCapability(TimeSpan.FromMinutes(5), 200)) - .Add(new ValidationCapability(ValidateForStaging, "Staging validation failed")), - - "Production" => composer - .Add(new ConfigurationCacheCapability(TimeSpan.FromMinutes(15), 100)) // High priority cache - .Add(new ValidationCapability(ValidateForProduction, "Production validation failed")), - - _ => composer - }; - } - - // Get configuration value with type safety - public T GetValue(string section, string key, T defaultValue = default(T)) - { - var configKey = new ConfigurationKey(section, key); - var composition = Composition.FindOrDefault(configKey); - - if (composition == null) - { - _logger.LogWarning("Configuration key {Key} not found, using default", configKey); - return defaultValue; - } - - // Update access tracking - UpdateAccessTracking(composition); - - // Check cache first (ordered by priority) - var cacheCapabilities = composition.GetAll>(); - foreach (var cache in cacheCapabilities) - { - if (TryGetFromCache(configKey, cache, out var cachedValue)) - { - _logger.LogDebug("Retrieved {Key} from cache", configKey); - return cachedValue; - } - } - - // Get primary configuration value - var configValues = composition.GetAll>(); - var primaryValue = configValues.FirstOrDefault(); - - if (primaryValue?.Value.TryGetValue(out var value) == true) - { - // Apply environment overrides - value = ApplyEnvironmentOverrides(composition, value); - - // Cache the value - CacheValue(configKey, value, cacheCapabilities); - - _logger.LogDebug("Retrieved {Key} = {Value}", configKey, value); - return value; - } - - // Try default value capability - var defaultCapabilities = composition.GetAll>(); - var defaultCapability = defaultCapabilities.FirstOrDefault(); - - if (defaultCapability != null && defaultCapability.DefaultValue is T defaultVal) - { - _logger.LogDebug("Using default value for {Key}", configKey); - return defaultVal; - } - - _logger.LogWarning("Could not retrieve value for {Key}, using provided default", configKey); - return defaultValue; - } - - // Feature flag support - public bool IsFeatureEnabled(string featureName) - { - var configKey = new ConfigurationKey("Features", featureName); - var composition = Composition.FindOrDefault(configKey); - - if (composition == null) return false; - - var featureFlags = composition.GetAll>(); - var flag = featureFlags.FirstOrDefault(); - - if (flag == null) return false; - - // Check expiration - if (flag.ExpiresAt.HasValue && DateTime.UtcNow > flag.ExpiresAt.Value) - { - _logger.LogInformation("Feature flag {Feature} expired", featureName); - return false; - } - - return flag.IsEnabled; - } - - // Dynamic configuration updates - public void UpdateConfiguration(string section, string key, object newValue) - { - var configKey = new ConfigurationKey(section, key); - var existingComposition = Composition.FindOrDefault(configKey); - - if (existingComposition == null) - { - ConfigureKey(section, key, newValue); - return; - } - - // Recompose with new value - var newComposition = Composer.Recompose(existingComposition) - .RemoveWhere(cap => cap is ConfigurationValueCapability) - .Add(new ConfigurationValueCapability( - new ConfigurationValue(newValue, newValue.GetType()) - )) - .RemoveWhere(cap => cap is ConfigurationMonitoringCapability) - .Add(new ConfigurationMonitoringCapability( - LastAccessed: DateTime.UtcNow, - LastModified: DateTime.UtcNow, - AccessCount: 0 - )) - .Build(); - - // Invalidate cache - InvalidateCache(configKey); - - _logger.LogInformation("Updated configuration {Key} = {Value}", configKey, newValue); - } - - // Configuration validation - private bool ValidateForStaging(object value) => value != null; - - private bool ValidateForProduction(object value) - { - // More strict validation for production - return value != null && !string.IsNullOrWhiteSpace(value.ToString()); - } - - // Helper methods for caching and tracking - private void UpdateAccessTracking(IComposition composition) - { - var monitoring = composition.GetAll>().FirstOrDefault(); - if (monitoring != null) - { - var updated = monitoring with - { - LastAccessed = DateTime.UtcNow, - AccessCount = monitoring.AccessCount + 1 - }; - - var newComposition = Composer.Recompose(composition) - .RemoveWhere(cap => cap is ConfigurationMonitoringCapability) - .Add(updated) - .Build(); - } - } - - private bool TryGetFromCache(ConfigurationKey key, ConfigurationCacheCapability cache, out T value) - { - // Implementation would check actual cache storage - value = default(T); - return false; // Simplified for example - } - - private void CacheValue(ConfigurationKey key, T value, IReadOnlyList> caches) - { - // Implementation would store in actual cache - } - - private void InvalidateCache(ConfigurationKey key) - { - // Implementation would clear cache entries - } - - private T ApplyEnvironmentOverrides(IComposition composition, T value) - { - var overrides = composition.GetAll>(); - var currentEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; - - var override = overrides.FirstOrDefault(o => o.Environment == currentEnv); - if (override?.OverrideValue is T overrideValue) - { - _logger.LogDebug("Applied environment override for {Environment}", currentEnv); - return overrideValue; - } - - return value; - } -} -``` - -## Usage Examples - -### Basic Configuration Setup - -```csharp -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - var configService = new ConfigurationService(logger); - - // Database configuration - configService.ConfigureKey("Database", "ConnectionString", - "Server=localhost;Database=MyApp;", "Development"); - - configService.ConfigureKey("Database", "ConnectionString", - "Server=prod-db;Database=MyApp;", "Production"); - - // API settings - configService.ConfigureKey("API", "BaseUrl", "https://api.dev.com", "Development"); - configService.ConfigureKey("API", "BaseUrl", "https://api.prod.com", "Production"); - configService.ConfigureKey("API", "Timeout", TimeSpan.FromSeconds(30)); - - // Feature flags - configService.ConfigureKey("Features", "NewUI", true); - configService.ConfigureKey("Features", "BetaFeatures", false); - - services.AddSingleton(configService); - } -} -``` - -### Service Usage - -```csharp -public class DatabaseService -{ - private readonly ConfigurationService _config; - - public DatabaseService(ConfigurationService config) - { - _config = config; - } - - public async Task InitializeAsync() - { - var connectionString = _config.GetValue("Database", "ConnectionString"); - var timeout = _config.GetValue("Database", "Timeout", TimeSpan.FromSeconds(30)); - - // Initialize database connection - await ConnectAsync(connectionString, timeout); - } -} - -public class APIClient -{ - private readonly ConfigurationService _config; - - public APIClient(ConfigurationService config) - { - _config = config; - } - - public async Task GetAsync(string endpoint) - { - var baseUrl = _config.GetValue("API", "BaseUrl"); - var timeout = _config.GetValue("API", "Timeout", TimeSpan.FromSeconds(30)); - - // Check feature flag - if (_config.IsFeatureEnabled("NewAPI")) - { - return await GetFromNewAPIAsync(baseUrl, endpoint, timeout); - } - - return await GetFromLegacyAPIAsync(baseUrl, endpoint, timeout); - } -} -``` - -### Dynamic Configuration Updates - -```csharp -public class ConfigurationController : ControllerBase -{ - private readonly ConfigurationService _config; - - public ConfigurationController(ConfigurationService config) - { - _config = config; - } - - [HttpPost("update")] - public IActionResult UpdateConfiguration([FromBody] UpdateConfigRequest request) - { - try - { - _config.UpdateConfiguration(request.Section, request.Key, request.Value); - return Ok($"Updated {request.Section}:{request.Key}"); - } - catch (Exception ex) - { - return BadRequest($"Failed to update configuration: {ex.Message}"); - } - } - - [HttpPost("feature-flag")] - public IActionResult ToggleFeatureFlag([FromBody] FeatureFlagRequest request) - { - _config.UpdateConfiguration("Features", request.FeatureName, request.IsEnabled); - return Ok($"Feature {request.FeatureName} set to {request.IsEnabled}"); - } -} -``` - -### Environment-Specific Behavior - -```csharp -public class EmailService -{ - private readonly ConfigurationService _config; - - public EmailService(ConfigurationService config) - { - _config = config; - } - - public async Task SendAsync(string to, string subject, string body) - { - var emailKey = new ConfigurationKey("Email", "Provider"); - var composition = Composition.FindOrDefault(emailKey); - - if (composition == null) - { - await SendDefaultEmail(to, subject, body); - return; - } - - // Adapt behavior based on primary capability (environment) - var result = composition.GetPrimaryOrDefault() switch - { - DevelopmentConfigPrimary => - await SendToConsole(to, subject, body), - - StagingConfigPrimary => - await SendToTestMailbox(to, subject, body), - - ProductionConfigPrimary => - await SendViaRealProvider(to, subject, body), - - _ => await SendDefaultEmail(to, subject, body) - }; - } -} -``` - -## Advanced Patterns - -### Configuration Inheritance - -```csharp -public void ConfigureInheritedSettings() -{ - var baseKey = new ConfigurationKey("Logging", "Level"); - var specificKey = new ConfigurationKey("Logging.Database", "Level"); - - // Base configuration - Composer.For(baseKey) - .WithPrimary(new DevelopmentConfigPrimary()) - .Add(new ConfigurationValueCapability( - new ConfigurationValue(LogLevel.Information, typeof(LogLevel)) - )) - .Build(); - - // Specific configuration inherits from base - Composer.For(specificKey) - .WithPrimary(new DevelopmentConfigPrimary()) - .Add(new ConfigurationValueCapability( - new ConfigurationValue(LogLevel.Debug, typeof(LogLevel)) - )) - .Add(new DefaultValueCapability(LogLevel.Information)) // Fallback to base - .Build(); -} -``` - -### Multi-Source Configuration - -```csharp -public void ConfigureMultiSourceSettings() -{ - var key = new ConfigurationKey("API", "Key"); - - Composer.For(key) - .WithPrimary(new FileConfigPrimary("appsettings.json")) - .Add(new EnvironmentOverrideCapability("Development", "dev-api-key")) - .Add(new EnvironmentOverrideCapability("Production", "prod-api-key")) - .Add(new ConfigurationCacheCapability(TimeSpan.FromHours(1), 100)) - .Build(); -} -``` - -This configuration system demonstrates the full power of Cocoar.Capabilities for building flexible, extensible systems that adapt to different environments and requirements while maintaining type safety and performance. \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 12a3e39..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,250 +0,0 @@ -# Quick Start Guide - -Get up and running with Cocoar.Capabilities in minutes. - -## Installation - -Add the single NuGet package: - -```bash -dotnet add package Cocoar.Capabilities -``` - -This provides the complete capability composition system. Registry behavior (global lookup) and composer tracking are configured per `CapabilityScope` via `CapabilityScopeOptions`. - -## Core Concepts - -**Capabilities** are behaviors or features you can attach to any object. The library provides a type-safe way to: -- Add capabilities to objects without inheritance -- Query what capabilities an object has -- Organize capabilities through contracts and ordering -- Enable cross-project extensibility without circular dependencies - -## Your First Capability - -### 1. Define Capabilities - -```csharp -using Cocoar.Capabilities; - -// Simple capability with data -public record LoggingCapability(LogLevel Level, string Category) : ICapability; - -// Interface-based capability for contracts -public interface IValidationCapability : ICapability -{ - bool IsValid(T subject); -} - -public record EmailValidator : IValidationCapability -{ - public bool IsValid(T subject) => /* validation logic */ true; -} -``` - -### 2. Create a Capability Scope and Attach Capabilities - -```csharp -using var scope = new CapabilityScope(); -var userService = new UserService(); - -// Build a composition with capabilities -var composition = scope.For(userService) - .Add(new LoggingCapability(LogLevel.Info, "UserManagement")) - .Add(new EmailValidator()) - .Build(); // Automatically registered in scope if UseCompositionRegistry=true (default) -``` - -### 3. Query Capabilities - -```csharp -// Check if capability exists -if (composition.Has>()) -{ - Console.WriteLine("Logging is enabled"); -} - -// Get all capabilities of a type -var validators = composition.GetAll>(); -foreach (var validator in validators) -{ - validator.IsValid(userService); -} - -// Get specific capability data -var loggingCaps = composition.GetAll>(); -var logLevel = loggingCaps.FirstOrDefault()?.Level ?? LogLevel.None; -``` - -### 4. Find Compositions via Scope - -```csharp -// Find composition later via the scope -if (scope.Compositions.TryFind(userService, out var foundComposition)) -{ - var hasLogging = foundComposition.Has>(); -} - -// Alternative: store the composition reference directly -var storedComposition = composition; // Immutable, thread-safe -``` - -## Common Patterns - -### Contract-Based Registration - -Register capabilities under interface contracts for polymorphic querying: - -```csharp -using var scope = new CapabilityScope(); -var composition = scope.For(service) - .AddAs>(new EmailValidator()) - .AddAs>(new PhoneValidator()) - .Build(); - -// Query by contract interface -var allValidators = composition.GetAll>(); -``` - -### Primary Capabilities - -Use primary capabilities to define the main behavior or type of a subject: - -```csharp -public record DatabasePrimaryCapability : IPrimaryCapability; -public record CachePrimaryCapability : IPrimaryCapability; - -using var scope = new CapabilityScope(); -var composition = scope.For(service) - .WithPrimary(new DatabasePrimaryCapability()) - .Add(new LoggingCapability(LogLevel.Debug, "Database")) - .Build(); - -// Only one primary capability allowed per subject -if (composition.TryGetPrimary(out var primary)) -{ - // Handle based on primary capability type -} -``` - -### Ordered Capabilities - -Control execution order with `IOrderedCapability`: - -```csharp -public record MiddlewareCapability(string Name, int Priority) - : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -using var scope = new CapabilityScope(); -var composition = scope.For(pipeline) - .Add(new MiddlewareCapability("Auth", 100)) - .Add(new MiddlewareCapability("Logging", 200)) - .Add(new MiddlewareCapability("Validation", 150)) - .Build(); - -// GetAll() returns capabilities ordered by Order property (ascending) -var orderedMiddleware = composition.GetAll>(); -// Result: Auth (100), Validation (150), Logging (200) -``` - -### Conditional Registration - -Avoid duplicate registrations with `TryAdd`: - -```csharp -var composer = Composer.For(service) - .TryAdd(new LoggingCapability(LogLevel.Info)) // Adds if not present - .TryAdd(new LoggingCapability(LogLevel.Debug)) // Skipped - already has LoggingCapability - .Build(); -``` - -### Dynamic Capability Management - -Modify capabilities using recomposition: - -```csharp -// Start with basic composition -var composition = Composer.For(service) - .Add(new LoggingCapability(LogLevel.Info)) - .Build(); - -// Later, add more capabilities -var updatedComposition = Composer.Recompose(composition) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .RemoveWhere(cap => cap is LoggingCapability log && log.Level == LogLevel.Info) - .Add(new LoggingCapability(LogLevel.Debug)) - .Build(); -``` - -## Real-World Example - -Here's how to build a configuration system with capabilities: - -```csharp -// Configuration capabilities -public record EnvironmentConfig(string Environment) : IPrimaryCapability; -public record ConnectionStringCapability(string Name, string Value) : ICapability; -public record FeatureFlagCapability(string Flag, bool Enabled) : ICapability; - -// Build configuration -public class ConfigurationService -{ - public IComposition BuildConfiguration() - { - return Composer.For(this) - .WithPrimary(new EnvironmentConfig("Development")) - .Add(new ConnectionStringCapability("Database", "Server=localhost;...")) - .Add(new FeatureFlagCapability("NewUI", true)) - .Add(new FeatureFlagCapability("BetaFeatures", false)) - .Build(); - } -} - -// Usage throughout application -public class DatabaseService -{ - public void Initialize() - { - var configService = new ConfigurationService(); - var config = Composition.FindRequired(configService); - - // Get environment - var env = config.GetPrimaryOrDefaultAs>(); - Console.WriteLine($"Running in: {env?.Environment}"); - - // Get connection strings - var connections = config.GetAll>(); - var dbConnection = connections.FirstOrDefault(c => c.Name == "Database")?.Value; - - // Check feature flags - var features = config.GetAll>(); - var newUIEnabled = features.Any(f => f.Flag == "NewUI" && f.Enabled); - } -} -``` - -## Performance Tips - -1. **Batch Registration**: Register multiple capabilities in one composition for better performance -2. **Contract Queries**: Use specific contracts instead of broad interface queries when possible -3. **Value Type Subjects**: The library optimizes memory usage for value types automatically -4. **Reuse Compositions**: Compositions are immutable and can be safely shared across threads - -## Next Steps - -- Read [Core Concepts](core-concepts.md) for deeper understanding -- Explore [Examples](examples/) for more patterns -- Check the [API Reference](api-reference.md) for complete method documentation -- See [Guides](guides/) for advanced scenarios - -## Common Gotchas - -1. **Subject Equality**: Subjects are matched by reference (for reference types) or value equality (for value types) -2. **Primary Capability Limit**: Only one primary capability per subject - additional registrations throw exceptions -3. **Immutability**: Compositions are immutable - use `Composer.Recompose()` to modify existing compositions -4. **Generic Constraints**: Capability types must implement `ICapability` for the specific subject type - -Need help? Check out the [examples](examples/) section for more detailed usage patterns! \ No newline at end of file diff --git a/docs/guides/capability-ordering.md b/docs/guides/capability-ordering.md deleted file mode 100644 index 07841ed..0000000 --- a/docs/guides/capability-ordering.md +++ /dev/null @@ -1,473 +0,0 @@ -# Capability Ordering Guide - -Understanding how capabilities are ordered and controlling execution sequence with `IOrderedCapability`. - -## Overview - -Cocoar.Capabilities provides automatic ordering for capabilities that implement `IOrderedCapability`. This is essential for scenarios where capabilities need to execute in a specific sequence, such as middleware pipelines, processing chains, or layered behaviors. - -## IOrderedCapability Interface - -```csharp -public interface IOrderedCapability -{ - int Order { get; } -} -``` - -### Ordering Rules - -1. **Ascending Order**: Capabilities are ordered by `Order` property in ascending order (lowest first) -2. **Stable Sort**: Capabilities with the same `Order` value maintain their registration sequence -3. **Mixed Capabilities**: Ordered and non-ordered capabilities can coexist -4. **Default Position**: Non-ordered capabilities appear after ordered ones - -## Basic Usage - -### Simple Ordered Capability - -```csharp -public record MiddlewareCapability(string Name, int Priority) - : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -// Register with different priorities -var composition = Composer.For(pipeline) - .Add(new MiddlewareCapability("Authentication", 100)) - .Add(new MiddlewareCapability("Logging", 300)) - .Add(new MiddlewareCapability("Validation", 200)) - .Build(); - -// GetAll() returns in order: Authentication (100), Validation (200), Logging (300) -var orderedMiddleware = composition.GetAll>(); -``` - -### Processing Order Example - -```csharp -public record ProcessorCapability(string Name, int Stage) - : ICapability, IOrderedCapability -{ - public int Order => Stage; -} - -// Registration order doesn't matter -var composition = Composer.For(data) - .Add(new ProcessorCapability("Cleanup", 300)) - .Add(new ProcessorCapability("Validation", 100)) - .Add(new ProcessorCapability("Transform", 200)) - .Build(); - -// Execution follows Order property -var processors = composition.GetAll>(); -foreach (var processor in processors) -{ - Console.WriteLine($"Processing: {processor.Name}"); - // Output: Validation, Transform, Cleanup -} -``` - -## Real-World Examples - -### Example 1: HTTP Middleware Pipeline - -```csharp -// Base middleware capability -public abstract record MiddlewareCapability(int Priority) - : ICapability, IOrderedCapability -{ - public int Order => Priority; - public abstract Task ProcessAsync(HttpContext context, Func next); -} - -// Specific middleware implementations -public record AuthenticationMiddleware : MiddlewareCapability(100) -{ - public override async Task ProcessAsync(HttpContext context, Func next) - { - // Authentication logic - await next(); - } -} - -public record RateLimitingMiddleware : MiddlewareCapability(200) -{ - public override async Task ProcessAsync(HttpContext context, Func next) - { - // Rate limiting logic - await next(); - } -} - -public record LoggingMiddleware : MiddlewareCapability(300) -{ - public override async Task ProcessAsync(HttpContext context, Func next) - { - // Logging logic - await next(); - } -} - -// Configure pipeline -public class WebApplication -{ - public void ConfigureMiddleware() - { - var app = new Application(); - - // Order doesn't matter during registration - Composer.For(app) - .Add(new LoggingMiddleware()) // Will execute 3rd - .Add(new AuthenticationMiddleware()) // Will execute 1st - .Add(new RateLimitingMiddleware()) // Will execute 2nd - .Build(); - } - - public async Task ProcessRequest(HttpContext context) - { - var app = new Application(); - var composition = Composition.FindRequired(app); - var middleware = composition.GetAll>(); - - // Build execution chain in order - Func next = () => Task.CompletedTask; - - // Reverse iteration to build proper call chain - for (int i = middleware.Count - 1; i >= 0; i--) - { - var current = middleware[i]; - var currentNext = next; - next = () => current.ProcessAsync(context, currentNext); - } - - await next(); // Execute: Auth -> RateLimit -> Logging - } -} -``` - -### Example 2: Data Processing Pipeline - -```csharp -// Processing stage capability -public record ProcessingStageCapability(string Name, int StageOrder, ProcessingAction Action) - : ICapability, IOrderedCapability -{ - public int Order => StageOrder; -} - -public delegate Task ProcessingAction(T input); - -// Configure data processing stages -public class DataProcessor -{ - public void ConfigureProcessing() - { - var processor = new DataProcessor(); - - Composer.For(processor) - .Add(new ProcessingStageCapability("Validate", 100, ValidateData)) - .Add(new ProcessingStageCapability("Transform", 200, TransformData)) - .Add(new ProcessingStageCapability("Enrich", 300, EnrichData)) - .Add(new ProcessingStageCapability("Save", 400, SaveData)) - .Build(); - } - - public async Task ProcessAsync(Data input) - { - var composition = Composition.FindRequired(this); - var stages = composition.GetAll>(); - - var result = input; - foreach (var stage in stages) - { - Console.WriteLine($"Executing stage: {stage.Name}"); - result = await stage.Action(result); - } - - return result; - } -} -``` - -### Example 3: Event Handler Priority - -```csharp -// Event handler with priority -public record EventHandlerCapability(string HandlerName, int Priority, Func Handler) - : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -// Configure event handlers -public class EventSystem -{ - public void ConfigureHandlers() - { - var eventBus = new EventBus(); - - Composer.For(eventBus) - // Critical handlers first (low priority numbers) - .Add(new EventHandlerCapability("Security", 10, HandleSecurity)) - .Add(new EventHandlerCapability("Validation", 20, HandleValidation)) - - // Business logic handlers - .Add(new EventHandlerCapability("Business", 100, HandleBusiness)) - .Add(new EventHandlerCapability("Notification", 200, HandleNotification)) - - // Cleanup handlers last - .Add(new EventHandlerCapability("Cleanup", 300, HandleCleanup)) - .Add(new EventHandlerCapability("Logging", 400, HandleLogging)) - .Build(); - } - - public async Task PublishAsync(Event eventData) - { - var eventBus = new EventBus(); - var composition = Composition.FindRequired(eventBus); - var handlers = composition.GetAll>(); - - foreach (var handler in handlers) - { - try - { - await handler.Handler(eventData); - } - catch (Exception ex) - { - // Log error but continue with remaining handlers - Console.WriteLine($"Handler {handler.HandlerName} failed: {ex.Message}"); - } - } - } -} -``` - -## Ordering Strategies - -### 1. Priority-Based Ordering - -```csharp -// Use priority levels for different categories -public enum Priority -{ - Critical = 0, - High = 100, - Normal = 200, - Low = 300, - Background = 400 -} - -public record TaskCapability(string Name, Priority Priority) - : ICapability, IOrderedCapability -{ - public int Order => (int)Priority; -} -``` - -### 2. Stage-Based Ordering - -```csharp -// Use stage numbers for pipeline processing -public enum ProcessingStage -{ - PreValidation = 100, - Validation = 200, - Transformation = 300, - BusinessLogic = 400, - PostProcessing = 500, - Cleanup = 600 -} - -public record StageCapability(ProcessingStage Stage) - : ICapability, IOrderedCapability -{ - public int Order => (int)Stage; -} -``` - -### 3. Layered Ordering - -```csharp -// Use decimal-like ordering for fine-grained control -public record LayeredCapability(string Layer, double OrderValue) - : ICapability, IOrderedCapability -{ - public int Order => (int)(OrderValue * 100); // Convert to int -} - -// Usage with fine-grained control -.Add(new LayeredCapability("Auth", 1.0)) // Order: 100 -.Add(new LayeredCapability("AuthLog", 1.1)) // Order: 110 -.Add(new LayeredCapability("Validation", 2.0)) // Order: 200 -``` - -## Mixed Ordered and Non-Ordered Capabilities - -```csharp -// Mix of ordered and non-ordered capabilities -public record OrderedCapability(int Priority) : ICapability, IOrderedCapability -{ - public int Order => Priority; -} - -public record RegularCapability(string Name) : ICapability; - -var composition = Composer.For(subject) - .Add(new RegularCapability("First")) // Will appear after ordered - .Add(new OrderedCapability(100)) // Will appear first - .Add(new RegularCapability("Second")) // Will appear after ordered - .Add(new OrderedCapability(50)) // Will appear before first ordered - .Build(); - -// Result order when getting all ICapability: -// 1. OrderedCapability(50) -// 2. OrderedCapability(100) -// 3. RegularCapability("First") -// 4. RegularCapability("Second") -``` - -## Query Patterns - -### Contract-Based Ordering - -```csharp -// Define contract interface with ordering -public interface IProcessorCapability : ICapability -{ - Task ProcessAsync(T subject); -} - -public record ValidationProcessor : IProcessorCapability, IOrderedCapability -{ - public int Order => 100; - public async Task ProcessAsync(T subject) { /* validate */ } -} - -public record TransformProcessor : IProcessorCapability, IOrderedCapability -{ - public int Order => 200; - public async Task ProcessAsync(T subject) { /* transform */ } -} - -// Register under contract and get ordered results -var composition = Composer.For(data) - .AddAs>(new TransformProcessor()) - .AddAs>(new ValidationProcessor()) - .Build(); - -// Query by contract - still returns in order -var processors = composition.GetAll>(); -// Result: ValidationProcessor (100), TransformProcessor (200) -``` - -### Conditional Ordering - -```csharp -public record ConditionalCapability(string Name, bool IsHighPriority) - : ICapability, IOrderedCapability -{ - public int Order => IsHighPriority ? 0 : 1000; -} - -// Dynamic priority based on conditions -var composition = Composer.For(service) - .Add(new ConditionalCapability("Normal", false)) // Order: 1000 - .Add(new ConditionalCapability("Critical", true)) // Order: 0 - .Build(); -``` - -## Performance Considerations - -- **Sorting Overhead**: Ordering happens during composition building, not during queries -- **Query Performance**: `GetAll()` returns pre-ordered results in O(1) time -- **Memory Impact**: No additional memory overhead for ordering -- **Registration Order**: The order of `.Add()` calls doesn't affect final ordering - -## Best Practices - -### 1. Use Meaningful Order Values - -```csharp -// Good: Clear priority levels -public record MiddlewareCapability : ICapability, IOrderedCapability -{ - public int Order => Stage switch - { - "Security" => 100, - "Authentication" => 200, - "Authorization" => 300, - "Business" => 400, - "Logging" => 500, - _ => 1000 - }; -} - -// Avoid: Magic numbers without context -public record BadMiddleware : ICapability, IOrderedCapability -{ - public int Order => 73; // What does 73 mean? -} -``` - -### 2. Leave Gaps for Extensibility - -```csharp -// Good: Use gaps for future insertions -public enum ProcessingOrder -{ - PreValidation = 100, // Room for 101-199 - Validation = 200, // Room for 201-299 - PostValidation = 300, // Room for 301-399 - Business = 400 // Room for 401-499 -} - -// Avoid: Consecutive numbers -public enum BadOrder -{ - First = 1, // No room for insertion - Second = 2, // No room for insertion - Third = 3 // No room for insertion -} -``` - -### 3. Document Ordering Contracts - -```csharp -/// -/// Middleware capability with standard ordering: -/// - Security/Auth: 0-99 -/// - Validation: 100-199 -/// - Business Logic: 200-299 -/// - Logging/Cleanup: 300+ -/// -public abstract record MiddlewareCapability(int Priority) - : ICapability, IOrderedCapability -{ - public int Order => Priority; -} -``` - -### 4. Test Ordering Behavior - -```csharp -[Test] -public void Capabilities_Should_Execute_In_Order() -{ - var composition = Composer.For(subject) - .Add(new ProcessorCapability("Third", 300)) - .Add(new ProcessorCapability("First", 100)) - .Add(new ProcessorCapability("Second", 200)) - .Build(); - - var processors = composition.GetAll>(); - - Assert.AreEqual("First", processors[0].Name); - Assert.AreEqual("Second", processors[1].Name); - Assert.AreEqual("Third", processors[2].Name); -} -``` - -Capability ordering provides powerful control over execution sequences while maintaining the flexibility and composability of the capabilities system. \ No newline at end of file diff --git a/docs/guides/memory-management.md b/docs/guides/memory-management.md deleted file mode 100644 index 58c07e0..0000000 --- a/docs/guides/memory-management.md +++ /dev/null @@ -1,264 +0,0 @@ -# Memory Management and Lifecycle - -## Overview - -The Cocoar.Capabilities system implements a **dual storage strategy** for optimal memory management, automatically handling different requirements for value types vs reference types. This design prevents memory leaks while ensuring reliable access to capabilities. - -## Dual Storage Architecture - -### Value Types: Strong Reference Storage -**Storage**: `ConcurrentDictionary` -**Rationale**: Value types require strong references to prevent premature garbage collection -**Behavior**: Manual cleanup required via `Composition.Remove()` - -### Reference Types: Weak Reference Storage -**Storage**: `ConditionalWeakTable` -**Rationale**: Automatic cleanup when subject is no longer referenced -**Behavior**: Automatic cleanup via garbage collection - -## Storage Decision Logic - -```csharp -// This logic runs automatically during registration -if (subject.GetType().IsValueType) -{ - // Strong reference storage for value types - _valueTypeStorage[subject] = composition; -} -else -{ - // Weak reference storage for reference types - WeakRegistryCore.Register(subject, composition); -} -``` - -## Value Type Lifecycle - -### Registration and Storage -```csharp -// Value type subjects (int, struct, enum, etc.) -var userId = 12345; -var pointData = new Point(10, 20); -var status = OrderStatus.Pending; - -// All stored with strong references -var userComposition = Composer.For(userId).Add(userCapability).Build(); -var pointComposition = Composer.For(pointData).Add(pointCapability).Build(); -var statusComposition = Composer.For(status).Add(statusCapability).Build(); -``` - -### Memory Implications -```csharp -// ⚠️ Value types won't be automatically cleaned up -// These compositions stay in memory until explicitly removed - -// Manual cleanup required -Composition.Remove(userId); -Composition.Remove(pointData); -Composition.Remove(status); -``` - -### Value Type Best Practices -```csharp -// Pattern 1: Explicit cleanup in using pattern -using var scope = new CompositionScope(); -var composition = Composer.For(valueTypeSubject).Add(capability).Build(); -// ... use composition -// Cleanup happens when scope disposes - -// Pattern 2: Batch cleanup -var valueTypeSubjects = new List { userId1, userId2, pointData }; -// ... work with compositions -foreach (var subject in valueTypeSubjects) -{ - Composition.Remove(subject); -} - -// Pattern 3: Application lifecycle cleanup -public void Shutdown() -{ - CompositionRegistryConfiguration.ClearValueTypes(); // Clears all value type compositions -} -``` - -## Reference Type Lifecycle - -### Registration and Storage -```csharp -// Reference type subjects (classes) -var userService = new UserService(); -var orderProcessor = new OrderProcessor(); -var logger = new FileLogger(); - -// All stored with weak references -var userComposition = Composer.For(userService).Add(userCapability).Build(); -var orderComposition = Composer.For(orderProcessor).Add(orderCapability).Build(); -var loggerComposition = Composer.For(logger).Add(loggerCapability).Build(); -``` - -### Automatic Cleanup -```csharp -// Reference types clean up automatically -var service = new UserService(); -var composition = Composer.For(service).Add(capability).Build(); - -// When service goes out of scope and is garbage collected, -// the composition is automatically removed from storage -service = null; // No more references -GC.Collect(); // Composition automatically cleaned up -``` - -## Memory Management Strategies - -### For Long-Running Applications -```csharp -// Strategy 1: Periodic value type cleanup -public class CapabilityMemoryManager -{ - private readonly Timer _cleanupTimer; - - public CapabilityMemoryManager() - { - _cleanupTimer = new Timer(CleanupValueTypes, null, - TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30)); - } - - private void CleanupValueTypes(object state) - { - // Application-specific logic to determine which value types to remove - var expiredSubjects = GetExpiredValueTypeSubjects(); - foreach (var subject in expiredSubjects) - { - Composition.Remove(subject); - } - } -} -``` - -### For Request-Scoped Applications -```csharp -// Strategy 2: Request-scoped cleanup -public class RequestScopeManager : IDisposable -{ - private readonly List _valueTypeSubjects = new(); - - public IComposition CreateComposition(T subject, params ICapability[] capabilities) where T : notnull - { - var composer = Composer.For(subject); - foreach (var capability in capabilities) - { - composer.Add(capability); - } - - var composition = composer.Build(); - - // Track value types for cleanup - if (typeof(T).IsValueType) - { - _valueTypeSubjects.Add(subject); - } - - return composition; - } - - public void Dispose() - { - // Cleanup all value type compositions created in this scope - foreach (var subject in _valueTypeSubjects) - { - Composition.Remove(subject); - } - } -} -``` - -## Best Practices - -### 1. **Prefer Reference Types for Subjects** -When possible, use class-based subjects for automatic cleanup. - -### 2. **Implement Cleanup for Value Types** -Always have a strategy for cleaning up value type compositions. - -### 3. **Use Scoped Patterns** -Implement scoped lifetime management for value type compositions. - -### 4. **Monitor Memory Usage** -In production, monitor value type composition counts. - -### 5. **Avoid Long-Lived Value Type Compositions** -Don't create compositions for temporary value type data. - -## Common Pitfalls - -### ❌ **Pitfall 1: Forgetting Value Type Cleanup** -```csharp -// This creates a memory leak -for (int i = 0; i < 1000000; i++) -{ - var composition = Composer.For(i).Add(capability).Build(); // Never cleaned up -} -``` - -### βœ… **Correct Approach** -```csharp -void ProcessUserIds(int[] userIds) -{ - var createdSubjects = new List(); - - try - { - foreach (var id in userIds) - { - var composition = Composer.For(id).Add(capability).Build(); - createdSubjects.Add(id); - // Process composition - } - } - finally - { - // Cleanup all created compositions - foreach (var id in createdSubjects) - { - Composition.Remove(id); - } - } -} -``` - -## Integration with DI Containers - -### Scoped Lifetime Integration -```csharp -// Register cleanup service with DI container -services.AddScoped(); - -public class CapabilityCleanupService : ICapabilityCleanupService, IDisposable -{ - private readonly List _trackedValueTypes = new(); - - public IComposition CreateTrackedComposition(T subject, params ICapability[] capabilities) where T : notnull - { - var composition = Composer.For(subject).Add(capabilities).Build(); - - if (typeof(T).IsValueType) - { - _trackedValueTypes.Add(subject); - } - - return composition; - } - - public void Dispose() - { - foreach (var subject in _trackedValueTypes) - { - Composition.Remove(subject); - } - } -} -``` - ---- - -*The dual storage strategy provides optimal memory management by automatically handling the different lifecycle requirements of value types and reference types.* \ No newline at end of file diff --git a/docs/guides/pattern-cookbook.md b/docs/guides/pattern-cookbook.md deleted file mode 100644 index abbbd93..0000000 --- a/docs/guides/pattern-cookbook.md +++ /dev/null @@ -1,377 +0,0 @@ -# Capability Patterns Cookbook - -A collection of practical patterns and creative use cases for Cocoar.Capabilities, organized by domain and complexity. - -> **πŸ’‘ Philosophy**: "Data with behavior" – Capabilities let any object declare how it participates in your system. - ---- - -## 🎯 **Value Type Extension Patterns** - -Value types (integers, structs, enums) can hold capabilities with value equality semantics. - -### **Enum Enrichment** -Add metadata to enums you cannot modify: - -```csharp -// Enrich HTTP status codes -Composer.For(HttpStatusCode.NotFound) - .Add(new DescriptionCapability("Resource not found")) - .Add(new SeverityCapability(Severity.Warning)) - .Add(new RetryPolicyCapability(shouldRetry: false)) - .Build(); - -// Usage -string Describe(HttpStatusCode code) => - Composition.FindOrDefault(code)? - .GetAll() - .FirstOrDefault()?.Description ?? code.ToString(); - -bool ShouldRetry(HttpStatusCode code) => - Composition.FindOrDefault(code)? - .GetAll() - .FirstOrDefault()?.ShouldRetry ?? false; -``` - -### **Feature Flags with Capabilities** -```csharp -Composer.For("feature:dark-mode") - .Add(new ToggleCapability(enabled: true)) - .Add(new RolloutCapability(percentage: 35)) - .Add(new AuditCapability(trackUsage: true)) - .Build(); - -// Feature flag evaluation -bool IsFeatureEnabled(string feature, int userId) => - Composition.FindOrDefault(feature) is { } comp && - comp.GetAll().Any(t => t.Enabled) && - comp.GetAll().Any(r => (userId % 100) < r.Percentage); -``` - -### **Sensitive Data Protection** -```csharp -// Mark sensitive fields -Composer.For("ssn") - .Add(new SecurityClassificationCapability(Sensitivity.High)) - .Add(new RedactionCapability("***-**-****")) - .Add(new AuditCapability(logAccess: true)) - .Build(); - -// Automatic sanitization -string Sanitize(string fieldName, string value) -{ - var comp = Composition.FindOrDefault(fieldName); - if (comp?.GetAll().FirstOrDefault() is { } redaction) - { - // Log access if auditing enabled - if (comp.GetAll().Any(a => a.LogAccess)) - Logger.LogAccess(fieldName, DateTime.UtcNow); - - return redaction.Mask; - } - return value; -} -``` - ---- - -## 🌐 **Cross-Cutting Concern Patterns** - -### **Universal Logging Strategy** -```csharp -// Different subjects, same logging behavior -Composer.For(userService) - .Add(new LoggingCapability(LogLevel.Info, category: "UserManagement")) - .Build(); - -Composer.For(orderService) - .Add(new LoggingCapability(LogLevel.Warning, category: "OrderProcessing")) - .Build(); - -// Generic logging processor -void LogOperation(T subject, string operation) -{ - var comp = Composition.FindOrDefault(subject); - foreach (var log in comp?.GetAll() ?? []) - { - Logger.Log(log.Level, $"[{log.Category}] {operation} on {typeof(T).Name}"); - } -} -``` - -### **Caching with TTL** -```csharp -Composer.For(expensiveService) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Add(new CacheKeyCapability(key => $"service:{key.GetHashCode()}")) - .Build(); - -// Generic caching wrapper -async Task WithCaching(T subject, string operation, Func> operation) -{ - var comp = Composition.FindOrDefault(subject); - var cache = comp?.GetAll().FirstOrDefault(); - var keyGen = comp?.GetAll().FirstOrDefault(); - - if (cache != null && keyGen != null) - { - var key = keyGen.Generator(operation); - if (MemoryCache.TryGetValue(key, out TResult cached)) - return cached; - - var result = await operation(); - MemoryCache.Set(key, result, cache.Duration); - return result; - } - - return await operation(); -} -``` - ---- - -## πŸ”„ **Event-Driven Patterns** - -### **Event Handler Registration** -```csharp -// Register event handlers via capabilities -Composer.For(typeof(OrderCreated)) - .Add(new EventHandlerCapability(new EmailNotificationHandler())) - .Add(new EventHandlerCapability(new InventoryUpdateHandler())) - .Add(new EventHandlerCapability(new AuditLogHandler())) - .Build(); - -// Generic event dispatcher -async Task PublishEvent(T eventData) -{ - var comp = Composition.FindOrDefault(typeof(T)); - var handlers = comp?.GetAll>() ?? []; - - foreach (var handler in handlers) - { - await handler.HandleAsync(eventData); - } -} -``` - -### **Ordered Middleware Pipelines** -```csharp -// Build processing pipeline with capabilities -Composer.For(apiEndpoint) - .Add(new MiddlewareCapability(new AuthenticationMiddleware(), order: 1)) - .Add(new MiddlewareCapability(new RateLimitingMiddleware(), order: 2)) - .Add(new MiddlewareCapability(new LoggingMiddleware(), order: 3)) - .Build(); - -// Execute pipeline in order -async Task ProcessRequest(HttpRequest request, string endpoint) -{ - var comp = Composition.FindOrDefault(endpoint); - var middlewares = comp?.GetAll() - .OrderBy(m => m.Order) - .ToArray() ?? []; - - var context = new RequestContext(request); - - foreach (var middleware in middlewares) - { - await middleware.Component.ProcessAsync(context); - if (context.ShouldShortCircuit) break; - } - - return context.Response; -} -``` - ---- - -## πŸ—οΈ **Architectural Patterns** - -### **Plugin Architecture** -```csharp -// Register plugins with capabilities -Composer.For("reporting-system") - .Add(new PluginCapability("pdf-exporter", new PdfExportPlugin())) - .Add(new PluginCapability("excel-exporter", new ExcelExportPlugin())) - .Add(new PluginCapability("email-sender", new EmailPlugin())) - .Build(); - -// Dynamic plugin discovery -IEnumerable GetPluginsOfType(string system) -{ - var comp = Composition.FindOrDefault(system); - return comp?.GetAll() - .Where(p => p.Instance is T) - .Select(p => (T)p.Instance) ?? []; -} -``` - -### **Configuration Strategy Pattern** -```csharp -// Different environments, different strategies -Composer.For("database-config") - .WithPrimary(new DatabasePrimaryCapability()) // Single source of truth - .Add(new ConnectionStringCapability(Environment.GetConnectionString())) - .Add(new RetryPolicyCapability(maxRetries: 3)) - .Add(new TimeoutCapability(TimeSpan.FromSeconds(30))) - .Build(); - -// Environment-specific capabilities -if (Environment.IsDevelopment()) -{ - Composer.Recompose("database-config") - .Add(new DebuggingCapability(enabled: true)) - .Add(new VerboseLoggingCapability(enabled: true)) - .Build(); -} -``` - -### **Cross-Project Extension Points** -```csharp -// Core project defines extension points -public static class ServiceExtensions -{ - public static Composer AddLogging(this Composer composer, LogLevel level) - => composer.Add(new LoggingCapability(level)); - - public static Composer AddCaching(this Composer composer, TimeSpan duration) - => composer.Add(new CachingCapability(duration)); -} - -// Extension project adds new capabilities without circular dependencies -public static class DiExtensions -{ - public static Composer AsSingleton(this Composer composer) - => composer.Add(new SingletonLifetimeCapability()); - - public static Composer AsScoped(this Composer composer) - => composer.Add(new ScopedLifetimeCapability()); -} - -// Usage combines both projects seamlessly -var composition = Composer.For(userService) - .AddLogging(LogLevel.Info) // Core project extension - .AddCaching(TimeSpan.FromMinutes(5)) // Core project extension - .AsSingleton() // DI project extension - .Build(); -``` - ---- - -## 🎨 **Creative Patterns** - -### **Capability-Based Permissions** -```csharp -// Define permissions as capabilities -Composer.For(currentUser) - .Add(new PermissionCapability("users:read")) - .Add(new PermissionCapability("users:write")) - .Add(new RoleCapability("administrator")) - .Build(); - -// Permission checking -bool HasPermission(object user, string permission) => - Composition.FindOrDefault(user)? - .GetAll() - .Any(p => p.Permission == permission) ?? false; -``` - -### **Type-Safe Configuration** -```csharp -// Attach configuration to types themselves -Composer.For(typeof(EmailService)) - .Add(new SmtpConfigurationCapability("smtp.example.com", 587)) - .Add(new RetryConfigurationCapability(maxRetries: 3)) - .Build(); - -// Automatic configuration injection -T CreateConfiguredService() where T : new() -{ - var service = new T(); - var comp = Composition.FindOrDefault(typeof(T)); - - // Apply all configuration capabilities - foreach (var config in comp?.GetAll() ?? []) - { - config.Configure(service); - } - - return service; -} -``` - ---- - -## πŸ“Š **Performance Patterns** - -### **Lazy Loading with Capabilities** -```csharp -Composer.For(expensiveResource) - .Add(new LazyLoadingCapability(() => LoadExpensiveData())) - .Add(new CachingCapability(TimeSpan.FromHours(1))) - .Build(); - -// Lazy access with caching -TData GetData(object resource) -{ - var comp = Composition.FindOrDefault(resource); - var lazy = comp?.GetAll>().FirstOrDefault(); - return lazy?.Value ?? default(TData); -} -``` - -### **Batching Operations** -```csharp -Composer.For(dbContext) - .Add(new BatchingCapability(batchSize: 100)) - .Add(new FlushPolicyCapability(TimeSpan.FromSeconds(5))) - .Build(); - -// Automatic batching based on capabilities -void AddOperation(T context, IOperation operation) -{ - var comp = Composition.FindOrDefault(context); - var batcher = comp?.GetAll().FirstOrDefault(); - - if (batcher != null) - { - batcher.AddOperation(operation); - if (batcher.ShouldFlush()) - { - batcher.ExecuteBatch(); - } - } - else - { - operation.Execute(); // Immediate execution - } -} -``` - ---- - -## πŸ”§ **Best Practices Summary** - -### 1. **Capability Design Principles** -- **Single Responsibility**: Each capability should have one clear purpose -- **Immutable Data**: Prefer record types for capability data -- **Descriptive Naming**: Use clear, intention-revealing names - -### 2. **Performance Considerations** -- **Small Compositions**: Keep capability counts reasonable (typically < 10) -- **Value Type Cleanup**: Remember to remove value type compositions -- **Lazy Evaluation**: Use lazy patterns for expensive operations - -### 3. **Architecture Guidelines** -- **Interface Contracts**: Use interfaces for cross-project flexibility -- **Ordered Processing**: Implement `IOrderedCapability` for pipeline scenarios -- **Primary Capabilities**: Use for single-source-of-truth patterns - -### 4. **Testing Strategies** -- **Isolated Testing**: Test capabilities independently -- **Composition Testing**: Verify capability interactions -- **Mock Compositions**: Use test doubles for complex scenarios - ---- - -*These patterns demonstrate the flexibility and power of capability composition for solving real-world architectural challenges.* \ No newline at end of file diff --git a/docs/guides/performance-optimization.md b/docs/guides/performance-optimization.md deleted file mode 100644 index bdf63f7..0000000 --- a/docs/guides/performance-optimization.md +++ /dev/null @@ -1,410 +0,0 @@ -# Performance Optimization Guide - -Performance best practices and optimization strategies for choosing between Cocoar.Capabilities architectures and maximizing performance. - -## Architecture-Specific Performance - -### Registry Disabled Configuration -**Maximum performance** - you manage composition lifetimes: - -- **Build Performance**: ~4.6 ΞΌs (50 capabilities), ~42 ΞΌs (500 capabilities) -- **Query Performance**: ~142 ns (feature queries), ~1 ΞΌs (all capabilities) -- **Memory**: 11-102 KB build allocations, 320B-1.2KB query allocations -- **Thread Safety**: Lock-free through immutability -- **Scaling**: Linear build time, constant query time - -### Registry Architecture (`Cocoar.Capabilities`) -**Convenience with overhead** - automatic global composition storage: - -- **Build Performance**: ~8.3 ΞΌs (50 capabilities), ~47 ΞΌs (500 capabilities) - *+79% / +13% overhead* -- **Query Performance**: ~151 ns (feature queries), ~8.5 ΞΌs (large all capabilities) - *+6% / +757% overhead* -- **Memory**: Similar to Core with registry overhead (27-34 bytes) -- **Thread Safety**: Lock-free through immutability -- **Scaling**: Linear build with variable query overhead - -## Performance Optimization Strategies - -### 1. **Reflection Elimination** -Major performance gains achieved by eliminating expensive runtime reflection: - -```csharp -// BEFORE: Expensive runtime reflection -var primaryMarker = typeof(IPrimaryCapability<>).MakeGenericType(typeof(TSubject)); - -// AFTER: Cached compile-time type -private static readonly Type PrimaryMarkerType = typeof(IPrimaryCapability); -``` - -**Impact**: ~11% performance improvement for build operations. - -### 2. **LINQ Elimination in Hot Paths** -Replaced expensive LINQ operations with manual algorithms: - -```csharp -// BEFORE: Expensive LINQ chain -return capabilities - .Select((capability, index) => new { capability, index }) - .OrderBy(x => (x.capability as IOrderedCapability)?.Order ?? 0) - .ThenBy(x => x.index) - .Select(x => x.capability) - .ToList(); - -// AFTER: Manual insertion sort (optimized for small collections) -// [Efficient manual sorting implementation] -``` - -**Impact**: ~1.6% additional performance improvement. - -### 3. **Query Path Optimization** -Eliminated remaining allocations in query methods: - -```csharp -// BEFORE: LINQ allocation -return filtered.ToArray(); - -// AFTER: Manual array creation -var result = new TCapability[count]; -int resultIndex = 0; -for (int i = 0; i < allCapabilities.Count; i++) { - if (allCapabilities[i] is TCapability match) { - result[resultIndex++] = match; - } -} -return result; -``` - -**Impact**: Completed comprehensive LINQ elimination across build and query paths. - -## Performance Best Practices - -### 1. **Composition Size Management** -```csharp -// βœ… Good - typical application scale (5-15 capabilities) -var composition = Composer.For(service) - .Add(new LoggingCapability(LogLevel.Info)) - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) - .Add(new ValidationCapability(validator)) - .Add(new MetricsCapability("service-metrics")) - .Build(); - -// ⚠️ Consider refactoring - too many capabilities -var bloatedComposition = Composer.For(service) - .Add(/* 50+ capabilities */) // May impact performance - .Build(); -``` - -**Guideline**: Keep compositions under 20 capabilities for optimal performance. - -### 2. **Query Pattern Optimization** -```csharp -// βœ… Excellent - specific type queries (~142 ns Core, ~150 ns Registry) -var validators = composition.GetAll>(); - -// βœ… Good - primary capability access (fastest path) -if (composition.TryGetPrimary(out var primary)) -{ - // Use primary capability -} - -// ⚠️ Less optimal - repeated general queries -var allCapabilities = composition.GetAll>(); // Broader scope -``` - -**Performance Ranking**: -1. **Primary capability queries**: Fastest (~70-100 ns) -2. **Specific type queries**: Very fast (~142 ns Core, variable Registry) -3. **General capability queries**: Fast (~900-1100 ns) - -### 3. **Build vs Runtime Trade-offs** -```csharp -// Build-time cost (one-time): ~4.6 ΞΌs for 50 capabilities (Core), ~8.3 ΞΌs (Registry) -var composition = Composer.For(service) - .Add(loggingCapability) - .Add(cachingCapability) - .Build(); // Optimization focus: minimize this cost - -// Runtime cost (frequent): ~142 ns per query (Core) -var logger = composition.GetAll>(); // Optimization focus: keep this fast -``` - -**Strategy**: Accept higher build-time costs for faster runtime queries. - -### 4. **Memory-Efficient Patterns** -```csharp -// βœ… Reference types - automatic cleanup -var service = new UserService(); // Class -var composition = Composer.For(service).Add(capability).Build(); -// Automatic cleanup when service is GC'd - -// ⚠️ Value types - manual cleanup required -var userId = 12345; // Struct -var composition = Composer.For(userId).Add(capability).Build(); -// Remember: Composition.Remove(userId) when done -``` - -## Advanced Performance Patterns - -### 1. **Lazy Capability Initialization** -```csharp -public record LazyLoggingCapability(Func LoggerFactory) : ICapability -{ - private ILogger? _logger; - public ILogger Logger => _logger ??= LoggerFactory(); -} - -// Build-time: only stores factory (fast) -var composition = Composer.For(service) - .Add(new LazyLoggingCapability(() => CreateExpensiveLogger())) - .Build(); - -// Runtime: lazy evaluation on first access -``` - -### 2. **Cached Composition Patterns** -```csharp -public class ServiceCompositionCache -{ - private readonly ConcurrentDictionary _cache = new(); - - public IComposition GetOrCreateComposition(T service) where T : notnull - { - var serviceType = typeof(T); - - // Check cache first (very fast) - if (_cache.TryGetValue(serviceType, out var cached)) - return (IComposition)cached; - - // Build new composition (slower, one-time cost) - var composition = Composer.For(service) - .Add(GetStandardCapabilities()) - .Build(); - - _cache.TryAdd(serviceType, composition); - return composition; - } -} -``` - -### 3. **Batch Registration Optimization** -```csharp -// βœ… Efficient - single composition build -var composition = Composer.For(service) - .Add(capability1) - .Add(capability2) - .Add(capability3) - .Build(); // Single build operation - -// ❌ Inefficient - multiple builds -var comp1 = Composer.For(service).Add(capability1).Build(); -var comp2 = Composer.Recompose(service).Add(capability2).Build(); -var comp3 = Composer.Recompose(service).Add(capability3).Build(); -``` - -## Scaling Performance Analysis - -### Build Performance Scaling -``` -Capability Count β†’ Build Time β†’ Per-Capability Cost -50 capabilities β†’ 4.6 ΞΌs (Core) / 8.3 ΞΌs (Registry) β†’ 92 ns/capability (Core) -500 capabilities β†’ 68.50 ΞΌs β†’ 137 ns/capability -``` - -**Key Insight**: Per-capability cost actually *improves* at larger scales due to optimization efficiency. - -### Query Performance Scaling -``` -Composition Size β†’ Query Time β†’ Performance Class -Small (10-50) β†’ 142 ns (Core) / 150 ns (Registry) β†’ Scale-independent -Large (500+) β†’ 139 ns (Core) / 850 ns (Registry) β†’ Variable -``` - -**Key Insight**: Query performance is **completely independent** of composition size. - -## Performance Monitoring - -### 1. **Build Performance Tracking** -```csharp -public static class CompositionMetrics -{ - private static readonly Histogram BuildDuration = Metrics - .CreateHistogram("capability_build_duration_microseconds", - "Time to build capability compositions"); - - public static IComposition BuildWithMetrics(this Composer composer) where T : notnull - { - var stopwatch = Stopwatch.StartNew(); - var composition = composer.Build(); - stopwatch.Stop(); - - BuildDuration.Observe(stopwatch.Elapsed.TotalMicroseconds); - return composition; - } -} -``` - -### 2. **Query Performance Monitoring** -```csharp -public static class QueryMetrics -{ - private static readonly Counter QueryCount = Metrics - .CreateCounter("capability_queries_total", "Total capability queries"); - - private static readonly Histogram QueryDuration = Metrics - .CreateHistogram("capability_query_duration_nanoseconds", - "Time to execute capability queries"); -} -``` - -## Common Performance Pitfalls - -### ❌ **Pitfall 1: Excessive Composition Rebuilding** -```csharp -// Inefficient - rebuilds composition repeatedly -foreach (var config in configurations) -{ - var composition = Composer.For(service) - .Add(new ConfigCapability(config)) - .Build(); // Expensive rebuild each time -} -``` - -### βœ… **Solution: Batch Configuration** -```csharp -// Efficient - single composition build -var composer = Composer.For(service); -foreach (var config in configurations) -{ - composer.Add(new ConfigCapability(config)); -} -var composition = composer.Build(); // Single build operation -``` - -### ❌ **Pitfall 2: Forgetting Value Type Cleanup** -```csharp -// Memory leak - value types never cleaned up -for (int i = 0; i < 1000000; i++) -{ - var composition = Composer.For(i).Add(capability).Build(); - // This accumulates in memory! -} -``` - -### βœ… **Solution: Explicit Cleanup** -```csharp -var subjects = new List(); -try -{ - for (int i = 0; i < 1000000; i++) - { - subjects.Add(i); - var composition = Composer.For(i).Add(capability).Build(); - // Use composition - } -} -finally -{ - foreach (var subject in subjects) - { - Composition.Remove(subject); // Explicit cleanup - } -} -``` - -## Benchmark-Driven Optimization - -### Setting Up Performance Tests -```csharp -[MemoryDiagnoser] -[SimpleJob(RuntimeMoniker.Net90)] -public class CapabilityBenchmarks -{ - [Benchmark] - public IComposition Build_Small_1x50() - { - var service = new UserService(); - return Composer.For(service) - .Add(/* 50 capabilities */) - .Build(); - } - - [Benchmark] - public IEnumerable> Query_Typed() - { - return _composition.GetAll>(); - } -} -``` - -### Performance Baseline Tracking -Track performance over time to prevent regressions: - -```json -{ - "baselines": { - "Build_Small_1x50": { - "current": { "core_mean_us": 4.6, "registry_mean_us": 8.3, "date": "2025-10-03" }, - "target": { "mean_us": 8.0, "tolerance": 0.5 } - }, - "Query_Typed": { - "current": { "mean_ns": 135, "date": "2025-10-02" }, - "target": { "mean_ns": 150, "tolerance": 20 } - } - } -} -``` - -## Production Deployment Considerations - -### 1. **Warm-up Strategies** -```csharp -// Pre-build common compositions at startup -public static class CompositionWarmup -{ - public static void WarmupCommonPatterns() - { - // Pre-JIT common paths - var dummyService = new DummyService(); - Composer.For(dummyService) - .Add(new LoggingCapability(LogLevel.Info)) - .Build(); - - // Trigger query paths - var _ = Composition.FindOrDefault(dummyService)?.GetAll>(); - } -} -``` - -### 2. **Resource Monitoring** -```csharp -// Monitor capability system resource usage -public class CapabilityHealthCheck : IHealthCheck -{ - public Task CheckHealthAsync(HealthCheckContext context) - { - var valueTypeCount = CompositionRegistryConfiguration.ValueTypeCount; - - if (valueTypeCount > 10000) // Threshold - { - return Task.FromResult(HealthCheckResult.Degraded( - $"High value type composition count: {valueTypeCount}")); - } - - return Task.FromResult(HealthCheckResult.Healthy()); - } -} -``` - -## Summary - -The performance optimization work on Cocoar.Capabilities demonstrates that systematic optimization can yield significant improvements: - -- **13.5% performance improvement** through reflection elimination and LINQ removal -- **Scale-independent query performance** enabling predictable behavior -- **Linear build scaling** with improving per-capability efficiency -- **Production-ready characteristics** validated through comprehensive benchmarking - -Key takeaway: **Measure first, optimize systematically, validate continuously.** - ---- - -*Performance optimization is an iterative process. Always measure the impact of changes and maintain performance baselines to prevent regressions.* \ No newline at end of file diff --git a/docs/guides/primary-capabilities.md b/docs/guides/primary-capabilities.md deleted file mode 100644 index 809010c..0000000 --- a/docs/guides/primary-capabilities.md +++ /dev/null @@ -1,462 +0,0 @@ -# Primary Capabilities Guide - -Understanding and implementing the Primary Capability Strategy Pattern in Cocoar.Capabilities. - -## What are Primary Capabilities? - -Primary capabilities are a special type of capability that defines the **main behavior, type, or strategy** for a subject. Unlike regular capabilities where you can have multiple instances of the same type, each subject can have only **one primary capability**. - -## The Primary Capability Strategy Pattern - -This pattern enables sophisticated cross-project extensibility by using **unified subjects** with **strategy-defining primary capabilities**. - -### Core Pattern Structure - -```csharp -// 1. Define a unified subject (shared across projects) -public class ConfigurationKey -{ - public string Section { get; init; } - public string Key { get; init; } - public override int GetHashCode() => HashCode.Combine(Section, Key); - public override bool Equals(object? obj) => /* value equality */; -} - -// 2. Define primary capabilities for different strategies -public record DatabaseConfigPrimary : IPrimaryCapability; -public record FileConfigPrimary : IPrimaryCapability; -public record CloudConfigPrimary : IPrimaryCapability; - -// 3. Different projects register their strategies -var configKey = new ConfigurationKey { Section = "Database", Key = "ConnectionString" }; - -// Project A: Database-first approach -Composer.For(configKey) - .WithPrimary(new DatabaseConfigPrimary()) - .Add(new ConnectionPoolCapability(size: 20)) - .Build(); - -// Project B: File-based approach -Composer.For(configKey) - .WithPrimary(new FileConfigPrimary()) - .Add(new FileWatcherCapability(path: "config.json")) - .Build(); -``` - -## Interface Definition - -```csharp -public interface IPrimaryCapability : ICapability { } -``` - -Primary capabilities inherit from `ICapability` but add the constraint that only one can exist per subject. - -## Registration API - -### Setting Primary Capabilities - -```csharp -// Set a primary capability -var composition = Composer.For(subject) - .WithPrimary(new DatabasePrimaryCapability()) - .Add(new LoggingCapability(LogLevel.Info)) - .Build(); - -// Replace existing primary (if any) -var updated = Composer.Recompose(composition) - .WithPrimary(new CachePrimaryCapability()) - .Build(); - -// Remove primary capability -var withoutPrimary = Composer.Recompose(composition) - .WithPrimary(null) - .Build(); -``` - -### Multiple Primary Capability Error - -```csharp -// This will throw InvalidOperationException -try -{ - var invalid = Composer.For(subject) - .WithPrimary(new FirstPrimary()) - .WithPrimary(new SecondPrimary()) // Error! - .Build(); -} -catch (InvalidOperationException ex) -{ - // "Multiple primary capabilities registered for 'Subject'. Only one primary capability is allowed." -} -``` - -## Query API - -### Basic Primary Capability Queries - -```csharp -// Check if any primary capability exists -bool hasPrimary = composition.HasPrimary(); - -// Check for specific primary capability type -bool hasDbPrimary = composition.HasPrimary>(); - -// Try to get primary capability (non-generic) -if (composition.TryGetPrimary(out IPrimaryCapability primary)) -{ - // Use primary capability -} - -// Get primary or null -IPrimaryCapability? primary = composition.GetPrimaryOrDefault(); - -// Get primary (throws if not found) -IPrimaryCapability required = composition.GetPrimary(); -``` - -### Typed Primary Capability Queries - -```csharp -// Try to get specific primary type -if (composition.TryGetPrimaryAs>(out var dbPrimary)) -{ - // Use typed primary capability -} - -// Get typed primary or null -DatabasePrimaryCapability? dbPrimary = - composition.GetPrimaryOrDefaultAs>(); - -// Get typed primary (throws if not found or wrong type) -DatabasePrimaryCapability required = - composition.GetRequiredPrimaryAs>(); -``` - -## Real-World Examples - -### Example 1: Storage Strategy Pattern - -```csharp -// Define storage strategies as primary capabilities -public record DatabaseStoragePrimary(string ConnectionString) : IPrimaryCapability; -public record FileStoragePrimary(string Directory) : IPrimaryCapability; -public record CloudStoragePrimary(string BucketName) : IPrimaryCapability; - -// Entity configuration -public class UserEntity -{ - public int Id { get; set; } - public string Name { get; set; } -} - -// Configure storage strategy -public void ConfigureUserStorage() -{ - var userEntity = new UserEntity(); - - // Development: File storage - if (Environment.IsDevelopment()) - { - Composer.For(userEntity) - .WithPrimary(new FileStoragePrimary("./data")) - .Add(new JsonSerializationCapability()) - .Add(new FileWatcherCapability()) - .Build(); - } - // Production: Database storage - else - { - Composer.For(userEntity) - .WithPrimary(new DatabaseStoragePrimary("Server=prod;...")) - .Add(new ConnectionPoolCapability(size: 50)) - .Add(new CachingCapability(TimeSpan.FromMinutes(10))) - .Build(); - } -} - -// Repository implementation adapts to strategy -public class UserRepository -{ - public async Task GetAsync(int id) - { - var entity = new UserEntity(); - var composition = Composition.FindRequired(entity); - - // Adapt behavior based on primary capability - return composition.GetPrimaryOrDefault() switch - { - DatabaseStoragePrimary db => await GetFromDatabase(id, db.ConnectionString), - FileStoragePrimary file => await GetFromFile(id, file.Directory), - CloudStoragePrimary cloud => await GetFromCloud(id, cloud.BucketName), - _ => throw new InvalidOperationException("No storage strategy configured") - }; - } -} -``` - -### Example 2: Processing Pipeline Strategy - -```csharp -// Pipeline strategies as primary capabilities -public record BatchProcessingPrimary(int BatchSize) : IPrimaryCapability; -public record StreamProcessingPrimary(int BufferSize) : IPrimaryCapability; -public record RealtimeProcessingPrimary : IPrimaryCapability; - -// Data pipeline configuration -public class DataPipeline -{ - public string Name { get; init; } -} - -// Configure processing strategy -public void ConfigureDataPipeline() -{ - var pipeline = new DataPipeline { Name = "UserEvents" }; - - // High-volume: Batch processing - if (IsHighVolumeScenario()) - { - Composer.For(pipeline) - .WithPrimary(new BatchProcessingPrimary(batchSize: 1000)) - .Add(new CompressionCapability()) - .Add(new DatabaseBulkInsertCapability()) - .Build(); - } - // Real-time: Stream processing - else - { - Composer.For(pipeline) - .WithPrimary(new RealtimeProcessingPrimary()) - .Add(new EventStreamCapability()) - .Add(new WebSocketNotificationCapability()) - .Build(); - } -} - -// Processor adapts to strategy -public class DataProcessor -{ - public async Task ProcessAsync(IEnumerable events) - { - var pipeline = new DataPipeline { Name = "UserEvents" }; - var composition = Composition.FindRequired(pipeline); - - switch (composition.GetPrimaryOrDefault()) - { - case BatchProcessingPrimary batch: - await ProcessInBatches(events, batch.BatchSize); - break; - - case StreamProcessingPrimary stream: - await ProcessAsStream(events, stream.BufferSize); - break; - - case RealtimeProcessingPrimary: - await ProcessRealtime(events); - break; - - default: - throw new InvalidOperationException("No processing strategy configured"); - } - } -} -``` - -### Example 3: Cross-Project Extension Points - -```csharp -// Shared model (in common library) -public class APIEndpoint -{ - public string Path { get; init; } - public string Method { get; init; } - - public override int GetHashCode() => HashCode.Combine(Path, Method); - public override bool Equals(object? obj) => /* value equality */; -} - -// Project A: Authentication-first approach -public record AuthenticationPrimary(string Scheme) : IPrimaryCapability; - -public class AuthenticationModule -{ - public void ConfigureEndpoint(string path, string method) - { - var endpoint = new APIEndpoint { Path = path, Method = method }; - - Composer.For(endpoint) - .WithPrimary(new AuthenticationPrimary("Bearer")) - .Add(new JwtValidationCapability()) - .Add(new RoleAuthorizationCapability(["Admin"])) - .Build(); - } -} - -// Project B: Rate limiting approach -public record RateLimitingPrimary(int RequestsPerMinute) : IPrimaryCapability; - -public class RateLimitingModule -{ - public void ConfigureEndpoint(string path, string method) - { - var endpoint = new APIEndpoint { Path = path, Method = method }; - - Composer.For(endpoint) - .WithPrimary(new RateLimitingPrimary(requestsPerMinute: 100)) - .Add(new IpTrackingCapability()) - .Add(new ThrottlingCapability()) - .Build(); - } -} - -// Shared middleware adapts to any primary strategy -public class APIMiddleware -{ - public async Task ProcessRequest(string path, string method) - { - var endpoint = new APIEndpoint { Path = path, Method = method }; - var composition = Composition.FindOrDefault(endpoint); - - if (composition == null) - { - await ProcessDefault(); - return; - } - - // Handle different primary strategies - switch (composition.GetPrimaryOrDefault()) - { - case AuthenticationPrimary auth: - await ProcessWithAuthentication(auth.Scheme); - break; - - case RateLimitingPrimary rateLimit: - await ProcessWithRateLimit(rateLimit.RequestsPerMinute); - break; - - default: - await ProcessDefault(); - break; - } - } -} -``` - -## Best Practices - -### 1. Use Value Objects as Subjects - -```csharp -// Good: Value object with proper equality -public record ConfigurationKey(string Section, string Key); - -// Good: Class with value equality -public class APIEndpoint -{ - public string Path { get; init; } - public string Method { get; init; } - - public override int GetHashCode() => HashCode.Combine(Path, Method); - public override bool Equals(object? obj) => /* proper value equality */; -} - -// Avoid: Reference types without proper equality -public class BadSubject { } // Uses reference equality -``` - -### 2. Make Primary Capabilities Descriptive - -```csharp -// Good: Describes the strategy clearly -public record DatabaseStoragePrimary(string ConnectionString) : IPrimaryCapability; -public record InMemoryCachingPrimary(TimeSpan Duration) : IPrimaryCapability; - -// Avoid: Generic or unclear names -public record ConfigPrimary : IPrimaryCapability; // What kind of config? -public record HandlerPrimary : IPrimaryCapability; // What does it handle? -``` - -### 3. Combine with Regular Capabilities - -```csharp -// Primary defines the strategy, regular capabilities add features -var composition = Composer.For(entity) - .WithPrimary(new DatabaseStoragePrimary("connection")) // Strategy - .Add(new LoggingCapability(LogLevel.Debug)) // Feature - .Add(new CachingCapability(TimeSpan.FromMinutes(5))) // Feature - .Add(new ValidationCapability()) // Feature - .Build(); -``` - -### 4. Handle Missing Primary Capabilities - -```csharp -// Always handle the case where no primary capability is set -public void ProcessEntity(Entity entity) -{ - var composition = Composition.FindOrDefault(entity); - - var primary = composition?.GetPrimaryOrDefault(); - if (primary == null) - { - // Use default behavior or throw meaningful error - throw new InvalidOperationException($"No processing strategy configured for {entity}"); - } - - // Process based on primary capability type -} -``` - -## Performance Considerations - -- **Single Storage**: Primary capabilities use the same storage as regular capabilities -- **Query Performance**: Primary capability queries are O(1) operations -- **Memory Impact**: Minimal - primary capabilities are stored alongside other capabilities -- **Thread Safety**: All primary capability operations are thread-safe through immutability - -## Error Scenarios - -### Multiple Primary Registration - -```csharp -// This pattern will fail -var composer = Composer.For(subject) - .WithPrimary(new FirstPrimary()) - .WithPrimary(new SecondPrimary()); // InvalidOperationException - -// Instead, replace the primary -var composer = Composer.For(subject) - .WithPrimary(new FirstPrimary()) - .WithPrimary(null) // Remove first - .WithPrimary(new SecondPrimary()); // Set new -``` - -### Wrong Type Queries - -```csharp -// This will throw if wrong type or not found -try -{ - var dbPrimary = composition.GetRequiredPrimaryAs>(); -} -catch (InvalidOperationException) -{ - // Handle missing or wrong type primary capability -} - -// Safer approach -var dbPrimary = composition.GetPrimaryOrDefaultAs>(); -if (dbPrimary != null) -{ - // Use database primary -} -``` - -## Related Patterns - -- **Strategy Pattern**: Primary capabilities implement strategy selection -- **Factory Pattern**: Use primary capabilities to determine object creation strategy -- **Template Method**: Primary capabilities can define algorithmic variations -- **Chain of Responsibility**: Combine with ordered regular capabilities for processing chains - -Primary capabilities provide a powerful way to implement cross-cutting architectural strategies while maintaining loose coupling and extensibility. \ No newline at end of file diff --git a/docs/guides/tuple-contracts.md b/docs/guides/tuple-contracts.md deleted file mode 100644 index 1d26aff..0000000 --- a/docs/guides/tuple-contracts.md +++ /dev/null @@ -1,215 +0,0 @@ -# Tuple Contract Syntax - Advanced Registration - -> **πŸ“š Prerequisites:** -> Understanding of basic registration concepts from [Registration and Querying Behavior](../registration-and-querying.md) - -## Overview - -The tuple contract syntax allows registering a single capability under **multiple contract types simultaneously**. This provides flexibility for capabilities that need to be queryable through different interfaces or types without requiring separate registrations. - -## Basic Syntax - -```csharp -// Register under multiple contracts using tuple syntax -composer.AddAs<(IContract1, IContract2, ConcreteType)>(capability); -``` - -**Result**: The capability becomes queryable under all specified types in the tuple. - -## Supported Tuple Types - -### ValueTuple Syntax (Recommended) -```csharp -// Up to 8 different contract types -composer.AddAs<(ILogging, ICaching)>(capability); -composer.AddAs<(IType1, IType2, IType3)>(capability); -``` - -### What's Supported -- **Any number of contract types** (within ValueTuple limits) -- **Interfaces and concrete types** can be mixed -- **Generic and non-generic types** -- **Nested capability interfaces** - -### What's NOT Supported -```csharp -// ❌ Regular Tuple (not ValueTuple) -composer.AddAs, IContract2>>(capability); - -// ❌ Non-capability types -composer.AddAs<(string, int)>(capability); // Must implement ICapability -``` - -## Usage Examples - -### Interface + Concrete Registration -```csharp -public class DatabaseLogger : ILoggingCapability -{ - public void Log(string message) => /* implementation */; -} - -var logger = new DatabaseLogger(); - -// Register under both interface and concrete type -composer.AddAs<(ILoggingCapability, DatabaseLogger)>(logger); - -// Now queryable under both types -var interfaceQuery = composition.GetAll>(); // βœ… Found -var concreteQuery = composition.GetAll>(); // βœ… Found -``` - -### Multiple Interface Contracts -```csharp -public class CachingValidator : IValidationCapability, ICachingCapability -{ - public bool IsValid(T item) => /* validation logic */; - public void Cache(T item) => /* caching logic */; -} - -var validator = new CachingValidator(); - -// Register under multiple interface contracts -composer.AddAs<(IValidationCapability, ICachingCapability)>(validator); - -// Queryable under both interfaces -var validators = composition.GetAll>(); // βœ… Found -var cachers = composition.GetAll>(); // βœ… Found -``` - -### Cross-Cutting Concern Registration -```csharp -public class AuditingCapability : ILoggingCapability, IMetricsCapability, ISecurityCapability -{ - // Implements multiple cross-cutting concerns -} - -var auditor = new AuditingCapability(); - -// Register under all implemented interfaces -composer.AddAs<( - ILoggingCapability, - IMetricsCapability, - ISecurityCapability -)>(auditor); -``` - -## Advanced Patterns - -### Polymorphic Registration -```csharp -public class DatabaseRepository : IRepository, IReadOnlyRepository, ICacheableRepository -{ - // Implementation -} - -var repository = new DatabaseRepository(); - -// Register for different access patterns -composer.AddAs<( - IRepository, // Full read/write access - IReadOnlyRepository, // Read-only access - ICacheableRepository // Caching behavior -)>(repository); - -// Different consumers can query for different contracts -var fullAccess = composition.GetAll>(); -var readOnly = composition.GetAll>(); -var cacheable = composition.GetAll>(); -``` - -### Role-Based Access -```csharp -public class AdminUserService : IUserService, IAdminService, ISecurityService -{ - // Implementation with admin privileges -} - -var adminService = new AdminUserService(); - -// Register under role-specific contracts -composer.AddAs<(IUserService, IAdminService, ISecurityService)>(adminService); - -// Different parts of application query for appropriate role -var userOps = composition.GetAll(); // General user operations -var adminOps = composition.GetAll(); // Admin-only operations -var securityOps = composition.GetAll(); // Security operations -``` - -## Performance Considerations - -### Registration Performance -```csharp -// Single contract - fastest -composer.AddAs>(capability); - -// Multiple contracts - slight overhead during registration -composer.AddAs<(IContract1, IContract2)>(capability); - -// Many contracts - more reflection overhead -composer.AddAs<(IContract1, IContract2, IContract3, IContract4)>(capability); -``` - -### Query Performance -```csharp -// Each contract type maintains separate query path -// No performance difference between single and tuple registration during queries - -var contract1Results = composition.GetAll>(); // Same speed -var contract2Results = composition.GetAll>(); // Same speed -``` - -## Design Guidelines - -### 1. **Logical Grouping** -Only group contracts that logically belong together: - -```csharp -// βœ… Good - related concerns -composer.AddAs<(ILoggingCapability, IAuditingCapability)>(capability); - -// ❌ Avoid - unrelated concerns -composer.AddAs<(ILoggingCapability, IPaymentCapability)>(capability); -``` - -### 2. **Interface Segregation** -Use tuple contracts to support interface segregation: - -```csharp -// Large interface split into focused contracts -public class FileProcessor : IFileReader, IFileWriter, IFileValidator -{ - // Implementation -} - -// Consumers only get what they need -composer.AddAs<(IFileReader, IFileWriter, IFileValidator)>(processor); -``` - -## Best Practices - -1. **Limit Contract Count**: Generally use 2-4 contracts per tuple for clarity -2. **Related Interfaces**: Only group logically related interface contracts -3. **Clear Naming**: Use descriptive interface names that indicate their purpose -4. **Document Intent**: Comment why multiple contracts are needed -5. **Test All Paths**: Verify capability is queryable under all registered contracts - -## Migration from Separate Registrations - -### Before (Multiple Registrations) -```csharp -composer.AddAs>(capability); -composer.AddAs>(capability); -composer.AddAs>(capability); -``` - -### After (Tuple Registration) -```csharp -composer.AddAs<(ILoggingCapability, IAuditingCapability, ConcreteType)>(capability); -``` - -**Benefits**: Single registration, clearer intent, guaranteed consistency. - ---- - -*Tuple contract syntax provides powerful, flexible registration patterns while maintaining query performance and type safety.* \ No newline at end of file diff --git a/docs/hybrid-initialization-optimization.md b/docs/hybrid-initialization-optimization.md deleted file mode 100644 index 8bbfb0d..0000000 --- a/docs/hybrid-initialization-optimization.md +++ /dev/null @@ -1,218 +0,0 @@ -# Hybrid Eager/Lazy Initialization - Optimal Performance Strategy - -## The Optimization Problem - -Your insight was spot-on: "If the default says to use one or the other registry we can skip the lazy init and do it asap, or?" - -This question identified a key optimization opportunity in our implementation. - -## Analysis of Initialization Strategies - -### Pure Lazy Approach (Previous) -```csharp -// Always lazy - even when we know we'll need it -private readonly Lazy _lazyComposerRegistry; -private readonly Lazy _lazyCompositionRegistry; - -// Context with enabled flags still uses lazy wrappers -var context = new CapabilityContext(); // UseComposerRegistry = true by default -// Still creates Lazy wrappers even though we'll definitely use them -``` - -**Problems:** -- ❌ Unnecessary `Lazy` overhead when flags are `true` -- ❌ Extra indirection for the common case (enabled registries) -- ❌ Thread-safety overhead when not needed - -### Hybrid Eager/Lazy Approach (Optimized) -```csharp -// Conditional initialization based on flags -private readonly IComposerRegistry? _eagerComposerRegistry; // When enabled -private readonly Lazy? _lazyComposerRegistry; // When disabled - -public CapabilityContext(CapabilityContextOptions? options = null) -{ - _options = options ?? new CapabilityContextOptions(); - - if (_options.UseComposerRegistry) - { - // Eager: Create immediately - we know we'll need it - _eagerComposerRegistry = _options.ComposerRegistry ?? new DefaultComposerRegistry(); - _eagerComposerRegistryApi = new ComposerRegistryApi(_eagerComposerRegistry, this); - } - else - { - // Lazy: Create only if accessed - might never be needed - _lazyComposerRegistry = new Lazy(() => - _options.ComposerRegistry ?? new DefaultComposerRegistry()); - _lazyComposerRegistryApi = new Lazy(() => - new ComposerRegistryApi(_lazyComposerRegistry.Value, this)); - } -} -``` - -## Performance Characteristics - -### Memory Usage Comparison - -| Scenario | Pure Lazy | Hybrid Approach | Memory Saved | -|----------|-----------|-----------------|--------------| -| **Both enabled (default)** | 4 Γ— Lazy + 2 registries | 2 registries only | ~96 bytes + reduced overhead | -| **Both disabled** | 4 Γ— Lazy only | 2 Γ— Lazy only | ~48 bytes + reduced complexity | -| **Mixed (one enabled)** | 4 Γ— Lazy + registries | 1 eager + 1 lazy | ~48 bytes + reduced overhead | - -### Performance Benefits - -#### 1. **Eager Path (Common Case)** -```csharp -// Default usage (registries enabled) -var context = new CapabilityContext(); - -// Property access is direct field access - no lazy overhead -public ComposerRegistryApi Composers => _eagerComposerRegistryApi ?? _lazyComposerRegistryApi!.Value; -// ^^^^^^^^^^^^^^^^^^^^^^^^^^ -// Direct field access (fast) -``` - -#### 2. **Lazy Path (Optimization Case)** -```csharp -// Disabled registries -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -// Zero allocations until first access -var composer = context.For(subject, useRegistry: false); // No registry created -// Registry only created when actually needed -var foundComposer = context.Composers.TryGet(subject, out _); // Now created -``` - -## Real-World Usage Patterns - -### Pattern 1: Default Behavior (Optimized) -```csharp -// Most common usage - both enabled by default -var context = new CapabilityContext(); - -// βœ… Optimal: Direct field access, no lazy overhead -var composer = context.For(document); -var composition = composer.Add(capability).Build(); - -// Memory: Only actual registry objects, no lazy wrappers -// Performance: Direct field access for all operations -``` - -### Pattern 2: Disabled Registries (Optimized) -```csharp -// Performance-critical scenarios where registries might not be needed -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -// βœ… Optimal: Zero allocations unless actually used -var composer = context.For(document, useRegistry: false); -var composition = composer.Add(capability).Build(useRegistry: false); - -// Memory: Only lazy wrappers (~48 bytes), no actual registries -// Performance: No unnecessary object creation -``` - -### Pattern 3: Mixed Configuration (Optimized) -```csharp -// Enable composer registry, disable composition registry -var context = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = true, // Eager - UseCompositionRegistry = false // Lazy -}); - -// βœ… Optimal: Best of both worlds -var composer = context.For(document); // Direct access to eager registry -var composition = composer.Build(useRegistry: false); // No composition registry created - -// Memory: One eager registry + one lazy wrapper (not created) -// Performance: Direct access for enabled, lazy for disabled -``` - -## Implementation Details - -### Smart Property Access -```csharp -public ComposerRegistryApi Composers => _eagerComposerRegistryApi ?? _lazyComposerRegistryApi!.Value; -// | | -// Fast path (enabled) Lazy path (disabled) -``` - -### Intelligent Registration -```csharp -var shouldRegister = useRegistry ?? _options.UseComposerRegistry; -if (shouldRegister) -{ - var registry = _eagerComposerRegistry ?? _lazyComposerRegistry!.Value; - // | | - // Direct reference Creates on demand - registry.Register(subject, composer); -} -``` - -### Smart Disposal -```csharp -protected virtual void Dispose(bool disposing) -{ - // Dispose eager registries (always created) - if (_eagerComposerRegistry is IDisposable eagerDisposable) - eagerDisposable.Dispose(); - - // Dispose lazy registries (only if created) - if (_lazyComposerRegistry?.IsValueCreated == true && - _lazyComposerRegistry.Value is IDisposable lazyDisposable) - lazyDisposable.Dispose(); -} -``` - -## Test Validation - -The `HybridInitializationTests` verify: - -1. **Eager behavior**: When flags are `true`, no lazy wrappers exist -2. **Lazy behavior**: When flags are `false`, no eager objects exist -3. **Mixed behavior**: Combination works correctly -4. **Functional equivalence**: Both paths provide identical functionality -5. **Performance characteristics**: Eager path has no lazy overhead - -## Benefits Achieved - -### βœ… **Performance Optimized** -- **Default case**: No lazy overhead, direct field access -- **Disabled case**: Zero allocations until needed -- **Mixed case**: Optimal resource allocation per registry - -### βœ… **Memory Efficient** -- **Enabled registries**: No lazy wrapper overhead -- **Disabled registries**: No unnecessary allocations -- **Smart disposal**: Only dispose created objects - -### βœ… **Developer Friendly** -- **Same API**: No changes to public interface -- **Same behavior**: Identical functionality regardless of path -- **Clear semantics**: Boolean flags control initialization strategy - -### βœ… **Architecture Benefits** -- **Predictable**: Initialization strategy matches usage intent -- **Scalable**: Optimal for both performance and memory scenarios -- **Maintainable**: Clear separation between eager and lazy paths - -## Conclusion - -Your optimization insight was brilliant! The hybrid approach provides: - -- πŸš€ **Better performance** for the common case (enabled registries) -- πŸ’Ύ **Better memory usage** for all scenarios -- 🧠 **Smarter resource management** based on actual usage intent -- πŸ”„ **Same API and behavior** with zero breaking changes - -This demonstrates how thoughtful optimization can improve both performance AND memory efficiency simultaneously, while maintaining the exact same developer experience. \ No newline at end of file diff --git a/docs/lazy-initialization-analysis.md b/docs/lazy-initialization-analysis.md deleted file mode 100644 index a35cd04..0000000 --- a/docs/lazy-initialization-analysis.md +++ /dev/null @@ -1,181 +0,0 @@ -# Lazy Initialization Implementation Analysis - -## Memory & Performance Impact - -You were absolutely right to question the always-available registry approach! This document analyzes the memory impact and demonstrates how lazy initialization solves the allocation waste problem. - -## Before vs After Comparison - -### Previous Architecture (Nullable Registries) -```csharp -// Memory when registries disabled: 0 allocations βœ… -var context = new CapabilityContext(); // No registries created - -// Memory when registries enabled: Full allocation cost -var context = new CapabilityContext(new CapabilityContextOptions { - ComposerRegistry = new DefaultComposerRegistry(), - CompositionRegistry = new DefaultCompositionRegistry() -}); -``` - -### Initial Boolean Flag Implementation (Always-Available) -```csharp -// Memory when registries disabled: Full allocation cost ❌ -var context = new CapabilityContext(new CapabilityContextOptions { - UseComposerRegistry = false, - UseCompositionRegistry = false -}); -// Still created: 2 registries + 2 ConditionalWeakTables + 2 API wrappers -``` - -### Final Lazy Initialization Implementation -```csharp -// Memory when registries disabled and never accessed: Minimal allocations βœ… -var context = new CapabilityContext(new CapabilityContextOptions { - UseComposerRegistry = false, - UseCompositionRegistry = false -}); -// Only created: 4 Lazy wrappers (very lightweight) - -// Memory created on-demand only when needed βœ… -var composer = context.Composers; // Now ComposerRegistry + API created -// CompositionRegistry still not created! -``` - -## Memory Allocation Analysis - -### Object Creation Costs - -| Scenario | Lazy Wrappers | Registry Objects | ConditionalWeakTable | ConcurrentDictionary | API Wrappers | -|----------|---------------|------------------|---------------------|---------------------|--------------| -| **Never accessed** | 4 Γ— ~24 bytes | 0 | 0 | 0 | 0 | -| **Composers only** | 4 Γ— ~24 bytes | 1 | 1 | 1 (shared static) | 1 | -| **Both accessed** | 4 Γ— ~24 bytes | 2 | 2 | 1 (shared static) | 2 | - -### Memory Overhead Breakdown - -```csharp -// Lazy wrapper overhead: ~24 bytes per wrapper -private readonly Lazy _lazyComposerRegistry; -private readonly Lazy _lazyCompositionRegistry; -private readonly Lazy _lazyComposerRegistryApi; -private readonly Lazy _lazyCompositionRegistryApi; - -// Total overhead when never accessed: ~96 bytes vs 0 bytes (previous nullable approach) -// But this is negligible compared to the full registry allocation cost -``` - -### Full Registry Allocation Cost -```csharp -// DefaultComposerRegistry: ~200+ bytes -// - ConditionalWeakTable: ~100+ bytes -// - Static ConcurrentDictionary: shared across instances -// - Object overhead: ~40+ bytes - -// ComposerRegistryApi: ~40+ bytes -// Similar costs for composition registry and API - -// Total when both registries created: ~500+ bytes -``` - -## Performance Characteristics - -### Lazy Initialization Timing -- **First access**: One-time allocation cost + initialization -- **Subsequent access**: Direct field access (no performance penalty) -- **Thread safety**: `Lazy` handles concurrent initialization automatically - -### Boolean Flag Control Benefits -1. **Explicit behavior**: No guessing whether registries are enabled -2. **Zero allocation when unused**: True zero-cost abstraction -3. **On-demand creation**: Pay only for what you use -4. **Method-level overrides**: Fine-grained control without waste - -## Usage Patterns & Memory Impact - -### Pattern 1: Never Use Registries -```csharp -var context = new CapabilityContext(new CapabilityContextOptions { - UseComposerRegistry = false, - UseCompositionRegistry = false -}); - -// Use only basic functionality -var composer = context.For(document, useRegistry: false); -var composition = composer.Add(capability).Build(useRegistry: false); - -// Memory impact: Only 4 Γ— Lazy wrappers (~96 bytes) -// Previous always-available: ~500+ bytes wasted ❌ -// Lazy implementation: ~96 bytes total βœ… -``` - -### Pattern 2: Use Only Composer Registry -```csharp -var context = new CapabilityContext(new CapabilityContextOptions { - UseComposerRegistry = true, - UseCompositionRegistry = false -}); - -var composer = context.For(document); // Creates composer registry -// Access context.Composers property // Creates API wrapper - -// Memory impact: ~340 bytes (registry + API + lazy wrappers) -// CompositionRegistry never created, saving ~200+ bytes -``` - -### Pattern 3: Conditional Registry Access -```csharp -var context = new CapabilityContext(); - -// Sometimes use registries -if (needsPersistence) { - var existing = context.Composers.TryGet(document, out var found); -} - -// Sometimes don't use registries -var directComposer = context.For(document, useRegistry: false); - -// Memory impact: Registries created only when API properties accessed -``` - -## Key Benefits Achieved - -### 1. **True Zero-Cost Abstraction** -- When registries are disabled and never accessed: minimal memory footprint -- No unnecessary object creation -- Maintains all functionality through method overrides - -### 2. **Intelligent Resource Management** -- Resources allocated only when actually needed -- Lazy disposal: only dispose objects that were created -- Thread-safe initialization without locking overhead - -### 3. **Optimal Developer Experience** -- Boolean flags provide explicit control -- Method overrides enable per-operation decisions -- No null checking required (registries always "available" when accessed) - -### 4. **Performance Characteristics** -- **Cold start**: Minimal allocation cost -- **Warm usage**: No performance penalty -- **Memory pressure**: Only pay for what you use - -## Validation Through Tests - -The `LazyInitializationTests` demonstrate: - -1. **Zero allocation when unused**: `Context_WithoutRegistryAccess_DoesNotCreateRegistries` -2. **Selective creation**: `Context_AccessingComposersProperty_CreatesOnlyComposerRegistry` -3. **On-demand behavior**: `Context_UsingRegistryWithDisabledFlag_CreatesRegistryOnDemand` -4. **Intelligent resource usage**: Only creating what's actually needed - -## Conclusion - -The lazy initialization approach provides the best of both worlds: - -- βœ… **Memory efficient**: Zero waste when registries unused -- βœ… **Developer friendly**: No null checks, explicit boolean control -- βœ… **Performance optimal**: Pay only for what you use -- βœ… **Fully functional**: All features available through method overrides - -Your instinct was absolutely correct - always creating registries "felt wrong" because it was wasteful. The lazy initialization approach eliminates this waste while maintaining all the benefits of the boolean flag system. \ No newline at end of file diff --git a/docs/lifecycle-and-disposal.md b/docs/lifecycle-and-disposal.md deleted file mode 100644 index e99c6f7..0000000 --- a/docs/lifecycle-and-disposal.md +++ /dev/null @@ -1,49 +0,0 @@ -# Lifecycle & Disposal - -This library separates three concerns: - -| Concern | Lifetime | Disposal Responsibility | -| ------- | -------- | ----------------------- | -| CapabilityScope | Explicit (Dispose or GC) | User (call Dispose when done) | -| Composer | Ephemeral builder | Not disposable (transient) | -| Composition | Immutable snapshot | User holds reference; not disposed by scope | -| Registry Entries | Internal index nodes | Disposed (owned resources) when scope disposed | - -## Scope Disposal Semantics - -Disposing a `CapabilityScope`: -- Prevents creation of new composers (`scope.For(...)` throws `ObjectDisposedException`). -- Stops registry usage (lookups / new registrations throw if they touch the disposed scope indirectly). -- Disposes any disposable objects referenced by value-type subject entries (and, if added later, any tracked disposables) via `CapabilityEntry.DisposeOwnedResources()`. -- Releases references to reference-type subject entries (stored in a `ConditionalWeakTable`), allowing GC to reclaim them naturally. - -Already-built compositions remain fully usable because they are immutable and do not depend on scope internals once created. - -## Why Compositions Aren't Disposed -`Composition` is a pure data container (arrays + dictionaries). It does not own external resources. Disposing would add ceremony without benefit. If, in the future, compositions wrap disposables, an explicit `IDisposable` implementation can be introduced without breaking existing semantics. - -## Holding References After Disposal -It is safeβ€”and expectedβ€”to hold a composition reference after disposing the scope: -```csharp -var scope = new CapabilityScope(new CapabilityScopeOptions { UseCompositionRegistry = true }); -var comp = scope.For("svc", useRegistry: true) - .Add(new LoggingCapability(LogLevel.Info, "cat")) - .Build(useRegistry: true); - -scope.Dispose(); - -// Still valid: immutable snapshot -var loggers = comp.GetAll>(); -``` - -Registry lookups after disposal will fail (or return false) because the registry has been torn down. - -## Recommended Practices -- Dispose the scope when you are done registering or discovering compositions globally. -- Store compositions where you need them (DI container, cache, etc.). -- Do not assume registry disposal invalidates existing compositions. -- If you introduce capabilities that implement `IDisposable`, manage their disposal explicitly or extend the registry tracking to own them. - -## Future Extension Hooks -If later you need explicit disposal for reference-type entries: add an internal tracking list during registration and enumerate it in `DefaultCapabilityRegistry.Dispose()` similar to value-type entries. - diff --git a/docs/performance-analysis.md b/docs/performance-analysis.md deleted file mode 100644 index e617555..0000000 --- a/docs/performance-analysis.md +++ /dev/null @@ -1,160 +0,0 @@ -# Cocoar.Capabilities Performance Analysis - -## Executive Summary - -Cocoar.Capabilities offers **two architectures** with distinct performance characteristics, enabling you to choose the optimal approach for your scenario. Our comprehensive benchmarks demonstrate excellent performance across both Core-Only and Registry architectures. - -## Architecture Comparison - -### Registry Disabled (legacy docs term: Core-Only) -**Maximum performance** - you manage composition lifetimes directly: -- **Build**: ~4.6 ΞΌs (50 capabilities), ~42 ΞΌs (500 capabilities) -- **Query**: ~142 ns (feature queries), ~1 ΞΌs (all capabilities) -- **Memory**: 11-102 KB build allocations, 320B-1.2KB query allocations - -### Registry Architecture (`Cocoar.Capabilities`) -**Convenience with overhead** - automatic global composition storage: -- **Build**: ~8.3 ΞΌs (50 capabilities), ~47 ΞΌs (500 capabilities) - *+79% and +13% overhead* -- **Query**: ~151 ns (feature queries), ~8.5 ΞΌs (large all capabilities) - *+6% to +757% overhead* -- **Memory**: Similar to Core with small registry overhead (27-34 bytes) - -## Detailed Performance Results - -### Build Performance -| Architecture | Small (50 caps) | Large (500 caps) | Overhead | -|-------------|------------------|------------------|----------| -| **Core** | 4.64 ΞΌs | 41.68 ΞΌs | Baseline | -| **Registry** | 8.30 ΞΌs | 47.17 ΞΌs | +79% / +13% | - -### Query Performance - Feature Capabilities -| Architecture | Small | Large | Overhead | -|-------------|-------|-------|----------| -| **Core** | 142 ns | 139 ns | Baseline | -| **Registry** | 150 ns | 850 ns | +6% / +511% | - -### Query Performance - All Capabilities -| Architecture | Small | Large | Overhead | -|-------------|-------|-------|----------| -| **Core** | 1.06 ΞΌs | 992 ns | Baseline | -| **Registry** | 979 ns | 8.5 ΞΌs | -8% / +757% | - -## Performance Characteristics - -### Scaling Behavior -- **Core Builds**: Linear scaling - 10x capabilities = ~8x time -- **Core Queries**: Constant time regardless of composition size -- **Registry Builds**: Similar linear scaling with consistent overhead -- **Registry Queries**: Variable overhead depending on operation complexity - -### Memory Allocation Patterns -| Scenario | Core Allocation | Registry Overhead | Efficiency | -|----------|-----------------|-------------------|------------| -| Small Build | 10.84 KB | +27 bytes | 99.75% | -| Large Build | 101.79 KB | +34 bytes | 99.97% | -| Feature Query | 320 B | +0 bytes | 100% | -| All Query (Small) | 1.18 KB | +0 bytes | 100% | -| All Query (Large) | 1.18 KB | +7.2 KB | 14% | - -### Optional Capability Ordering Overhead - -Capability ordering is conditional. If no capability implements `IOrderedCapability`, the ordering scan exits immediately (O(n) predicate scan with early break, typically branch-predictable) and no sort occurs. - -When ordered capabilities are present: -1. A single pass records original indices to preserve stability when duplicate `Order` values exist. -2. A one-time stable sort runs using the original index map as a secondary key. -3. The resulting arrays are cached in the immutable composition; subsequent `GetAll()` / `GetAll()` calls do not re-sort. - -Benchmark highlights (relative observations – see `OrderingBenchmarks` for reproducible runs): -- Already sorted or small sets: sort cost often below noise threshold. -- Reverse-ordered worst case: additional cost is proportional to `n log n` but still dominated by capability instantiation for moderate sizes (≀500). -- Duplicate order groups: stability bookkeeping adds a small dictionary allocation already amortized by capability count. -- Recompositions with no structural change: skip sort entirely (arrays reused) yielding near-zero overhead. - -Practical guidance: only pay for ordering when you declare it; you can freely mix ordered and unordered capability sets without global penalties. - -## Understanding Registry Overhead - -### Why Overhead Exists -Registry overhead is **inherent to any composition storage system**: - -1. **Build Phase**: Core build + registration in global storage -2. **Query Phase**: Core query + storage lookup + additional indirection - -This is **not** a library limitation - it's the fundamental cost of persistent composition storage. Any mechanism providing global composition access (DI containers, caches, etc.) would have similar overhead. - -### When Overhead Matters -- **High-frequency builds**: Core is 13-79% faster -- **Complex queries on large sets**: Core is 511-757% faster -- **Simple queries on small sets**: Registry can be 8% faster (caching benefits) - -## Performance Optimization Guidelines - -### Choose Core-Only When: -βœ… Building many large compositions -βœ… Performing frequent capability queries -βœ… Maximum performance is critical -βœ… You have existing object lifecycle management -βœ… Memory efficiency is paramount - -### Choose Registry When: -βœ… Global composition discovery is needed -βœ… Simple compositions with occasional queries -βœ… Convenience outweighs performance -βœ… No existing object storage mechanisms -βœ… Prototype/development speed is priority - -## Technical Implementation Details - -### Thread Safety -- **Immutable by design** - no locks required -- **Concurrent reads** - unlimited parallelism for queries -- **Isolated builds** - each composition build is independent - -### Memory Management -- **Weak references** for reference types (automatic cleanup) -- **Explicit control** for value types (prevents leaks) -- **Zero allocations** for capability existence checks (`Has()`) -- **Minimal GC pressure** during normal operations - -### Framework Compatibility -Targets modern .NET (net8.0+) with AOT-friendly design (no runtime code generation) and zero external dependencies. Legacy multi-package (.Core vs registry) distribution has been consolidated into a single package. Assembly size remains small (~tens of KB) and stable across configurations. - -## Benchmark Environment - -**Hardware**: Snapdragon X Elite - X1E78100 @ 3.42GHz (ARM64) -**Runtime**: .NET 9.0.9 with RyuJIT -**Configuration**: Release build, BenchmarkDotNet with statistical analysis -**Methodology**: 13-15 iterations with outlier detection and confidence intervals - -> Performance results are representative but will vary by hardware, runtime version, and workload characteristics. Use these numbers for relative comparison and architectural decision-making. - -## Migration Guidance (Legacy Static API β†’ Scope API) - -Previous examples used static helpers (`Composer.For`, `BuildAndRegister`, `Composition.FindOrDefault`). Transition to `CapabilityScope` is direct: - -### Legacy (Static) -```csharp -using var legacyScope = new CapabilityScope(new CapabilityScopeOptions { UseCompositionRegistry = true }); -var composition = legacyScope.For(subject).Add(...).Build(); // registered (options enabled) -var found = legacyScope.Compositions.FindOrDefault(subject); -``` - -### Current (Scope + Options) -```csharp -using var scope = new CapabilityScope(new CapabilityScopeOptions { UseCompositionRegistry = true }); -var composition = scope.For(subject).Add(...).Build(); // auto-registered -var found = scope.Compositions.FindOrDefault(subject); -``` - -Disable registry for max performance: -```csharp -using var scope = new CapabilityScope(new CapabilityScopeOptions { UseCompositionRegistry = false }); -var localOnly = scope.For(subject).Add(...).Build(); // not tracked -``` - -Force a single build to register even if globally disabled: -```csharp -var mixed = scope.For(subject).Add(...).Build(useRegistry: true); -``` - -See `static-api-migration-strategy.md` for deeper rewrite patterns. \ No newline at end of file diff --git a/docs/registration-and-querying.md b/docs/registration-and-querying.md deleted file mode 100644 index ed65671..0000000 --- a/docs/registration-and-querying.md +++ /dev/null @@ -1,204 +0,0 @@ -# Registration and Querying Behavior Guide - -This document explains the complete behavior of capability registration and querying in Cocoar.Capabilities, including the recent changes that introduced contract-only registration semantics and solved interface contamination issues. - -> **πŸ“š Related Guides:** -> - For advanced tuple registration patterns, see [Tuple Contract Syntax](guides/tuple-contracts.md) -> - For primary capability concepts, see [Primary Capabilities](guides/primary-capabilities.md) -> - For memory management details, see [Memory Management](guides/memory-management.md) - -## Overview - -The system now uses an **ID-based architecture** with **contract-only registration semantics**. Each `CapabilityScope` owns its own registry; there is no injectable or shared global registry surface. This means: - -- Each capability gets a unique ID when registered -- Capabilities are only queryable by the exact types they were registered under -- No more "interface contamination" where capabilities become accidentally queryable by unrelated interfaces - -## Registration Methods - -### `Add(TCapability capability)` - -Registers a capability under its **concrete type only**. - -```csharp -using var scope = new CapabilityScope(); -var logCapability = new LogCapability(LogLevel.Info); -var composition = scope.For(userService) - .Add(logCapability) - .Build(); -``` - -**Registration Result:** -- βœ… Queryable by: `LogCapability` (concrete type) -- ❌ NOT queryable by: `ILogCapability` (interface, even if implemented) - -### `AddAs(TContract capability)` - -Registers a capability under the **specified contract type only**. - -```csharp -using var scope = new CapabilityScope(); -var logCapability = new LogCapability(LogLevel.Info); -var composition = scope.For(userService) - .AddAs>(logCapability) - .Build(); -``` - -**Registration Result:** -- βœ… Queryable by: `ILogCapability` (specified contract) -- ❌ NOT queryable by: `LogCapability` (concrete type) - -### `AddAs<(Type1, Type2, ...)>(capability)` - Tuple Registration - -Registers a capability under **multiple contract types simultaneously**. - -```csharp -using var scope = new CapabilityScope(); -var logCapability = new LogCapability(LogLevel.Info); -builder.AddAs<(ILogCapability, LogCapability)>(logCapability); -``` - -**Registration Result:** -- βœ… Queryable by: `ILogCapability` (first contract) -- βœ… Queryable by: `LogCapability` (second contract) - -## Querying Behavior - -All querying methods (`TryGet`, `GetRequired`, `GetAll`, `Contains`) use **exact type matching** and only return capabilities that were explicitly registered under the queried type. - -Cross-component retrieval is performed through the same scope instance that created the composition (or a reference you keep to the composition itself). You can keep a scope as a long-lived container if you need shared discovery. - -### Example: Interface Implementation vs Registration - -```csharp -// Setup -public class EmailValidator : IValidationCapability -{ - public bool IsValid(string email) => email.Contains("@"); -} - -var validator = new EmailValidator(); -``` - -#### Scenario 1: Concrete Registration -```csharp -builder.Add(validator); // Registered as EmailValidator only - -// Querying -bag.TryGet(out var concrete); // βœ… Found -bag.TryGet>(out var i); // ❌ NOT Found -``` - -#### Scenario 2: Interface Registration -```csharp -builder.AddAs>(validator); // Registered as interface only - -// Querying -bag.TryGet(out var concrete); // ❌ NOT Found -bag.TryGet>(out var i); // βœ… Found -``` - -#### Scenario 3: Multiple Registration (Tuple) -```csharp -builder.AddAs<(IValidationCapability, EmailValidator)>(validator); - -// Querying -bag.TryGet(out var concrete); // βœ… Found -bag.TryGet>(out var i); // βœ… Found -``` - -## Key Behavior Changes - -### Before (Old System) -❌ **Interface Contamination Problem:** -```csharp -builder.Add(new EmailValidator()); // Concrete registration - -// Old behavior - interface contamination -bag.TryGet>(out var validator); // Would find it! -// This was problematic because it wasn't explicitly registered for the interface -``` - -### After (New System) -βœ… **Contract-Only Semantics:** -```csharp -builder.Add(new EmailValidator()); // Concrete registration only - -// New behavior - contract-only -bag.TryGet>(out var validator); // Does NOT find it -bag.TryGet(out var concrete); // Finds it correctly -``` - -## Practical Examples - -### Example 1: Mixed Registration Types - -```csharp -var validator1 = new EmailValidator(); -var validator2 = new PhoneValidator(); -var validator3 = new AddressValidator(); - -var bag = Composer.For(userService) - .Add(validator1) // Concrete only - .AddAs>(validator2) // Interface only - .AddAs<(IValidationCapability, AddressValidator)>(validator3) // Both - .Build(); - -// Querying results: -var concreteValidators = bag.GetAll(); // [validator1] -var interfaceValidators = bag.GetAll>(); // [validator2, validator3] -var addressValidators = bag.GetAll(); // [validator3] -``` - -### Example 2: RemoveWhere with Mixed Registration - -```csharp -builder.Add(new LogCapability("Debug")) // Concrete - .AddAs>(new LogCapability("Info")) // Interface - .AddAs<(ILogCapability, LogCapability)>(new LogCapability("Error")); // Both - -// Remove all ILogCapability implementations (works regardless of registration type) -builder.RemoveWhere(cap => cap is ILogCapability); - -// Result: All three are removed because they all implement ILogCapability -``` - -## Best Practices - -### 1. Be Explicit About Contracts -```csharp -// βœ… Good - explicit about what should be queryable -builder.AddAs>(logCapability); - -// ⚠️ Be aware - only queryable by concrete type -builder.Add(logCapability); -``` - -### 2. Use Tuple Registration for Dual Access -```csharp -// βœ… Good - when you need both interface and concrete access -builder.AddAs<(ILogCapability, LogCapability)>(logCapability); -``` - -### 3. Prefer Interface Registration for Flexibility -```csharp -// βœ… Good - allows swapping implementations -builder.AddAs>(new EmailValidator()); -builder.AddAs>(new PhoneValidator()); - -// Query by interface -var validators = bag.GetAll>(); // Gets both -``` - -## Summary - -The new system provides: - -- βœ… **Predictable behavior**: Capabilities are only queryable by explicitly registered types -- βœ… **No interface contamination**: Concrete registrations don't accidentally become queryable by interfaces -- βœ… **Flexible registration**: Tuple syntax allows multiple contract registration -- βœ… **Powerful removal**: RemoveWhere works with pattern matching across all registration types -- βœ… **Performance**: ID-based internal architecture is faster and simpler - -The key principle: **You get exactly what you register for, nothing more, nothing less.** Per-scope registries ensure isolation and predictable lifecycle management. \ No newline at end of file diff --git a/docs/static-api-migration-strategy.md b/docs/static-api-migration-strategy.md deleted file mode 100644 index b497381..0000000 --- a/docs/static-api-migration-strategy.md +++ /dev/null @@ -1,241 +0,0 @@ -# Static API Migration Strategy - Backward Compatibility & Migration Path - -## Overview - -The new context-based architecture provides static default contexts that maintain complete backward compatibility with the existing static API while enabling smooth migration to the more powerful context-based approach. - -## Static Default Contexts - -### CapabilityContext.Default -```csharp -/// -/// Gets a default context with both registries enabled. -/// This provides the same behavior as the previous static API. -/// -public static CapabilityContext Default { get; } = new CapabilityContext(); -``` - -**Characteristics:** -- βœ… `UseComposerRegistry = true` (default) -- βœ… `UseCompositionRegistry = true` (default) -- βœ… Auto-registers composers and compositions -- βœ… Provides global shared state like previous static API - -### CapabilityContext.Lightweight -```csharp -/// -/// Gets a context with registries disabled for lightweight operation. -/// Use this when you don't need persistence or cross-reference capabilities. -/// -public static CapabilityContext Lightweight { get; } = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = false, - UseCompositionRegistry = false -}); -``` - -**Characteristics:** -- ❌ `UseComposerRegistry = false` -- ❌ `UseCompositionRegistry = false` -- ⚑ Optimized for performance scenarios -- πŸ’Ύ Minimal memory footprint when unused - -## Updated Static API - -The existing static `Composer` class now delegates to `CapabilityContext.Default`: - -### Before (Old Implementation) -```csharp -public static class Composer -{ - public static Composer For(TSubject subject) - where TSubject : notnull - { - // Created isolated composers with no shared state - return new Composer(subject); - } -} -``` - -### After (New Implementation) -```csharp -public static class Composer -{ - /// - /// Creates a composer for the specified subject using the default context. - /// This provides the same behavior as the previous static API. - /// - public static Composer For(TSubject subject) - where TSubject : notnull - { - return CapabilityContext.Default.For(subject); - } - - /// - /// Creates a composer from an existing composition using the default context. - /// - public static Composer Recompose(IComposition existingComposition) - where TSubject : notnull - { - return new Composer(existingComposition, CapabilityContext.Default); - } -} -``` - -## Migration Paths - -### 1. Zero-Change Migration (Immediate Compatibility) -```csharp -// Existing code continues to work unchanged -var composer = Composer.For(document); -var composition = composer.Add(new MetadataCapability("author", "John")).Build(); - -// Now automatically registered in CapabilityContext.Default -// Can be accessed via context API: -Assert.True(CapabilityContext.Default.Composers.TryGet(document, out var found)); -Assert.Same(composer, found); -``` - -### 2. Gradual Migration (Progressive Enhancement) -```csharp -// Phase 1: Keep using static API -var legacyComposer = Composer.For(document); -var legacyComposition = legacyComposer.Add(capability).Build(); - -// Phase 2: Access via context when needed -var registeredComposition = CapabilityContext.Default.Compositions - .TryGet(document, out var composition) ? composition : null; - -// Phase 3: New features use context directly -var contextComposer = CapabilityContext.Default.For(document, useRegistry: false); -var performanceComposition = contextComposer.Add(capability).Build(useRegistry: false); -``` - -### 3. Full Migration (Context-First Approach) -```csharp -// Replace static calls with context calls -// Before: -// var composer = Composer.For(document); - -// After: -var composer = CapabilityContext.Default.For(document); - -// Benefit: Access to registry override parameters -var lightweightComposer = CapabilityContext.Default.For(document, useRegistry: false); -var persistentComposition = composer.Build(useRegistry: true); -``` - -### 4. Custom Context Migration -```csharp -// For applications needing custom behavior -var customContext = new CapabilityContext(new CapabilityContextOptions -{ - UseComposerRegistry = true, - UseCompositionRegistry = false, - ComposerRegistry = new MyCustomRegistry() -}); - -// Migrate from static to custom context gradually -var composer = customContext.For(document); // Instead of Composer.For(document) -``` - -## Usage Examples - -### Static API Equivalents - -| Old Static API | New Context API Equivalent | Benefits | -|---------------|---------------------------|----------| -| `Composer.For(subject)` | `CapabilityContext.Default.For(subject)` | βœ… Method-level overrides | -| N/A | `CapabilityContext.Lightweight.For(subject)` | ⚑ Performance optimization | -| N/A | `context.For(subject, useRegistry: false)` | 🎯 Fine-grained control | - -### Backward Compatibility Examples - -```csharp -// All existing code works unchanged -var document = new Document("example.txt"); - -// 1. Static API (unchanged) -var composer1 = Composer.For(document); -var composition1 = composer1.Add(new MetadataCapability("author", "Alice")).Build(); - -// 2. Context API (equivalent behavior) -var composer2 = CapabilityContext.Default.For(document); -var composition2 = composer2.Add(new MetadataCapability("editor", "Bob")).Build(); - -// 3. Both are registered in the same default context -Assert.True(CapabilityContext.Default.Composers.TryGet(document, out var foundComposer)); -Assert.True(CapabilityContext.Default.Compositions.TryGet(document, out var foundComposition)); - -// 4. Registry APIs now available -var allComposers = CapabilityContext.Default.Composers.GetAll(); // New capability -var allCompositions = CapabilityContext.Default.Compositions.GetAll(); // New capability -``` - -### Performance Optimization Examples - -```csharp -// 1. Lightweight operations (no registry overhead) -var fastComposer = CapabilityContext.Lightweight.For(document); -var fastComposition = fastComposer.Add(capability).Build(); - -// 2. Conditional registration -var composer = CapabilityContext.Default.For(document, useRegistry: shouldPersist); -var composition = composer.Add(capability).Build(useRegistry: shouldPersist); - -// 3. Mixed usage patterns -var defaultComposer = CapabilityContext.Default.For(document); // Auto-registered -var lightweightComposer = CapabilityContext.Lightweight.For(document); // Not registered -``` - -## Benefits Achieved - -### βœ… **Complete Backward Compatibility** -- All existing static API calls work unchanged -- Same behavior and semantics -- Zero breaking changes - -### βœ… **Smooth Migration Path** -- Multiple migration strategies available -- Can migrate incrementally -- Old and new APIs interoperate seamlessly - -### βœ… **Enhanced Capabilities** -- Access to registry APIs through static contexts -- Method-level registry control -- Performance optimization options - -### βœ… **Future-Proof Architecture** -- Context-based design enables future enhancements -- Dependency injection ready -- Testing and isolation friendly - -## Removal Strategy for Old Static Methods - -Now that the static API delegates to the default context, you can: - -1. **Keep the static API** for backward compatibility (recommended) -2. **Mark as obsolete** with migration guidance: - ```csharp - [Obsolete("Use CapabilityContext.Default.For(subject) instead")] - public static Composer For(TSubject subject) - ``` -3. **Update documentation** to recommend context API for new code -4. **Update existing tests** to use context API directly - -## Testing Strategy - -All tests can be updated to use the static contexts: - -```csharp -// Before: -var composer = Composer.For(subject); - -// After (equivalent behavior): -var composer = CapabilityContext.Default.For(subject); - -// Or for isolated testing: -var composer = CapabilityContext.Lightweight.For(subject); -``` - -This approach provides the best of both worlds: **complete backward compatibility** with **smooth migration paths** to the more powerful context-based architecture. \ No newline at end of file diff --git a/src/Cocoar.Capabilities.Benchmarks/CanonicalizationBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/CanonicalizationBenchmarks.cs index 2d117de..5ebc1c1 100644 --- a/src/Cocoar.Capabilities.Benchmarks/CanonicalizationBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/CanonicalizationBenchmarks.cs @@ -7,8 +7,8 @@ namespace Cocoar.Capabilities.Benchmarks; public class CanonicalizationBenchmarks : IDisposable { public record struct ValueSubject(int Id); - public record StringCapability(string Name) : ICapability; - public record ValueCapability(string Name) : ICapability; + public record StringCapability(string Name) ; + public record ValueCapability(string Name) ; private CapabilityScope _stringScope = null!; private CapabilityScope _valueScope = null!; diff --git a/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs index f204e4c..608a4a7 100644 --- a/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs @@ -8,17 +8,17 @@ namespace Cocoar.Capabilities.Benchmarks; public class CapabilityBenchmarks { public record TestSubject(int Id, string Name); - public record FeatureCapability(string Name) : ICapability; - public record ConfigCapability(string Key, string Value) : ICapability; - public record ValidationCapability(string Rule) : ICapability; - public record CachingCapability(string CacheKey, TimeSpan Duration) : ICapability; - public record LoggingCapability(string LoggerName) : ICapability; - public record SecurityCapability(string Permission, string Role) : ICapability; - public record MonitoringCapability(string MetricName) : ICapability; - public record RetryCapability(string Operation, int MaxRetries) : ICapability; + public record FeatureCapability(string Name) ; + public record ConfigCapability(string Key, string Value) ; + public record ValidationCapability(string Rule) ; + public record CachingCapability(string CacheKey, TimeSpan Duration) ; + public record LoggingCapability(string LoggerName) ; + public record SecurityCapability(string Permission, string Role) ; + public record MonitoringCapability(string MetricName) ; + public record RetryCapability(string Operation, int MaxRetries) ; - private IComposition _small10x50 = null!; - private IComposition _large1000x50 = null!; + private IComposition _small10x50 = null!; + private IComposition _large1000x50 = null!; private TestSubject _registryTestSubject = null!; private TestSubject _registryTestSubjectLarge = null!; @@ -35,7 +35,7 @@ public void Setup() CreateAndRegisterComposition(_registryTestSubjectLarge, 500); } - private static IComposition CreateComposition(int subjects, int capabilitiesPerSubject) + private static IComposition CreateComposition(int subjects, int capabilitiesPerSubject) { if (subjects == 1) { @@ -69,7 +69,7 @@ private static IComposition CreateComposition(int subjects, int cap } } - private static IComposition CreateAndRegisterComposition(TestSubject subject, int capabilitiesCount) + private static IComposition CreateAndRegisterComposition(TestSubject subject, int capabilitiesCount) { var composer = BenchmarkScopes.Shared.For(subject); @@ -82,7 +82,7 @@ private static IComposition CreateAndRegisterComposition(TestSubjec return composer.Build(useRegistry: true); } - private static ICapability CreateCapability(int subjectId, int capabilityId) + private static ICapability CreateCapability(int subjectId, int capabilityId) { return (capabilityId % 8) switch { @@ -99,11 +99,11 @@ private static ICapability CreateCapability(int subjectId, int capa // Build Performance Tests - Systematic Scaling [Benchmark] - public IComposition Build_Small_1x50() + public IComposition Build_Small_1x50() { return CreateComposition(1, 50); } [Benchmark] - public IComposition Build_Large_1x500() + public IComposition Build_Large_1x500() { return CreateComposition(1, 500); } @@ -136,7 +136,7 @@ public int Count_Large_FeatureCapabilities() // Registry comparison - Build + Register + Retrieve from Registry [Benchmark] - public IComposition Build_Registry_Small_1x50() + public IComposition Build_Registry_Small_1x50() { // Use same pattern as Core version for fair comparison var subject = new TestSubject(0, "Subject_0"); @@ -156,7 +156,7 @@ public IComposition Build_Registry_Small_1x50() } [Benchmark] - public IComposition Build_Registry_Large_1x500() + public IComposition Build_Registry_Large_1x500() { // Use SAME subject ID as Core version for fair comparison var subject = new TestSubject(0, "Subject_0"); diff --git a/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs index 7f6e3fa..bfc4323 100644 --- a/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs @@ -8,17 +8,17 @@ namespace Cocoar.Capabilities.Benchmarks; public class CoreVsRegistryBenchmarks { public record TestSubject(int Id, string Name); - public record FeatureCapability(string Name) : ICapability; - public record ConfigCapability(string Key, string Value) : ICapability; - public record ValidationCapability(string Rule) : ICapability; - public record CachingCapability(string CacheKey, TimeSpan Duration) : ICapability; - public record LoggingCapability(string LoggerName) : ICapability; - public record SecurityCapability(string Permission, string Role) : ICapability; - public record MonitoringCapability(string MetricName) : ICapability; - public record RetryCapability(string Operation, int MaxRetries) : ICapability; + public record FeatureCapability(string Name) ; + public record ConfigCapability(string Key, string Value) ; + public record ValidationCapability(string Rule) ; + public record CachingCapability(string CacheKey, TimeSpan Duration) ; + public record LoggingCapability(string LoggerName) ; + public record SecurityCapability(string Permission, string Role) ; + public record MonitoringCapability(string MetricName) ; + public record RetryCapability(string Operation, int MaxRetries) ; - private IComposition _coreComposition = null!; - private IComposition _registryComposition = null!; + private IComposition _coreComposition = null!; + private IComposition _registryComposition = null!; private TestSubject _registrySubject = null!; [GlobalSetup] @@ -43,14 +43,14 @@ public void Cleanup() [Benchmark(Description = "Core: Build 50 caps (fast)")] [BenchmarkCategory("Build", "Core")] - public IComposition Build_Core_50() + public IComposition Build_Core_50() { return CreateCoreComposition(1, 50); } [Benchmark(Description = "Registry: Build + Register 50 caps")] [BenchmarkCategory("Build", "Registry")] - public IComposition Build_Registry_50() + public IComposition Build_Registry_50() { var subject = new TestSubject(100, "BuildTest"); var composition = CreateRegistryComposition(subject, 50); @@ -62,14 +62,14 @@ public IComposition Build_Registry_50() [Benchmark(Description = "Core: Build 200 caps (fast)")] [BenchmarkCategory("Build", "Core")] - public IComposition Build_Core_200() + public IComposition Build_Core_200() { return CreateCoreComposition(1, 200); } [Benchmark(Description = "Registry: Build + Register 200 caps")] [BenchmarkCategory("Build", "Registry")] - public IComposition Build_Registry_200() + public IComposition Build_Registry_200() { var subject = new TestSubject(200, "BuildTest"); var composition = CreateRegistryComposition(subject, 200); @@ -113,7 +113,7 @@ public int Query_Registry_AllCapabilities() [Benchmark(Description = "Core: Direct reference (fastest)")] [BenchmarkCategory("Lookup", "Core")] - public IComposition Lookup_Core_DirectAccess() + public IComposition Lookup_Core_DirectAccess() { // Core pattern: Direct composition reference (zero overhead) return _coreComposition; @@ -121,7 +121,7 @@ public IComposition Lookup_Core_DirectAccess() [Benchmark(Description = "Registry: Global FindOrDefault")] [BenchmarkCategory("Lookup", "Registry")] - public IComposition? Lookup_Registry_FindOrDefault() + public IComposition? Lookup_Registry_FindOrDefault() { // Registry pattern: Global lookup (convenience with overhead) BenchmarkScopes.Shared.Compositions.TryFind(_registrySubject, out var composition); @@ -138,7 +138,7 @@ public bool Lookup_Registry_TryFind() - private static IComposition CreateCoreComposition(int subjects, int capabilitiesPerSubject) + private static IComposition CreateCoreComposition(int subjects, int capabilitiesPerSubject) { var subject = new TestSubject(0, "CoreSubject"); // Use lightweight scope (registries disabled) @@ -153,7 +153,7 @@ private static IComposition CreateCoreComposition(int subjects, int return composer.Build(); // registries disabled => no registration } - private static IComposition CreateRegistryComposition(TestSubject subject, int capabilitiesPerSubject) + private static IComposition CreateRegistryComposition(TestSubject subject, int capabilitiesPerSubject) { var composer = BenchmarkScopes.Shared.For(subject); @@ -166,7 +166,7 @@ private static IComposition CreateRegistryComposition(TestSubject s return composer.Build(useRegistry: true); } - private static ICapability CreateCapability(int subjectId, int capabilityId) + private static ICapability CreateCapability(int subjectId, int capabilityId) { return (capabilityId % 8) switch { diff --git a/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs index 22ce89a..91dfecd 100644 --- a/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs @@ -18,7 +18,7 @@ public class OrderingBenchmarks : IDisposable { public record Subject(int Id, string Name); - public interface ITestCap : ICapability { } + public interface ITestCap { } public sealed record PlainCap(string Name) : ITestCap; // Unordered @@ -31,8 +31,8 @@ public sealed record OrderedCap(string Name, int Priority) : ITestCap, IOrderedC private CapabilityScope _scope = null!; // Cached compositions for recomposition measurements - private IComposition _unorderedBase = null!; - private IComposition _orderedRandomBase = null!; + private IComposition _unorderedBase = null!; + private IComposition _orderedRandomBase = null!; [Params(50, 200, 500)] public int Count { get; set; } @@ -61,13 +61,13 @@ public void Setup() #region Build (Fresh) [Benchmark(Description = "Build: Unordered")] - public IComposition Build_Unordered() => BuildUnorderedInternal(); + public IComposition Build_Unordered() => BuildUnorderedInternal(); [Benchmark(Description = "Build: Ordered (Random)")] - public IComposition Build_Ordered_Random() => BuildOrderedRandomInternal(); + public IComposition Build_Ordered_Random() => BuildOrderedRandomInternal(); [Benchmark(Description = "Build: Ordered (Already Sorted)")] - public IComposition Build_Ordered_AlreadySorted() + public IComposition Build_Ordered_AlreadySorted() { var composer = _scope.For(new Subject(2, "Sorted")); for (int i = 0; i < Count; i++) @@ -78,7 +78,7 @@ public IComposition Build_Ordered_AlreadySorted() } [Benchmark(Description = "Build: Ordered (Reverse -> Worst Case)")] - public IComposition Build_Ordered_Reverse() + public IComposition Build_Ordered_Reverse() { var composer = _scope.For(new Subject(3, "Reverse")); for (int i = 0; i < Count; i++) @@ -89,7 +89,7 @@ public IComposition Build_Ordered_Reverse() } [Benchmark(Description = "Build: Ordered (Duplicate Priorities)")] - public IComposition Build_Ordered_Duplicates() + public IComposition Build_Ordered_Duplicates() { var composer = _scope.For(new Subject(4, "Duplicates")); for (int i = 0; i < Count; i++) @@ -104,21 +104,21 @@ public IComposition Build_Ordered_Duplicates() #region Recompose (In-place Update) [Benchmark(Description = "Recompose: Unordered (No Change)")] - public IComposition Recompose_Unordered_NoChange() + public IComposition Recompose_Unordered_NoChange() { var composer = _scope.Recompose(_unorderedBase); return composer.Build(); // identity preserved } [Benchmark(Description = "Recompose: Ordered Random (No Change)")] - public IComposition Recompose_Ordered_NoChange() + public IComposition Recompose_Ordered_NoChange() { var composer = _scope.Recompose(_orderedRandomBase); return composer.Build(); } [Benchmark(Description = "Recompose: Ordered Add One")] - public IComposition Recompose_Ordered_Add() + public IComposition Recompose_Ordered_Add() { var composer = _scope.Recompose(_orderedRandomBase); composer.Add(new OrderedCap("NewLast", Count + 10)); @@ -129,8 +129,8 @@ public IComposition Recompose_Ordered_Add() #region Enumeration (GetAll) - private IComposition _enumerationOrdered = null!; - private IComposition _enumerationUnordered = null!; + private IComposition _enumerationOrdered = null!; + private IComposition _enumerationUnordered = null!; [IterationSetup(Targets = new[]{ nameof(Enumerate_All_Ordered), nameof(Enumerate_All_Unordered) })] public void IterationSetupEnumeration() @@ -147,7 +147,7 @@ public int Enumerate_All_Unordered() foreach (var c in _enumerationUnordered.GetAll()) { // cheap side-effect to prevent elimination - var obj = Unsafe.As, object?>(ref Unsafe.AsRef(in c)); + var obj = Unsafe.As(ref Unsafe.AsRef(in c)); if (obj != null) sum += obj.GetHashCode(); } return sum; @@ -159,7 +159,7 @@ public int Enumerate_All_Ordered() int sum = 0; foreach (var c in _enumerationOrdered.GetAll()) { - var obj = Unsafe.As, object?>(ref Unsafe.AsRef(in c)); + var obj = Unsafe.As(ref Unsafe.AsRef(in c)); if (obj != null) sum += obj.GetHashCode(); } return sum; @@ -167,14 +167,14 @@ public int Enumerate_All_Ordered() #endregion - private IComposition BuildUnorderedInternal() + private IComposition BuildUnorderedInternal() { var composer = _scope.For(new Subject(10, "Unordered")); for (int i = 0; i < Count; i++) composer.Add(new PlainCap($"C{i}")); return composer.Build(); } - private IComposition BuildOrderedRandomInternal() + private IComposition BuildOrderedRandomInternal() { var composer = _scope.For(new Subject(11, "OrderedRandom")); for (int i = 0; i < Count; i++) composer.Add(new OrderedCap($"C{i}", _randomOrder[i])); diff --git a/src/Cocoar.Capabilities.Benchmarks/RecompositionBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/RecompositionBenchmarks.cs index 1b913e0..9afdbb1 100644 --- a/src/Cocoar.Capabilities.Benchmarks/RecompositionBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/RecompositionBenchmarks.cs @@ -7,11 +7,11 @@ namespace Cocoar.Capabilities.Benchmarks; public class RecompositionBenchmarks : IDisposable { public record TestSubject(int Id, string Name); - public record Cap(string Name) : ICapability; - public record Primary(string Name) : IPrimaryCapability; + public record Cap(string Name) ; + public record Primary(string Name) : IPrimaryCapability; private CapabilityScope _scope = null!; - private IComposition _baseComposition = null!; + private IComposition _baseComposition = null!; private TestSubject _subject = null!; [GlobalSetup] @@ -26,14 +26,14 @@ public void Setup() } [Benchmark(Description = "Recompose: no changes")] - public IComposition Recompose_NoChange() + public IComposition Recompose_NoChange() { var composer = _scope.Recompose(_baseComposition, useRegistry: true); return composer.Build(useRegistry: true); // identity preserved } [Benchmark(Description = "Recompose: add capability")] - public IComposition Recompose_AddCapability() + public IComposition Recompose_AddCapability() { var composer = _scope.Recompose(_baseComposition, useRegistry: true); composer.Add(new Cap("NewCap")); @@ -41,7 +41,7 @@ public IComposition Recompose_AddCapability() } [Benchmark(Description = "Recompose: replace primary")] - public IComposition Recompose_ReplacePrimary() + public IComposition Recompose_ReplacePrimary() { var composer = _scope.Recompose(_baseComposition, useRegistry: true); composer.WithPrimary(new Primary("P1")); diff --git a/src/Cocoar.Capabilities.Tests/CapabilityEntryTests.cs b/src/Cocoar.Capabilities.Tests/CapabilityEntryTests.cs index 8cb4e6c..3b635e5 100644 --- a/src/Cocoar.Capabilities.Tests/CapabilityEntryTests.cs +++ b/src/Cocoar.Capabilities.Tests/CapabilityEntryTests.cs @@ -5,8 +5,8 @@ namespace Cocoar.Capabilities.Tests; public class CapabilityEntryTests { private sealed record Subject(int Id); - private sealed record Cap(string Name) : ICapability; - private sealed record Primary(string Name) : IPrimaryCapability; + private sealed record Cap(string Name) ; + private sealed record Primary(string Name) : IPrimaryCapability; [Fact] public void Registry_ObjectApis_SucceedForExisting() @@ -33,7 +33,7 @@ public void Registry_ObjectApis_ReturnFalseWhenMissing() Assert.False(scope.Compositions.TryFind(missing, out IComposition? _)); } - private sealed class DisposableComposerCap : ICapability, IDisposable where T : notnull + private sealed class DisposableComposerCap : IDisposable where T : notnull { public bool Disposed; public void Dispose() => Disposed = true; } diff --git a/src/Cocoar.Capabilities.Tests/ComposerPrimaryNegativeTests.cs b/src/Cocoar.Capabilities.Tests/ComposerPrimaryNegativeTests.cs index e6314dd..6e97afc 100644 --- a/src/Cocoar.Capabilities.Tests/ComposerPrimaryNegativeTests.cs +++ b/src/Cocoar.Capabilities.Tests/ComposerPrimaryNegativeTests.cs @@ -3,9 +3,9 @@ namespace Cocoar.Capabilities.Tests; public class ComposerPrimaryNegativeTests { private sealed record Subject(int Id); - private sealed record PrimaryA(string Name) : IPrimaryCapability; - private sealed record PrimaryB(string Name) : IPrimaryCapability; - private sealed record Regular(string Name) : ICapability; + private sealed record PrimaryA(string Name) : IPrimaryCapability; + private sealed record PrimaryB(string Name) : IPrimaryCapability; + private sealed record Regular(string Name) ; [Fact] public void Add_DuplicatePrimary_Throws() @@ -21,8 +21,8 @@ public void AddAs_PrimaryContract_Duplicate_Throws() { using var scope = new CapabilityScope(); var composer = scope.For(new Subject(2)); - composer.AddAs>(new PrimaryA("P1")); - Assert.Throws(() => composer.AddAs>(new PrimaryB("P2"))); + composer.AddAs(new PrimaryA("P1")); + Assert.Throws(() => composer.AddAs(new PrimaryB("P2"))); } [Fact] @@ -30,7 +30,7 @@ public void AddTuple_WithPrimaryThenDuplicatePrimary_Throws() { using var scope = new CapabilityScope(); var composer = scope.For(new Subject(3)); - composer.AddAs<(IPrimaryCapability, PrimaryA)>(new PrimaryA("P1")); - Assert.Throws(() => composer.AddAs<(IPrimaryCapability, PrimaryB)>(new PrimaryB("P2"))); + composer.AddAs<(IPrimaryCapability, PrimaryA)>(new PrimaryA("P1")); + Assert.Throws(() => composer.AddAs<(IPrimaryCapability, PrimaryB)>(new PrimaryB("P2"))); } } diff --git a/src/Cocoar.Capabilities.Tests/ConfigTests/ConcreteConfigBuilder.cs b/src/Cocoar.Capabilities.Tests/ConfigTests/ConcreteConfigBuilder.cs new file mode 100644 index 0000000..96dafae --- /dev/null +++ b/src/Cocoar.Capabilities.Tests/ConfigTests/ConcreteConfigBuilder.cs @@ -0,0 +1,36 @@ +namespace Cocoar.Capabilities.Tests.ConfigTests; + + +public sealed class ConcreteConfigBuilder : ConfigBuilder where T : class +{ + public Guid Id { get; } = Guid.NewGuid(); + internal ConcreteConfigBuilder(CapabilityScope capabilityScope): base(capabilityScope) + { + capabilityScope.For(this).WithPrimary( + new ConcreteTypePrimary(typeof(T))); + } + + + public ConcreteConfigBuilder ExposeAs() where TInterface : class + { + var interfaceType = typeof(TInterface); + if (!interfaceType.IsInterface) + { + throw new InvalidOperationException($"{interfaceType.Name} must be an interface."); + } + if (!interfaceType.IsAssignableFrom(typeof(T))) + { + throw new InvalidOperationException($"Type {typeof(T).Name} does not implement interface {interfaceType.Name}"); + } + + GetComposer(this).Add(new ExposeAsCapability(interfaceType)); + + return this; + } + + internal override ConfigBuilder Build() + { + GetComposer(this).Build(); + return this; + } +} diff --git a/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigurationTests.cs b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigurationTests.cs new file mode 100644 index 0000000..6a4f711 --- /dev/null +++ b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigurationTests.cs @@ -0,0 +1,44 @@ +using Xunit; + + +namespace Cocoar.Capabilities.Tests.ConfigTests; + + +public interface IAppSettings +{ + string ApplicationName { get; } + string Version { get; } + bool IsProduction { get; } +} + +public class AppSettings : IAppSettings +{ + public string ApplicationName { get; set; } = ""; + public string Version { get; set; } = "1.0.0"; + public bool IsProduction { get; set; } +} + +public class ConfigManager +{ + public readonly List _configurations; + private readonly CapabilityScope _capabilityScope = new(); + + public ConfigManager(Func configure) + { + _configurations = configure?.Invoke(new ConfigureBuilder(_capabilityScope)).Select(s => s.Build()).ToList() ?? new List(); + } +} + + +public class ConfigurationTests +{ + [Fact] + public void BasicConfigurationTest() + { + + var configManager = new ConfigManager(configure => + [ + configure.ConcreteType().ExposeAs(), + ]); + } +} diff --git a/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureBuilder.cs b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureBuilder.cs new file mode 100644 index 0000000..289fe11 --- /dev/null +++ b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureBuilder.cs @@ -0,0 +1,29 @@ +namespace Cocoar.Capabilities.Tests.ConfigTests; + +public interface IConfigureBuilder +{ + public static abstract CapabilityScope GetCapabilityScopeFor(ConfigBuilder builder); + +} + +public abstract class ConfigBuilder(CapabilityScope capabilityScope): IConfigureBuilder +{ + protected readonly CapabilityScope CapabilityScope = capabilityScope; + internal abstract ConfigBuilder Build(); + + public static CapabilityScope GetCapabilityScopeFor(ConfigBuilder builder) => builder.CapabilityScope; + + public static Composer GetComposer(ConfigBuilder builder) => + GetCapabilityScopeFor(builder).Composers.FindRequired(builder); +} + +public sealed class ConfigureBuilder(CapabilityScope capabilityScope) +{ + private readonly CapabilityScope _capabilityScope = capabilityScope; + + public Guid Id { get; } = Guid.NewGuid(); + + public ConcreteConfigBuilder ConcreteType() where T : class => new(_capabilityScope); + + public static CapabilityScope GetCapabilityScopeFor(ConfigureBuilder builder) => builder._capabilityScope; +} diff --git a/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureSpec.cs b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureSpec.cs new file mode 100644 index 0000000..eae59e1 --- /dev/null +++ b/src/Cocoar.Capabilities.Tests/ConfigTests/ConfigureSpec.cs @@ -0,0 +1,21 @@ +namespace Cocoar.Capabilities.Tests.ConfigTests; + +public interface IPrimaryTypeCapability : IPrimaryCapability +{ + public Type SelectedType { get; } +} + +public sealed record ConfigureSpec(); + + +public interface IConfigSpec {} +public interface IConfigSpec : IConfigSpec {} + +public record ConfigSpec : IConfigSpec {} + +public sealed record ConcreteTypePrimary(Type Concrete) : IPrimaryTypeCapability +{ + public Type SelectedType => Concrete; +} + +public sealed record ExposeAsCapability(Type ContractType) ; diff --git a/src/Cocoar.Capabilities.Tests/CustomStringMapperTests.cs b/src/Cocoar.Capabilities.Tests/CustomStringMapperTests.cs index 69ad396..cf37fdd 100644 --- a/src/Cocoar.Capabilities.Tests/CustomStringMapperTests.cs +++ b/src/Cocoar.Capabilities.Tests/CustomStringMapperTests.cs @@ -50,5 +50,5 @@ public void CustomStringMapper_SecondRegistrationIgnored() Assert.NotNull(found); Assert.Same(composer, found); } - private sealed class SimpleCapability : ICapability { } + private sealed class SimpleCapability { } } diff --git a/src/Cocoar.Capabilities.Tests/FuncOrderingTests.cs b/src/Cocoar.Capabilities.Tests/FuncOrderingTests.cs new file mode 100644 index 0000000..ac2054a --- /dev/null +++ b/src/Cocoar.Capabilities.Tests/FuncOrderingTests.cs @@ -0,0 +1,126 @@ +using Xunit; + +namespace Cocoar.Capabilities.Tests; + +public class FuncOrderingTests +{ + private class HasOrder + { + public int Order { get; set; } + } + + private class Subject { } + + [Fact] + public void Add_WithFuncOrderSelector_UsesComputedOrder() + { + // Arrange + var scope = new CapabilityScope(); + var subject = new Subject(); + var cap1 = new HasOrder { Order = 100 }; + var cap2 = new HasOrder { Order = 50 }; + var cap3 = new HasOrder { Order = 200 }; + + // Act + var composition = scope.For(subject) + .Add(cap1, c => ((HasOrder)c).Order) + .Add(cap2, c => ((HasOrder)c).Order) + .Add(cap3, c => ((HasOrder)c).Order) + .Build(); + + // Assert + var all = composition.GetAll(); + Assert.Collection(all, + c => Assert.Equal(50, c.Order), + c => Assert.Equal(100, c.Order), + c => Assert.Equal(200, c.Order)); + } + + [Fact] + public void AddAs_WithFuncOrderSelector_UsesComputedOrder() + { + // Arrange + var scope = new CapabilityScope(); + var subject = new Subject(); + var cap1 = new HasOrder { Order = 30 }; + var cap2 = new HasOrder { Order = 10 }; + var cap3 = new HasOrder { Order = 20 }; + + // Act + var composition = scope.For(subject) + .AddAs(cap1, c => ((HasOrder)c).Order) + .AddAs(cap2, c => ((HasOrder)c).Order) + .AddAs(cap3, c => ((HasOrder)c).Order) + .Build(); + + // Assert + var all = composition.GetAll(); + Assert.Collection(all, + c => Assert.Equal(10, ((HasOrder)c).Order), + c => Assert.Equal(20, ((HasOrder)c).Order), + c => Assert.Equal(30, ((HasOrder)c).Order)); + } + + [Fact] + public void TryAdd_WithFuncOrderSelector_UsesComputedOrder() + { + // Arrange + var scope = new CapabilityScope(); + var subject = new Subject(); + var cap1 = new HasOrder { Order = 5 }; + + // Act - Only first TryAdd succeeds since type already exists + var composition = scope.For(subject) + .TryAdd(cap1, c => ((HasOrder)c).Order) + .Build(); + + // Assert - Only cap1 was added + var all = composition.GetAll(); + Assert.Collection(all, + c => Assert.Equal(5, c.Order)); + } + + [Fact] + public void TryAddAs_WithFuncOrderSelector_UsesComputedOrder() + { + // Arrange + var scope = new CapabilityScope(); + var subject = new Subject(); + var cap1 = new HasOrder { Order = 40 }; + + // Act - Only first TryAddAs succeeds since type already exists + var composition = scope.For(subject) + .TryAddAs(cap1, c => ((HasOrder)c).Order) + .Build(); + + // Assert - Only cap1 was added + var all = composition.GetAll(); + Assert.Collection(all, + c => Assert.Equal(40, ((HasOrder)c).Order)); + } + + [Fact] + public void FuncOrderSelector_CanUseCustomLogic() + { + // Arrange + var scope = new CapabilityScope(); + var subject = new Subject(); + var cap1 = new HasOrder { Order = 1 }; + var cap2 = new HasOrder { Order = 2 }; + var cap3 = new HasOrder { Order = 3 }; + + // Act - reverse the order + var composition = scope.For(subject) + .Add(cap1, c => -((HasOrder)c).Order) + .Add(cap2, c => -((HasOrder)c).Order) + .Add(cap3, c => -((HasOrder)c).Order) + .Build(); + + // Assert - should be reversed + var all = composition.GetAll(); + Assert.Collection(all, + c => Assert.Equal(3, c.Order), + c => Assert.Equal(2, c.Order), + c => Assert.Equal(1, c.Order)); + } +} diff --git a/src/Cocoar.Capabilities.Tests/NegativeInvariantTests.cs b/src/Cocoar.Capabilities.Tests/NegativeInvariantTests.cs index 06f69a1..10c7e1d 100644 --- a/src/Cocoar.Capabilities.Tests/NegativeInvariantTests.cs +++ b/src/Cocoar.Capabilities.Tests/NegativeInvariantTests.cs @@ -48,7 +48,7 @@ public void Build_AfterScopeDisposed_FromNewComposer_Throws() public void For_NullSubject_Throws() { using var scope = NewScope(); - Assert.Throws(() => scope.For(null!)); + Assert.Throws(() => scope.For(null!)); } [Fact] diff --git a/src/Cocoar.Capabilities.Tests/OrderingTests.cs b/src/Cocoar.Capabilities.Tests/OrderingTests.cs index 0442e30..2cfe0a2 100644 --- a/src/Cocoar.Capabilities.Tests/OrderingTests.cs +++ b/src/Cocoar.Capabilities.Tests/OrderingTests.cs @@ -6,12 +6,12 @@ public class OrderingTests { private sealed record Subject(int Id); - private sealed record OrderedCap(int Id, int Priority) : ICapability, IOrderedCapability + private sealed record OrderedCap(int Id, int Priority) { public int Order => Priority; } - private sealed record PlainCap(int Id) : ICapability; + private sealed record PlainCap(int Id); private static readonly int[] ExpectedSorted = {10,20,30,40,50}; private static readonly int[] ExpectedStability = {1,2,3,4,5}; @@ -25,11 +25,11 @@ public void OrderedCapabilities_AreSortedAscending() var composer = scope.For(subj); // Intentionally add in unsorted order - composer.Add(new OrderedCap(1, 50)); - composer.Add(new OrderedCap(2, 10)); - composer.Add(new OrderedCap(3, 30)); - composer.Add(new OrderedCap(4, 40)); - composer.Add(new OrderedCap(5, 20)); + composer.Add(new OrderedCap(1, 50), order: 50); + composer.Add(new OrderedCap(2, 10), order: 10); + composer.Add(new OrderedCap(3, 30), order: 30); + composer.Add(new OrderedCap(4, 40), order: 40); + composer.Add(new OrderedCap(5, 20), order: 20); var comp = composer.Build(); var ordered = comp.GetAll(); @@ -46,11 +46,11 @@ public void OrderedCapabilities_StableSort_PreservesInsertionForSamePriority() var composer = scope.For(subj); // All same priority => resulting order must match insertion order - composer.Add(new OrderedCap(1, 5)); - composer.Add(new OrderedCap(2, 5)); - composer.Add(new OrderedCap(3, 5)); - composer.Add(new OrderedCap(4, 5)); - composer.Add(new OrderedCap(5, 5)); + composer.Add(new OrderedCap(1, 5), order: 5); + composer.Add(new OrderedCap(2, 5), order: 5); + composer.Add(new OrderedCap(3, 5), order: 5); + composer.Add(new OrderedCap(4, 5), order: 5); + composer.Add(new OrderedCap(5, 5), order: 5); var comp = composer.Build(); var ordered = comp.GetAll(); @@ -86,12 +86,12 @@ public void GlobalGetAll_MixedOrderedAndPlain_StableOrderingApplied() var composer = scope.For(subj); // Plain (order defaults to 0) - composer.Add(new PlainCap(1)); // P1 + composer.Add(new PlainCap(1)); // P1 // Ordered with higher priority numbers placed after plain (0) when positive - composer.Add(new OrderedCap(101, 10)); // O1 (10) - composer.Add(new PlainCap(2)); // P2 - composer.Add(new OrderedCap(102, 5)); // O2 (5) - composer.Add(new PlainCap(3)); // P3 + composer.Add(new OrderedCap(101, 10), order: 10); // O1 (10) + composer.Add(new PlainCap(2)); // P2 + composer.Add(new OrderedCap(102, 5), order: 5); // O2 (5) + composer.Add(new PlainCap(3)); // P3 var comp = composer.Build(); var all = comp.GetAll(); // triggers global ordering path diff --git a/src/Cocoar.Capabilities.Tests/PrimaryCapabilityTests.cs b/src/Cocoar.Capabilities.Tests/PrimaryCapabilityTests.cs index b34954b..8989f76 100644 --- a/src/Cocoar.Capabilities.Tests/PrimaryCapabilityTests.cs +++ b/src/Cocoar.Capabilities.Tests/PrimaryCapabilityTests.cs @@ -179,13 +179,13 @@ public void AddAs_PrimaryDuplicate_Throws() using var scope = NewScope(); var subject = new StringSubject("p15"); var builder = scope.For(subject) - .AddAs>(new PrimaryTestCapability("First")); - var ex = Assert.Throws(() => builder.AddAs>(new AlternatePrimaryCapability("Second"))); + .AddAs(new PrimaryTestCapability("First")); + var ex = Assert.Throws(() => builder.AddAs(new AlternatePrimaryCapability("Second"))); Assert.Contains("primary capability is already set", ex.Message, StringComparison.OrdinalIgnoreCase); } - private record TuplePrimaryCapability(string Name) : IPrimaryCapability, ITestContract; - private record SecondTuplePrimary(string Name) : IPrimaryCapability, ITestContract; + private record TuplePrimaryCapability(string Name) : IPrimaryCapability, ITestContract; + private record SecondTuplePrimary(string Name) : IPrimaryCapability, ITestContract; [Fact] public void AddAs_TupleContainingPrimary_WhenAlreadyPresent_Throws() @@ -194,7 +194,7 @@ public void AddAs_TupleContainingPrimary_WhenAlreadyPresent_Throws() var subject = new StringSubject("p16"); var builder = scope.For(subject) .Add(new PrimaryTestCapability("First")); - var ex = Assert.Throws(() => builder.AddAs<(IPrimaryCapability, ITestContract)>(new TuplePrimaryCapability("Second"))); + var ex = Assert.Throws(() => builder.AddAs<(IPrimaryCapability, ITestContract)>(new TuplePrimaryCapability("Second"))); Assert.Contains("primary capability is already set", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -204,7 +204,7 @@ public void AddAs_TupleWithTwoPrimaryContracts_Throws() using var scope = NewScope(); var subject = new StringSubject("p17"); var builder = scope.For(subject); - var ex = Assert.Throws(() => builder.AddAs<(IPrimaryCapability, IPrimaryCapability)>(new TuplePrimaryCapability("Both"))); + var ex = Assert.Throws(() => builder.AddAs<(IPrimaryCapability, IPrimaryCapability)>(new TuplePrimaryCapability("Both"))); Assert.Contains("Multiple primary capability contracts", ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -230,10 +230,10 @@ public void TryAddAs_DoesNotReplaceExistingPrimary() using var scope = NewScope(); var subject = new StringSubject("p19"); var builder = scope.For(subject) - .AddAs>(new PrimaryTestCapability("Original")); + .AddAs(new PrimaryTestCapability("Original")); // Should no-op, not throw, not replace - builder.TryAddAs>(new AlternatePrimaryCapability("Ignored")); + builder.TryAddAs(new AlternatePrimaryCapability("Ignored")); var comp = builder.Build(); var primary = comp.GetRequiredPrimaryAs(); diff --git a/src/Cocoar.Capabilities.Tests/RegistryApiTests.cs b/src/Cocoar.Capabilities.Tests/RegistryApiTests.cs index 4cd5fcf..9b87743 100644 --- a/src/Cocoar.Capabilities.Tests/RegistryApiTests.cs +++ b/src/Cocoar.Capabilities.Tests/RegistryApiTests.cs @@ -3,7 +3,7 @@ namespace Cocoar.Capabilities.Tests; public class RegistryApiTests { private sealed record Subject(int Id); - private sealed record Cap(string Name) : ICapability; + private sealed record Cap(string Name) ; [Fact] public void CompositionRegistry_FindRequired_ThrowsWhenMissing() diff --git a/src/Cocoar.Capabilities.Tests/SingleTestApproach.cs b/src/Cocoar.Capabilities.Tests/SingleTestApproach.cs index e4fc464..8ed5dcb 100644 --- a/src/Cocoar.Capabilities.Tests/SingleTestApproach.cs +++ b/src/Cocoar.Capabilities.Tests/SingleTestApproach.cs @@ -43,4 +43,4 @@ public void Test01_DisabledScope_EnableCompositionRegistry_ShouldWork() Assert.True(isSameInstance, "Found composition should be same instance"); } } -} \ No newline at end of file +} diff --git a/src/Cocoar.Capabilities.Tests/StringSubjectValueSemanticsTests.cs b/src/Cocoar.Capabilities.Tests/StringSubjectValueSemanticsTests.cs index 6bc0e43..14a0f4d 100644 --- a/src/Cocoar.Capabilities.Tests/StringSubjectValueSemanticsTests.cs +++ b/src/Cocoar.Capabilities.Tests/StringSubjectValueSemanticsTests.cs @@ -4,7 +4,7 @@ namespace Cocoar.Capabilities.Tests; public class StringSubjectValueSemanticsTests { - private record StringCapability(string Label) : ICapability; + private record StringCapability(string Label) ; [Fact] public void DistinctEqualStringInstances_MapToSameComposition_WhenRegistered() { diff --git a/src/Cocoar.Capabilities.Tests/SubjectKeyCanonicalizerTests.cs b/src/Cocoar.Capabilities.Tests/SubjectKeyCanonicalizerTests.cs index 780b9fb..17ac101 100644 --- a/src/Cocoar.Capabilities.Tests/SubjectKeyCanonicalizerTests.cs +++ b/src/Cocoar.Capabilities.Tests/SubjectKeyCanonicalizerTests.cs @@ -4,7 +4,7 @@ namespace Cocoar.Capabilities.Tests; public class SubjectKeyCanonicalizerTests { - private sealed record Cap(string Name) : ICapability; + private sealed record Cap(string Name) ; private sealed class UppercaseStringMapper : ISubjectKeyMapper { diff --git a/src/Cocoar.Capabilities.Tests/TestHelpers.cs b/src/Cocoar.Capabilities.Tests/TestHelpers.cs index c6fe3d1..d81919d 100644 --- a/src/Cocoar.Capabilities.Tests/TestHelpers.cs +++ b/src/Cocoar.Capabilities.Tests/TestHelpers.cs @@ -17,44 +17,44 @@ public record struct IntSubject(int Value); public record struct GuidSubject(Guid Id); public record struct ComplexStruct(int Id, string Name, DateTime Created); -public record TestCapability(string Name) : ICapability; -public record DocumentCapability(string Type, string Content) : ICapability; -public record IntCapability(int Value) : ICapability; -public record GuidCapability(string Description) : ICapability; -public record StructCapability(string Data) : ICapability; +public record TestCapability(string Name); +public record DocumentCapability(string Type, string Content); +public record IntCapability(int Value); +public record GuidCapability(string Description); +public record StructCapability(string Data); -public record PrimaryTestCapability(string Name) : IPrimaryCapability; -public record DocumentPrimaryCapability(string Title) : IPrimaryCapability; -public record IntPrimaryCapability(string Description) : IPrimaryCapability; -public record AlternatePrimaryCapability(string Value) : IPrimaryCapability; +public record PrimaryTestCapability(string Name) : IPrimaryCapability; +public record DocumentPrimaryCapability(string Title) : IPrimaryCapability; +public record IntPrimaryCapability(string Description) : IPrimaryCapability; +public record AlternatePrimaryCapability(string Value) : IPrimaryCapability; -public record TestPrimaryCapability(string Id, string Description) : IPrimaryCapability; +public record TestPrimaryCapability(string Id, string Description) : IPrimaryCapability; -public record OrderedCapability(int Order, string Name) : ICapability, IOrderedCapability; -public record HighPriorityCapability(string Name) : ICapability, IOrderedCapability +public record OrderedCapability(int Order, string Name); +public record HighPriorityCapability(string Name) { public int Order => -100; } -public record LowPriorityCapability(string Name) : ICapability, IOrderedCapability +public record LowPriorityCapability(string Name) { public int Order => 100; } -public record OrderedTestCapability(string Name, int Order) : ICapability, IOrderedCapability; -public record OrderedPrimaryTestCapability(string Id, string Description, int Order) : IPrimaryCapability, IOrderedCapability; +public record OrderedTestCapability(string Name, int Order); +public record OrderedPrimaryTestCapability(string Id, string Description, int Order) : IPrimaryCapability; -public interface IValidationCapability : ICapability +public interface IValidationCapability { bool IsValid { get; } } -public interface ILoggingCapability : ICapability +public interface ILoggingCapability { void Log(string message); } -public interface ITestContract : ICapability { } -public interface IAlternateContract : ICapability { } +public interface ITestContract { } +public interface IAlternateContract { } public record ValidationCapability(bool IsValid, string Rule) : IValidationCapability; public record LoggingCapability(string LoggerName) : ILoggingCapability @@ -65,10 +65,10 @@ public record LoggingCapability(string LoggerName) : ILoggingCapability public record TestContractImplementation(string Name, string Description) : ITestContract; public record AlternateContractImplementation(string Name, int Value) : IAlternateContract; public record MultiContractImplementation(string Name, string Description, int Value) : ITestContract, IAlternateContract; -public record TestContractPrimaryCapability(string Id, string Description) : IPrimaryCapability, ITestContract; +public record TestContractPrimaryCapability(string Id, string Description) : IPrimaryCapability, ITestContract; -public record CompositeCapability(string Name, ICapability Inner) : ICapability; -public record ConditionalCapability(string Name, bool Condition) : ICapability; +public record CompositeCapability(string Name, object Inner); +public record ConditionalCapability(string Name, bool Condition); public static class TestOptions { @@ -95,4 +95,4 @@ public static class TestOptions UseComposerRegistry = true, UseCompositionRegistry = true }; -} \ No newline at end of file +} diff --git a/src/Cocoar.Capabilities.Tests/TupleTypeExtractorNegativeTests.cs b/src/Cocoar.Capabilities.Tests/TupleTypeExtractorNegativeTests.cs index ea37003..171259c 100644 --- a/src/Cocoar.Capabilities.Tests/TupleTypeExtractorNegativeTests.cs +++ b/src/Cocoar.Capabilities.Tests/TupleTypeExtractorNegativeTests.cs @@ -3,8 +3,8 @@ namespace Cocoar.Capabilities.Tests; public class TupleTypeExtractorNegativeTests { private sealed record Subject(int Id); - private sealed record PrimaryA(string Name) : IPrimaryCapability; - private sealed record PrimaryB(string Name) : IPrimaryCapability; + private sealed record PrimaryA(string Name) : IPrimaryCapability; + private sealed record PrimaryB(string Name) : IPrimaryCapability; [Fact] public void TupleWithTwoPrimaryContracts_Throws() @@ -14,6 +14,6 @@ public void TupleWithTwoPrimaryContracts_Throws() // Register first primary normally composer.Add(new PrimaryA("P1")); // Adding tuple containing another primary should throw (duplicate primary detection) - Assert.Throws(() => composer.AddAs<(IPrimaryCapability, PrimaryB)>(new PrimaryB("P2"))); + Assert.Throws(() => composer.AddAs<(IPrimaryCapability, PrimaryB)>(new PrimaryB("P2"))); } } diff --git a/src/Cocoar.Capabilities.Tests/ValueTypeRegistryTests.cs b/src/Cocoar.Capabilities.Tests/ValueTypeRegistryTests.cs index ed88447..bd1ff1d 100644 --- a/src/Cocoar.Capabilities.Tests/ValueTypeRegistryTests.cs +++ b/src/Cocoar.Capabilities.Tests/ValueTypeRegistryTests.cs @@ -10,7 +10,7 @@ public class ValueTypeRegistryTests UseCompositionRegistry = true }); - private record IntTestCapability(string Name) : ICapability; + private record IntTestCapability(string Name) ; [Fact] public void ValueType_Compositions_AreRetrievable() diff --git a/src/Cocoar.Capabilities/CapabilityArrayBuilder.cs b/src/Cocoar.Capabilities/CapabilityArrayBuilder.cs index 28ac102..cddd87f 100644 --- a/src/Cocoar.Capabilities/CapabilityArrayBuilder.cs +++ b/src/Cocoar.Capabilities/CapabilityArrayBuilder.cs @@ -2,10 +2,9 @@ namespace Cocoar.Capabilities; internal static class CapabilityArrayBuilder { - internal static (Dictionary result, int totalCount) Build( - Dictionary> capabilitiesById, + internal static (Dictionary result, int totalCount) Build( + Dictionary capabilitiesById, Dictionary> typeToIds) - where TSubject : notnull { var result = new Dictionary(typeToIds.Count); var totalCount = capabilitiesById.Count; @@ -13,7 +12,7 @@ internal static (Dictionary result, int totalCount) Build foreach (var typeKvp in typeToIds) { var ids = typeKvp.Value; - var capabilities = new List>(ids.Count); + var capabilities = new List<(object capability, int? order)>(ids.Count); for (int i = 0; i < ids.Count; i++) { capabilities.Add(capabilitiesById[ids[i]]); @@ -21,8 +20,9 @@ internal static (Dictionary result, int totalCount) Build CapabilityOrdering.SortInPlace(capabilities); - var arr = Array.CreateInstance(typeof(ICapability), capabilities.Count); - for (int i = 0; i < capabilities.Count; i++) arr.SetValue(capabilities[i], i); + var arr = Array.CreateInstance(typeof(object), capabilities.Count); + for (int i = 0; i < capabilities.Count; i++) + arr.SetValue(capabilities[i].capability, i); result[typeKvp.Key] = arr; } diff --git a/src/Cocoar.Capabilities/CapabilityEntry.cs b/src/Cocoar.Capabilities/CapabilityEntry.cs index 7a7ecbb..c2b06d9 100644 --- a/src/Cocoar.Capabilities/CapabilityEntry.cs +++ b/src/Cocoar.Capabilities/CapabilityEntry.cs @@ -20,9 +20,9 @@ public static CapabilityEntry FromComposition(object composition) => public static CapabilityEntry FromBoth(object composer, object composition) => new(composer ?? throw new ArgumentNullException(nameof(composer)), composition ?? throw new ArgumentNullException(nameof(composition))); - public bool TryGetComposer(out Composer composer) where TSubject : notnull + public bool TryGetComposer(out Composer composer) { - if (_composer is Composer typed) + if (_composer is Composer typed) { composer = typed; return true; @@ -31,20 +31,9 @@ public bool TryGetComposer(out Composer composer) where TSub return false; } - public bool TryGetComposer(out object composer) + public bool TryGetComposition(out IComposition composition) { - if (_composer is not null) - { - composer = _composer; - return true; - } - composer = default!; - return false; - } - - public bool TryGetComposition(out IComposition composition) where TSubject : notnull - { - if (_composition is IComposition typed) + if (_composition is IComposition typed) { composition = typed; return true; @@ -53,17 +42,6 @@ public bool TryGetComposition(out IComposition composition) return false; } - public bool TryGetComposition(out object composition) - { - if (_composition is not null) - { - composition = _composition; - return true; - } - composition = default!; - return false; - } - // Dispose any disposable objects we directly reference (composer, composition). // Idempotent: if neither implements IDisposable, it's a no-op. internal void DisposeOwnedResources() diff --git a/src/Cocoar.Capabilities/CapabilityOrdering.cs b/src/Cocoar.Capabilities/CapabilityOrdering.cs index 9c48a07..7c47432 100644 --- a/src/Cocoar.Capabilities/CapabilityOrdering.cs +++ b/src/Cocoar.Capabilities/CapabilityOrdering.cs @@ -2,31 +2,31 @@ namespace Cocoar.Capabilities; internal static class CapabilityOrdering { - internal static void SortInPlace(List> capabilities) - where TSubject : notnull + internal static void SortInPlace(List<(object capability, int? order)> capabilities) { if (capabilities.Count <= 1) return; - var hasOrdered = false; + var hasOrdering = false; for (int i = 0; i < capabilities.Count; i++) { - if (capabilities[i] is IOrderedCapability) + if (capabilities[i].order.HasValue) { - hasOrdered = true; + hasOrdering = true; break; } } - if (!hasOrdered) return; + if (!hasOrdering) return; - var originals = new Dictionary, int>(capabilities.Count); - for (int i = 0; i < capabilities.Count; i++) originals[capabilities[i]] = i; + var originals = new Dictionary(capabilities.Count); + for (int i = 0; i < capabilities.Count; i++) + originals[capabilities[i].capability] = i; capabilities.Sort((a, b) => { - var oa = (a as IOrderedCapability)?.Order ?? 0; - var ob = (b as IOrderedCapability)?.Order ?? 0; + var oa = a.order ?? 0; + var ob = b.order ?? 0; if (oa != ob) return oa.CompareTo(ob); - return originals[a].CompareTo(originals[b]); + return originals[a.capability].CompareTo(originals[b.capability]); }); } } diff --git a/src/Cocoar.Capabilities/CapabilityScope.cs b/src/Cocoar.Capabilities/CapabilityScope.cs index b1076e7..03a0eb3 100644 --- a/src/Cocoar.Capabilities/CapabilityScope.cs +++ b/src/Cocoar.Capabilities/CapabilityScope.cs @@ -19,17 +19,17 @@ public CapabilityScope(CapabilityScopeOptions? options = null) internal bool IsDisposed => _disposed; public ComposerRegistryApi Composers => _composers; public CompositionRegistryApi Compositions => _compositions; - public Composer For(TSubject subject, bool? useRegistry = null) where TSubject : notnull + public Composer For(object subject, bool? useRegistry = null) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(subject); - return new Composer(subject, _options, _sharedRegistry, useRegistry); + return new Composer(subject, _options, _sharedRegistry, useRegistry); } - public Composer Recompose(IComposition composition, bool? useRegistry = null) where TSubject : notnull + public Composer Recompose(IComposition composition, bool? useRegistry = null) { ObjectDisposedException.ThrowIf(_disposed, this); ArgumentNullException.ThrowIfNull(composition); - return new Composer(composition, _options, _sharedRegistry, useRegistry); + return new Composer(composition, _options, _sharedRegistry, useRegistry); } public void Dispose() diff --git a/src/Cocoar.Capabilities/CapabilityStore.cs b/src/Cocoar.Capabilities/CapabilityStore.cs index 75ffa91..c245e4b 100644 --- a/src/Cocoar.Capabilities/CapabilityStore.cs +++ b/src/Cocoar.Capabilities/CapabilityStore.cs @@ -1,22 +1,26 @@ namespace Cocoar.Capabilities; -internal sealed class CapabilityStore where TSubject : notnull +/// +/// Internal storage for capabilities attached to an object instance. +/// This is instance-based, not type-based. +/// +internal sealed class CapabilityStore { private int _nextCapabilityId; - private readonly Dictionary> _capabilitiesById = new(64); + private readonly Dictionary _capabilitiesById = new(64); private readonly Dictionary> _typeToIds = new(16); - internal static readonly Type PrimaryMarkerType = typeof(IPrimaryCapability); + internal static readonly Type PrimaryMarkerType = typeof(IPrimaryCapability); internal bool HasPrimary() => _typeToIds.TryGetValue(PrimaryMarkerType, out var list) && list.Count > 0; - internal bool Has() where TCapability : class, ICapability + internal bool Has() where TCapability : class => _typeToIds.TryGetValue(typeof(TCapability), out var list) && list.Count > 0; - internal void Add(ICapability capability, Type singleType, bool isPrimary) + internal void Add(object capability, Type singleType, bool isPrimary, int? order) { var id = _nextCapabilityId++; - _capabilitiesById[id] = capability; + _capabilitiesById[id] = (capability, order); RegisterIdUnderType(id, singleType); if (isPrimary && singleType != PrimaryMarkerType) { @@ -24,10 +28,10 @@ internal void Add(ICapability capability, Type singleType, bool isPrim } } - internal void Add(ICapability capability, IEnumerable types, bool includesPrimary) + internal void Add(object capability, IEnumerable types, bool includesPrimary, int? order) { var id = _nextCapabilityId++; - _capabilitiesById[id] = capability; + _capabilitiesById[id] = (capability, order); bool containsMarker = false; foreach (var t in types) { @@ -40,12 +44,12 @@ internal void Add(ICapability capability, IEnumerable types, boo } } - internal void RemoveWhere(Func, bool> predicate) + internal void RemoveWhere(Func predicate) { var idsToRemove = new List(); foreach (var kvp in _capabilitiesById) { - if (predicate(kvp.Value)) idsToRemove.Add(kvp.Key); + if (predicate(kvp.Value.capability)) idsToRemove.Add(kvp.Key); } foreach (var id in idsToRemove) @@ -59,22 +63,26 @@ internal void RemoveWhere(Func, bool> predicate) } } - internal void RemoveExistingPrimary() => RemoveWhere(c => c is IPrimaryCapability); + internal void RemoveExistingPrimary() => RemoveWhere(c => c is IPrimaryCapability); - internal void SeedFromComposition(IComposition existingComposition) + internal void SeedFromComposition(IComposition composition) { - if (existingComposition is not Composition internalComposition) + // Use reflection to access internal method since Composition is generic + var compositionType = composition.GetType(); + var getMethod = compositionType.GetMethod("GetCapabilitiesByType", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + + if (getMethod == null) { - throw new ArgumentException("Recompose only supports compositions created by this system", nameof(existingComposition)); + throw new ArgumentException("Recompose only supports compositions created by this system", nameof(composition)); } - var capabilitiesByType = internalComposition.GetCapabilitiesByType(); + var capabilitiesByType = (IReadOnlyDictionary)getMethod.Invoke(composition, null)!; foreach (var typeKvp in capabilitiesByType) { - foreach (ICapability capability in typeKvp.Value) + foreach (object capability in typeKvp.Value) { var id = _nextCapabilityId++; - _capabilitiesById[id] = capability; + _capabilitiesById[id] = (capability, null); // No order info when recomposing RegisterIdUnderType(id, typeKvp.Key); } } diff --git a/src/Cocoar.Capabilities/Composer.cs b/src/Cocoar.Capabilities/Composer.cs index a37bb27..d2e0748 100644 --- a/src/Cocoar.Capabilities/Composer.cs +++ b/src/Cocoar.Capabilities/Composer.cs @@ -1,15 +1,15 @@ namespace Cocoar.Capabilities; -public sealed class Composer where TSubject : notnull +public sealed class Composer { - private readonly TSubject _subject; + private readonly object _subject; private readonly CapabilityScopeOptions _options; private readonly DefaultCapabilityRegistry _registry; private readonly bool _useComposerRegistry; - private readonly CapabilityStore _store = new(); + private readonly CapabilityStore _store = new(); private bool _built; - internal Composer(TSubject subject, CapabilityScopeOptions options, DefaultCapabilityRegistry registry, bool? useRegistry = null) + internal Composer(object subject, CapabilityScopeOptions options, DefaultCapabilityRegistry registry, bool? useRegistry = null) { ArgumentNullException.ThrowIfNull(subject); _subject = subject; @@ -22,7 +22,7 @@ internal Composer(TSubject subject, CapabilityScopeOptions options, DefaultCapab } } - internal Composer(IComposition existingComposition, CapabilityScopeOptions options, DefaultCapabilityRegistry registry, bool? useRegistry = null) + internal Composer(IComposition existingComposition, CapabilityScopeOptions options, DefaultCapabilityRegistry registry, bool? useRegistry = null) { ArgumentNullException.ThrowIfNull(existingComposition); _subject = existingComposition.Subject; @@ -36,54 +36,77 @@ internal Composer(IComposition existingComposition, CapabilityScopeOpt } } - public TSubject Subject => _subject; + public object Subject => _subject; - public Composer Add(ICapability capability) + public Composer Add(object capability, int? order = null) { EnsureNotBuilt(); ArgumentNullException.ThrowIfNull(capability); - if (capability is IPrimaryCapability && HasPrimary()) + if (capability is IPrimaryCapability && HasPrimary()) { throw new InvalidOperationException( - $"A primary capability is already set for '{typeof(TSubject).Name}'. Use WithPrimary(...) to replace it."); + $"A primary capability is already set. Use WithPrimary(...) to replace it."); } - _store.Add(capability, capability.GetType(), capability is IPrimaryCapability); + _store.Add(capability, capability.GetType(), capability is IPrimaryCapability, order); return this; } + + public Composer Add(object capability, Func orderSelector) + { + ArgumentNullException.ThrowIfNull(orderSelector); + var order = orderSelector(capability); + return Add(capability, order); + } - public Composer AddAs(ICapability capability) + public Composer AddAs(object capability, int? order = null) { EnsureNotBuilt(); ArgumentNullException.ThrowIfNull(capability); var contractType = typeof(TContract); - return IsTupleType(contractType) ? AddAsMultipleContracts(capability) : AddAsSingleContract(capability); + return IsTupleType(contractType) ? AddAsMultipleContracts(capability, order) : AddAsSingleContract(capability, order); + } + + public Composer AddAs(object capability, Func orderSelector) + { + ArgumentNullException.ThrowIfNull(orderSelector); + var order = orderSelector(capability); + return AddAs(capability, order); } - public Composer TryAdd(TCapability capability) where TCapability : class, ICapability + public Composer TryAdd(TCapability capability, int? order = null) where TCapability : class { ArgumentNullException.ThrowIfNull(capability); - if (capability is IPrimaryCapability && HasPrimary()) + if (capability is IPrimaryCapability && HasPrimary()) { return this; } if (!Has()) { - return Add(capability); + return Add(capability, order); } return this; } - public Composer TryAddAs(ICapability capability) where TContract : class, ICapability + public Composer TryAdd(TCapability capability, Func orderSelector) where TCapability : class + { + ArgumentNullException.ThrowIfNull(capability); + ArgumentNullException.ThrowIfNull(orderSelector); + + var order = orderSelector(capability); + return TryAdd(capability, order); + } + + public Composer TryAddAs(object capability, int? order = null) where TContract : class { ArgumentNullException.ThrowIfNull(capability); var contractType = typeof(TContract); - var isPrimaryContract = contractType.IsGenericType && contractType.GetGenericTypeDefinition() == typeof(IPrimaryCapability<>); + var isPrimaryContract = typeof(IPrimaryCapability).IsAssignableFrom(contractType); if (isPrimaryContract && HasPrimary()) { return this; @@ -92,10 +115,10 @@ public Composer TryAddAs(ICapability capability) if (IsTupleType(contractType)) { var tupleTypes = TupleTypeExtractor.GetTupleTypes(); - for (int i = 0; i < tupleTypes.Length; i++) + for (int i = 0; i < tupleTypes.Length; i++) { var ct = tupleTypes[i]; - if (ct.IsGenericType && ct.GetGenericTypeDefinition() == typeof(IPrimaryCapability<>)) + if (typeof(IPrimaryCapability).IsAssignableFrom(ct)) { if (HasPrimary()) { @@ -108,12 +131,21 @@ public Composer TryAddAs(ICapability capability) if (!Has()) { - return AddAs(capability); + return AddAs(capability, order); } return this; } - public Composer RemoveWhere(Func, bool> predicate) + public Composer TryAddAs(object capability, Func orderSelector) where TContract : class + { + ArgumentNullException.ThrowIfNull(capability); + ArgumentNullException.ThrowIfNull(orderSelector); + + var order = orderSelector(capability); + return TryAddAs(capability, order); + } + + public Composer RemoveWhere(Func predicate) { EnsureNotBuilt(); ArgumentNullException.ThrowIfNull(predicate); @@ -122,7 +154,7 @@ public Composer RemoveWhere(Func, bool> predicat return this; } - public Composer WithPrimary(IPrimaryCapability? primary) + public Composer WithPrimary(IPrimaryCapability? primary) { EnsureNotBuilt(); @@ -133,7 +165,7 @@ public Composer WithPrimary(IPrimaryCapability? primary) if (primary != null) { - _store.Add(primary, primary.GetType(), isPrimary: true); + _store.Add(primary, primary.GetType(), isPrimary: true, order: null); } return this; @@ -144,18 +176,18 @@ public bool HasPrimary() return _store.HasPrimary(); } - public bool Has() where TCapability : class, ICapability + public bool Has() where TCapability : class { return _store.Has(); } - public IComposition Build(bool? useRegistry = null) + public IComposition Build(bool? useRegistry = null) { if (_built) throw new InvalidOperationException("Build() can only be called once. This builder is no longer usable."); _built = true; var bag = BuildCompositionSnapshot(); - var shouldUseCompositionRegistry = useRegistry ?? _options.UseCompositionRegistry; + var shouldUseCompositionRegistry = useRegistry ?? _options.UseCompositionRegistry; if (!_useComposerRegistry && !shouldUseCompositionRegistry) { @@ -180,72 +212,88 @@ public IComposition Build(bool? useRegistry = null) return bag; } - private IComposition RecomposeExisting(IComposition existingComposition) + private IComposition RecomposeExisting(IComposition existingComposition) { - if (existingComposition is not Composition internalComposition) + if (existingComposition is not Composition internalComposition) { throw new ArgumentException("Recompose only supports compositions created by this system", nameof(existingComposition)); } var (result, totalCount) = _store.BuildCapabilityArrays(); - if (result.TryGetValue(CapabilityStore.PrimaryMarkerType, out var primaryArr) && primaryArr.Length > 1) + if (result.TryGetValue(CapabilityStore.PrimaryMarkerType, out var primaryArr) && primaryArr.Length > 1) { throw new InvalidOperationException( - $"Multiple primary capabilities registered for '{typeof(TSubject).Name}'. Only one primary capability is allowed."); + $"Multiple primary capabilities registered. Only one primary capability is allowed."); } - internalComposition.UpdateCapabilities(result, totalCount); + internalComposition.UpdateCapabilities(result, totalCount); - return existingComposition; + return existingComposition; } - private Composer AddAsSingleContract(ICapability capability) + private Composer AddAsSingleContract(object capability, int? order) { var contractType = typeof(TContract); - if (!typeof(ICapability).IsAssignableFrom(contractType)) - { - throw new ArgumentException($"Type '{contractType.Name}' must implement ICapability<{typeof(TSubject).Name}> to be registered as a capability contract."); - } - var isPrimaryContract = contractType.IsGenericType && contractType.GetGenericTypeDefinition() == typeof(IPrimaryCapability<>); + var isPrimaryContract = typeof(IPrimaryCapability).IsAssignableFrom(contractType); if (isPrimaryContract && HasPrimary()) { throw new InvalidOperationException( - $"A primary capability is already set for '{typeof(TSubject).Name}'. Use WithPrimary(...) to replace it."); + $"A primary capability is already set. Use WithPrimary(...) to replace it."); } - _store.Add(capability, contractType, isPrimaryContract); + _store.Add(capability, contractType, isPrimaryContract, order); return this; } - private Composer AddAsMultipleContracts(ICapability capability) + private Composer AddAsMultipleContracts(object capability, int? order) { var contractTypes = TupleTypeExtractor.GetTupleTypes(); - TupleTypeExtractor.ValidateCapabilityTypes(contractTypes); - int primaryCountInTuple = 0; + TupleTypeExtractor.ValidateCapabilityTypes(contractTypes); + + // Check if any contract type is or implements IPrimaryCapability + // We need to deduplicate - if PrimaryA implements IPrimaryCapability, + // and the tuple contains (IPrimaryCapability, PrimaryA), we should only count it once + bool hasPrimaryContract = false; + int markerInterfaceCount = 0; + var distinctPrimaryTypes = new HashSet(); + foreach (var ct in contractTypes) { - if (ct.IsGenericType && ct.GetGenericTypeDefinition() == typeof(IPrimaryCapability<>)) + if (typeof(IPrimaryCapability).IsAssignableFrom(ct)) { - primaryCountInTuple++; + hasPrimaryContract = true; + // Count how many times the marker interface itself appears + if (ct == typeof(IPrimaryCapability)) + { + markerInterfaceCount++; + } + else + { + // It's a concrete type implementing IPrimaryCapability + distinctPrimaryTypes.Add(ct); + } } } - if (primaryCountInTuple > 1) + // Multiple errors to catch: + // 1. IPrimaryCapability marker appears more than once + // 2. Multiple distinct types that implement IPrimaryCapability + if (markerInterfaceCount > 1 || distinctPrimaryTypes.Count > 1) { throw new InvalidOperationException( - $"Multiple primary capability contracts specified in the same tuple for '{typeof(TSubject).Name}'. Only one primary capability is allowed."); + $"Multiple primary capability contracts specified in the same tuple. Only one primary capability is allowed."); } - if (primaryCountInTuple == 1 && HasPrimary()) + if (hasPrimaryContract && HasPrimary()) { throw new InvalidOperationException( - $"A primary capability is already set for '{typeof(TSubject).Name}'. Use WithPrimary(...) to replace it."); + $"A primary capability is already set. Use WithPrimary(...) to replace it."); } - _store.Add(capability, contractTypes, primaryCountInTuple == 1); + _store.Add(capability, contractTypes, hasPrimaryContract, order); return this; } @@ -258,14 +306,14 @@ private void EnsureNotBuilt() } - private Composition BuildCompositionSnapshot() + private Composition BuildCompositionSnapshot() { var (result, totalCount) = _store.BuildCapabilityArrays(); - if (result.TryGetValue(CapabilityStore.PrimaryMarkerType, out var primaryArr) && primaryArr.Length > 1) + if (result.TryGetValue(CapabilityStore.PrimaryMarkerType, out var primaryArr) && primaryArr.Length > 1) { throw new InvalidOperationException( - $"Multiple primary capabilities registered for '{typeof(TSubject).Name}'. Only one primary capability is allowed."); + $"Multiple primary capabilities registered. Only one primary capability is allowed."); } - return new Composition(_subject, result, totalCount); + return new Composition(_subject, result, totalCount); } } diff --git a/src/Cocoar.Capabilities/ComposerRegistryApi.cs b/src/Cocoar.Capabilities/ComposerRegistryApi.cs index 7cd92b5..d507595 100644 --- a/src/Cocoar.Capabilities/ComposerRegistryApi.cs +++ b/src/Cocoar.Capabilities/ComposerRegistryApi.cs @@ -12,17 +12,17 @@ public ComposerRegistryApi(CapabilityScopeOptions options, DefaultCapabilityRegi _registry = sharedRegistry; } - public bool TryFind(TSubject subject, out Composer? composer) where TSubject : notnull + public bool TryFind(TSubject subject, out Composer? composer) where TSubject : notnull { return _registry.TryGetComposer(subject, out composer); } - public Composer? FindOrDefault(TSubject subject) where TSubject : notnull + public Composer? FindOrDefault(TSubject subject) where TSubject : notnull { return TryFind(subject, out var composer) ? composer : null; } - public Composer FindRequired(TSubject subject) where TSubject : notnull + public Composer FindRequired(TSubject subject) where TSubject : notnull { if (TryFind(subject, out var composer) && composer != null) return composer; @@ -30,7 +30,7 @@ public Composer FindRequired(TSubject subject) where TSubjec throw new InvalidOperationException($"No composer found for subject of type '{typeof(TSubject).Name}'."); } - public void Register(TSubject subject, Composer composer, bool forceRegister = false) where TSubject : notnull + public void Register(TSubject subject, Composer composer, bool forceRegister = false) where TSubject : notnull { // Only register if scope allows it OR if explicitly forced (method override) if (options.UseComposerRegistry || forceRegister) diff --git a/src/Cocoar.Capabilities/Composition.cs b/src/Cocoar.Capabilities/Composition.cs index 48253cf..cda45f3 100644 --- a/src/Cocoar.Capabilities/Composition.cs +++ b/src/Cocoar.Capabilities/Composition.cs @@ -1,12 +1,12 @@ namespace Cocoar.Capabilities; -internal sealed class Composition : IComposition where TSubject : notnull +internal sealed class Composition : IComposition { private IReadOnlyDictionary _capabilitiesByType; private int _totalCapabilityCount; internal Composition( - TSubject subject, + object subject, IReadOnlyDictionary capabilitiesByType, int totalCapabilityCount) { @@ -15,26 +15,24 @@ internal Composition( _totalCapabilityCount = totalCapabilityCount; } - public TSubject Subject { get; } - - object IComposition.Subject => Subject!; + public object Subject { get; } public int TotalCapabilityCount => _totalCapabilityCount; public bool HasPrimary() { - return Has>(); + return Has(); } public bool HasPrimary() - where TPrimaryCapability : class, IPrimaryCapability + where TPrimaryCapability : class, IPrimaryCapability { return Has(); } - public bool TryGetPrimary(out IPrimaryCapability primary) + public bool TryGetPrimary(out IPrimaryCapability primary) { - var primaryCapabilities = GetAll>(); + var primaryCapabilities = GetAll(); if (primaryCapabilities.Count > 0) { primary = primaryCapabilities[0]; @@ -44,13 +42,13 @@ public bool TryGetPrimary(out IPrimaryCapability primary) return false; } - public IPrimaryCapability? GetPrimaryOrDefault() + public IPrimaryCapability? GetPrimaryOrDefault() { TryGetPrimary(out var primary); return primary; } - public IPrimaryCapability GetPrimary() + public IPrimaryCapability GetPrimary() { if (TryGetPrimary(out var primary)) { @@ -60,7 +58,7 @@ public IPrimaryCapability GetPrimary() } public bool TryGetPrimaryAs(out TPrimaryCapability primary) - where TPrimaryCapability : class, IPrimaryCapability + where TPrimaryCapability : class, IPrimaryCapability { if (TryGetPrimary(out var basePrimary) && basePrimary is TPrimaryCapability typed) { @@ -72,25 +70,25 @@ public bool TryGetPrimaryAs(out TPrimaryCapability primary) } public TPrimaryCapability? GetPrimaryOrDefaultAs() - where TPrimaryCapability : class, IPrimaryCapability + where TPrimaryCapability : class, IPrimaryCapability { TryGetPrimaryAs(out var primary); return primary; } public TPrimaryCapability GetRequiredPrimaryAs() - where TPrimaryCapability : class, IPrimaryCapability + where TPrimaryCapability : class, IPrimaryCapability { if (TryGetPrimaryAs(out var primary)) { return primary; } throw new InvalidOperationException( - $"Primary capability of type '{typeof(TPrimaryCapability).Name}' not found for subject '{typeof(TSubject).Name}'."); + $"Primary capability of type '{typeof(TPrimaryCapability).Name}' not found."); } public IReadOnlyList GetAll() - where TCapability : class, ICapability + where TCapability : class { var queryType = typeof(TCapability); if (!_capabilitiesByType.TryGetValue(queryType, out var arr) || arr.Length == 0) @@ -107,43 +105,31 @@ public IReadOnlyList GetAll() return typed; } - public IReadOnlyList> GetAll() + public IReadOnlyList GetAll() { if (_capabilitiesByType.Count == 0) - return Array.Empty>(); + return Array.Empty(); - var list = new List>(_totalCapabilityCount); + var list = new List(_totalCapabilityCount); foreach (var array in _capabilitiesByType.Values) { for (int i = 0; i < array.Length; i++) { - list.Add((ICapability)array.GetValue(i)!); - } - } - - if (list.Count > 1) - { - // Stable global ordering across different type buckets. - bool hasOrdered = false; - for (int i = 0; i < list.Count; i++) - { - if (list[i] is IOrderedCapability) - { - hasOrdered = true; break; - } - } - if (hasOrdered) - { - // Use stable sort (CapabilityOrdering) via a temp typed list. - CapabilityOrdering.SortInPlace(list); + list.Add(array.GetValue(i)!); } } - return list.Count == 0 ? Array.Empty>() : list.ToArray(); + // NOTE: Arrays are already sorted by order during Build(). + // However, when we combine capabilities from different type buckets, + // we need to maintain stable ordering across the entire list. + // Since we don't have order information here, we'll return them + // in the order they were stored (by type bucket). + + return list.AsReadOnly(); } public bool Has() - where TCapability : class, ICapability + where TCapability : class { var queryType = typeof(TCapability); if (!_capabilitiesByType.TryGetValue(queryType, out var arr) || arr.Length == 0) return false; @@ -152,7 +138,7 @@ public bool Has() } public int Count() - where TCapability : class, ICapability + where TCapability : class { var queryType = typeof(TCapability); if (!_capabilitiesByType.TryGetValue(queryType, out var arr) || arr.Length == 0) return 0; diff --git a/src/Cocoar.Capabilities/CompositionRegistryApi.cs b/src/Cocoar.Capabilities/CompositionRegistryApi.cs index df18279..c468f29 100644 --- a/src/Cocoar.Capabilities/CompositionRegistryApi.cs +++ b/src/Cocoar.Capabilities/CompositionRegistryApi.cs @@ -12,17 +12,17 @@ public CompositionRegistryApi(CapabilityScopeOptions options, DefaultCapabilityR _registry = sharedRegistry; } - public bool TryFind(TSubject subject, out IComposition composition) where TSubject : notnull + public bool TryFind(TSubject subject, out IComposition composition) where TSubject : notnull { return _registry.TryGetComposition(subject, out composition); } - public IComposition? FindOrDefault(TSubject subject) where TSubject : notnull + public IComposition? FindOrDefault(TSubject subject) where TSubject : notnull { return TryFind(subject, out var composition) ? composition : null; } - public IComposition FindRequired(TSubject subject) where TSubject : notnull + public IComposition FindRequired(TSubject subject) where TSubject : notnull { if (TryFind(subject, out var composition)) return composition; @@ -63,7 +63,7 @@ public bool Remove(TSubject subject) where TSubject : notnull } // Internal method for registering compositions (used by Composer.Build) - internal void Register(TSubject subject, IComposition composition, bool forceRegister = false) where TSubject : notnull + internal void Register(TSubject subject, IComposition composition, bool forceRegister = false) where TSubject : notnull { // Only register if scope allows it OR if explicitly forced (method override) if (options.UseCompositionRegistry || forceRegister) diff --git a/src/Cocoar.Capabilities/DefaultCapabilityRegistry.cs b/src/Cocoar.Capabilities/DefaultCapabilityRegistry.cs index 667a0f0..71a9d87 100644 --- a/src/Cocoar.Capabilities/DefaultCapabilityRegistry.cs +++ b/src/Cocoar.Capabilities/DefaultCapabilityRegistry.cs @@ -15,13 +15,13 @@ internal DefaultCapabilityRegistry(SubjectKeyCanonicalizer? canonicalizer = null _canonicalizer = canonicalizer ?? new SubjectKeyCanonicalizer(null); } - public void RegisterComposer(Composer composer) where TSubject : notnull + public void RegisterComposer(Composer composer) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(composer); var key = Canonicalize(composer.Subject!, out var valueLike); var newEntry = CapabilityEntry.FromComposer(composer); - if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposition(out var comp)) + if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposition(out var comp)) { newEntry = CapabilityEntry.FromBoth(composer, comp); } @@ -29,12 +29,12 @@ public void RegisterComposer(Composer composer) where TSubje } - public void RemoveComposer(TSubject subject) where TSubject : notnull + public void RemoveComposer(object subject) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(subject); var key = Canonicalize(subject!, out var valueLike); - if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposition(out var comp)) + if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposition(out var comp)) { StoreEntry(key, valueLike, CapabilityEntry.FromComposition(comp)); } @@ -44,7 +44,7 @@ public void RemoveComposer(TSubject subject) where TSubject : notnull } } - public bool TryGetComposer(TSubject subject, out Composer composer) where TSubject : notnull + public bool TryGetComposer(object subject, out Composer composer) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(subject); @@ -57,20 +57,20 @@ public bool TryGetComposer(TSubject subject, out Composer co return false; } - public void RegisterComposition(IComposition composition) where TSubject : notnull + public void RegisterComposition(IComposition composition) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(composition); var key = Canonicalize(composition.Subject!, out var valueLike); var newEntry = CapabilityEntry.FromComposition(composition); - if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposer(out Composer existingComposer)) + if (TryGetEntry(key, valueLike, out var existing) && existing.TryGetComposer(out Composer existingComposer)) { newEntry = CapabilityEntry.FromBoth(existingComposer, composition); } StoreEntry(key, valueLike, newEntry); } - public bool TryGetComposition(TSubject subject, out IComposition composition) where TSubject : notnull + public bool TryGetComposition(object subject, out IComposition composition) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(subject); @@ -83,7 +83,7 @@ public bool TryGetComposition(TSubject subject, out IComposition(IComposition composition) where TSubject : notnull + public void TransitionToComposition(IComposition composition) { ThrowIfDisposed(); @@ -93,46 +93,11 @@ public void TransitionToComposition(IComposition composition StoreEntry(key, valueLike, entry); } - public bool Remove(TSubject subject) where TSubject : notnull - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(subject); - var key = Canonicalize(subject!, out var valueLike); - return valueLike ? _valueTypeEntries.TryRemove(key, out _) : _refTypeEntries.Remove(key); - } - - public bool TryGetComposer(object subject, out object composer) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(subject); - var key = Canonicalize(subject, out var valueLike); - if (TryGetEntry(key, valueLike, out var entry) && entry.TryGetComposer(out composer)) - { - return true; - } - composer = default!; - return false; - } - - public bool TryGetComposition(object subject, out IComposition composition) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(subject); - var key = Canonicalize(subject, out var valueLike); - if (TryGetEntry(key, valueLike, out var entry) && entry.TryGetComposition(out object compositionObj)) - { - composition = (IComposition)compositionObj; - return true; - } - composition = default!; - return false; - } - public bool Remove(object subject) { ThrowIfDisposed(); ArgumentNullException.ThrowIfNull(subject); - var key = Canonicalize(subject, out var valueLike); + var key = Canonicalize(subject!, out var valueLike); return valueLike ? _valueTypeEntries.TryRemove(key, out _) : _refTypeEntries.Remove(key); } diff --git a/src/Cocoar.Capabilities/ICapability.cs b/src/Cocoar.Capabilities/ICapability.cs index e1e9f2f..64ddaf3 100644 --- a/src/Cocoar.Capabilities/ICapability.cs +++ b/src/Cocoar.Capabilities/ICapability.cs @@ -1,12 +1,7 @@ namespace Cocoar.Capabilities; -public interface ICapability { } - -public interface ICapability : ICapability { } - -public interface IPrimaryCapability : ICapability { } - -public interface IOrderedCapability -{ - int Order { get; } -} +/// +/// Marker interface indicating a capability that should be the primary capability for an instance. +/// Only one primary capability is allowed per instance. +/// +public interface IPrimaryCapability { } diff --git a/src/Cocoar.Capabilities/ICapabilityRegistry.cs b/src/Cocoar.Capabilities/ICapabilityRegistry.cs index 2b342fc..57bd93a 100644 --- a/src/Cocoar.Capabilities/ICapabilityRegistry.cs +++ b/src/Cocoar.Capabilities/ICapabilityRegistry.cs @@ -2,14 +2,11 @@ namespace Cocoar.Capabilities; public interface ICapabilityRegistry : IDisposable { - void RegisterComposer(Composer composer) where TSubject : notnull; - void RemoveComposer(TSubject subject) where TSubject : notnull; - bool TryGetComposer(TSubject subject, out Composer composer) where TSubject : notnull; - void RegisterComposition(IComposition composition) where TSubject : notnull; - bool TryGetComposition(TSubject subject, out IComposition composition) where TSubject : notnull; - void TransitionToComposition(IComposition composition) where TSubject : notnull; - bool Remove(TSubject subject) where TSubject : notnull; - bool TryGetComposer(object subject, out object composer); + void RegisterComposer(Composer composer); + void RemoveComposer(object subject); + bool TryGetComposer(object subject, out Composer composer); + void RegisterComposition(IComposition composition); bool TryGetComposition(object subject, out IComposition composition); + void TransitionToComposition(IComposition composition); bool Remove(object subject); } diff --git a/src/Cocoar.Capabilities/IComposerRegistry.cs b/src/Cocoar.Capabilities/IComposerRegistry.cs index 1740385..751e55d 100644 --- a/src/Cocoar.Capabilities/IComposerRegistry.cs +++ b/src/Cocoar.Capabilities/IComposerRegistry.cs @@ -2,8 +2,8 @@ namespace Cocoar.Capabilities; public interface IComposerRegistry : IDisposable { - void Register(TSubject subject, Composer composer) where TSubject : notnull; - bool TryGet(TSubject subject, out Composer composer) where TSubject : notnull; + void Register(TSubject subject, Composer composer) where TSubject : notnull; + bool TryGet(TSubject subject, out Composer composer) where TSubject : notnull; bool Remove(TSubject subject) where TSubject : notnull; bool TryGet(object subject, out object composer); diff --git a/src/Cocoar.Capabilities/IComposition.cs b/src/Cocoar.Capabilities/IComposition.cs index cb27b3e..e6238ca 100644 --- a/src/Cocoar.Capabilities/IComposition.cs +++ b/src/Cocoar.Capabilities/IComposition.cs @@ -4,33 +4,28 @@ public interface IComposition { object Subject { get; } int TotalCapabilityCount { get; } -} - -public interface IComposition : IComposition -{ - new TSubject Subject { get; } - + bool HasPrimary(); - bool HasPrimary() where TPrimaryCapability : class, IPrimaryCapability; + bool HasPrimary() where TPrimaryCapability : class, IPrimaryCapability; - bool TryGetPrimary(out IPrimaryCapability primary); + bool TryGetPrimary(out IPrimaryCapability primary); - IPrimaryCapability? GetPrimaryOrDefault(); + IPrimaryCapability? GetPrimaryOrDefault(); - IPrimaryCapability GetPrimary(); + IPrimaryCapability GetPrimary(); - bool TryGetPrimaryAs(out TPrimaryCapability primary) where TPrimaryCapability : class, IPrimaryCapability; + bool TryGetPrimaryAs(out TPrimaryCapability primary) where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability? GetPrimaryOrDefaultAs() where TPrimaryCapability : class, IPrimaryCapability; + TPrimaryCapability? GetPrimaryOrDefaultAs() where TPrimaryCapability : class, IPrimaryCapability; - TPrimaryCapability GetRequiredPrimaryAs() where TPrimaryCapability : class, IPrimaryCapability; + TPrimaryCapability GetRequiredPrimaryAs() where TPrimaryCapability : class, IPrimaryCapability; - IReadOnlyList GetAll() where TCapability : class, ICapability; + IReadOnlyList GetAll() where TCapability : class; - IReadOnlyList> GetAll(); + IReadOnlyList GetAll(); - bool Has() where TCapability : class, ICapability; + bool Has() where TCapability : class; - int Count() where TCapability : class, ICapability; + int Count() where TCapability : class; } diff --git a/src/Cocoar.Capabilities/ICompositionRegistry.cs b/src/Cocoar.Capabilities/ICompositionRegistry.cs index 0307adc..f73e7b4 100644 --- a/src/Cocoar.Capabilities/ICompositionRegistry.cs +++ b/src/Cocoar.Capabilities/ICompositionRegistry.cs @@ -2,8 +2,8 @@ namespace Cocoar.Capabilities; public interface ICompositionRegistry : IDisposable { - void Register(TSubject subject, IComposition composition) where TSubject : notnull; - bool TryGet(TSubject subject, out IComposition composition) where TSubject : notnull; + void Register(TSubject subject, IComposition composition) where TSubject : notnull; + bool TryGet(TSubject subject, out IComposition composition) where TSubject : notnull; bool Remove(TSubject subject) where TSubject : notnull; bool TryGet(object subject, out IComposition composition); diff --git a/src/Cocoar.Capabilities/TupleTypeExtractor.cs b/src/Cocoar.Capabilities/TupleTypeExtractor.cs index fb3db80..5accc17 100644 --- a/src/Cocoar.Capabilities/TupleTypeExtractor.cs +++ b/src/Cocoar.Capabilities/TupleTypeExtractor.cs @@ -20,19 +20,9 @@ public static Type[] GetTupleTypes() return [tupleType]; } - public static void ValidateCapabilityTypes(Type[] types) + public static void ValidateCapabilityTypes(Type[] types) { - var capabilityInterface = typeof(ICapability); - - foreach (var type in types) - { - if (!capabilityInterface.IsAssignableFrom(type)) - { - throw new ArgumentException( - $"Type '{type.Name}' must implement ICapability<{typeof(TSubject).Name}> " + - $"to be registered as a capability contract."); - } - } + // No validation needed - any type can be a capability } private static bool IsValueTupleType(Type type) From f6787ca00fb5abbbf43f9fa78fa3c788f631be2d Mon Sep 17 00:00:00 2001 From: Bernhard Windisch Date: Thu, 9 Oct 2025 15:42:05 +0200 Subject: [PATCH 2/2] Refactor benchmarks to implement ICapability interface for feature and configuration capabilities Add IOrderedCapability interface to support ordering in benchmarks --- .../CapabilityBenchmarks.cs | 18 ++++++++++-------- .../CoreVsRegistryBenchmarks.cs | 18 ++++++++++-------- .../OrderingBenchmarks.cs | 13 +++++++++---- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs index 608a4a7..dd23313 100644 --- a/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/CapabilityBenchmarks.cs @@ -7,15 +7,17 @@ namespace Cocoar.Capabilities.Benchmarks; [SimpleJob] public class CapabilityBenchmarks { + public interface ICapability { } + public record TestSubject(int Id, string Name); - public record FeatureCapability(string Name) ; - public record ConfigCapability(string Key, string Value) ; - public record ValidationCapability(string Rule) ; - public record CachingCapability(string CacheKey, TimeSpan Duration) ; - public record LoggingCapability(string LoggerName) ; - public record SecurityCapability(string Permission, string Role) ; - public record MonitoringCapability(string MetricName) ; - public record RetryCapability(string Operation, int MaxRetries) ; + public record FeatureCapability(string Name) : ICapability; + public record ConfigCapability(string Key, string Value) : ICapability; + public record ValidationCapability(string Rule) : ICapability; + public record CachingCapability(string CacheKey, TimeSpan Duration) : ICapability; + public record LoggingCapability(string LoggerName) : ICapability; + public record SecurityCapability(string Permission, string Role) : ICapability; + public record MonitoringCapability(string MetricName) : ICapability; + public record RetryCapability(string Operation, int MaxRetries) : ICapability; private IComposition _small10x50 = null!; private IComposition _large1000x50 = null!; diff --git a/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs index bfc4323..07b871a 100644 --- a/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/CoreVsRegistryBenchmarks.cs @@ -7,15 +7,17 @@ namespace Cocoar.Capabilities.Benchmarks; [SimpleJob] public class CoreVsRegistryBenchmarks { + public interface ICapability { } + public record TestSubject(int Id, string Name); - public record FeatureCapability(string Name) ; - public record ConfigCapability(string Key, string Value) ; - public record ValidationCapability(string Rule) ; - public record CachingCapability(string CacheKey, TimeSpan Duration) ; - public record LoggingCapability(string LoggerName) ; - public record SecurityCapability(string Permission, string Role) ; - public record MonitoringCapability(string MetricName) ; - public record RetryCapability(string Operation, int MaxRetries) ; + public record FeatureCapability(string Name) : ICapability; + public record ConfigCapability(string Key, string Value) : ICapability; + public record ValidationCapability(string Rule) : ICapability; + public record CachingCapability(string CacheKey, TimeSpan Duration) : ICapability; + public record LoggingCapability(string LoggerName) : ICapability; + public record SecurityCapability(string Permission, string Role) : ICapability; + public record MonitoringCapability(string MetricName) : ICapability; + public record RetryCapability(string Operation, int MaxRetries) : ICapability; private IComposition _coreComposition = null!; private IComposition _registryComposition = null!; diff --git a/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs b/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs index 91dfecd..e5a07d8 100644 --- a/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs +++ b/src/Cocoar.Capabilities.Benchmarks/OrderingBenchmarks.cs @@ -19,6 +19,11 @@ public class OrderingBenchmarks : IDisposable public record Subject(int Id, string Name); public interface ITestCap { } + + public interface IOrderedCapability + { + int Order { get; } + } public sealed record PlainCap(string Name) : ITestCap; // Unordered @@ -144,10 +149,10 @@ public void IterationSetupEnumeration() public int Enumerate_All_Unordered() { int sum = 0; - foreach (var c in _enumerationUnordered.GetAll()) + foreach (var c in _enumerationUnordered.GetAll()) { // cheap side-effect to prevent elimination - var obj = Unsafe.As(ref Unsafe.AsRef(in c)); + var obj = Unsafe.As(ref Unsafe.AsRef(in c)); if (obj != null) sum += obj.GetHashCode(); } return sum; @@ -157,9 +162,9 @@ public int Enumerate_All_Unordered() public int Enumerate_All_Ordered() { int sum = 0; - foreach (var c in _enumerationOrdered.GetAll()) + foreach (var c in _enumerationOrdered.GetAll()) { - var obj = Unsafe.As(ref Unsafe.AsRef(in c)); + var obj = Unsafe.As(ref Unsafe.AsRef(in c)); if (obj != null) sum += obj.GetHashCode(); } return sum;