13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Segregation (CQRS) for .NET 10. It was forked from PoweredSoft.CQRS and provides:
- Automatic REST endpoint generation from command/query handlers
- Dynamic query capabilities (filtering, sorting, grouping, aggregation)
- ASP.NET Core MVC integration
- FluentValidation support
- AOT (Ahead-of-Time) compilation compatibility for core packages
Solution Structure
The solution contains 9 projects organized by responsibility:
Abstractions (interfaces and contracts only):
Svrnty.CQRS.Abstractions- Core interfaces (ICommandHandler, IQueryHandler, discovery contracts)Svrnty.CQRS.AspNetCore.Abstractions- ASP.NET Core attributesSvrnty.CQRS.DynamicQuery.Abstractions- Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)
Implementation:
Svrnty.CQRS- Core discovery and registration logicSvrnty.CQRS.AspNetCore- MVC controller generation (legacy/backward compatibility)Svrnty.CQRS.MinimalApi- Minimal API endpoint mapping (recommended for new projects)Svrnty.CQRS.DynamicQuery- PoweredSoft.DynamicQuery integrationSvrnty.CQRS.DynamicQuery.AspNetCore- Dynamic query controllersSvrnty.CQRS.FluentValidation- Validation integration helpers
Key Design Principle: Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. This allows consumers to reference abstractions without pulling in heavy implementation dependencies.
Build Commands
# Restore dependencies
dotnet restore
# Build entire solution
dotnet build
# Build in Release mode
dotnet build -c Release
# Create NuGet packages (with version)
dotnet pack -c Release -o ./artifacts -p:Version=1.0.0
# Build specific project
dotnet build Svrnty.CQRS/Svrnty.CQRS.csproj
Testing
This repository does not currently contain test projects. When adding tests:
- Place them in a
tests/directory or alongside source projects - Name them with
.Testssuffix (e.g.,Svrnty.CQRS.Tests)
Architecture
Core CQRS Pattern
The framework uses handler interfaces that follow this pattern:
// Command with no result
ICommandHandler<TCommand>
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default)
// Command with result
ICommandHandler<TCommand, TResult>
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default)
// Query (always returns result)
IQueryHandler<TQuery, TResult>
Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default)
Metadata-Driven Discovery
The framework uses a metadata pattern for runtime discovery:
-
When you register a handler using
services.AddCommand<TCommand, THandler>(), it:- Registers the handler in DI as
ICommandHandler<TCommand, THandler> - Creates metadata (
ICommandMeta) describing the command type, handler type, and result type - Stores metadata as singleton in DI
- Registers the handler in DI as
-
Discovery services (
ICommandDiscovery,IQueryDiscovery) implemented inSvrnty.CQRS:- Query all registered metadata from DI container
- Provide lookup methods:
GetCommand(string name),GetCommands(), etc.
-
ASP.NET Core feature providers use discovery to:
- Enumerate all registered commands/queries
- Dynamically generate generic controller instances at startup
- Apply naming conventions (convert to lowerCamelCase)
Key Files:
Svrnty.CQRS.Abstractions/Discovery/- Metadata interfacesSvrnty.CQRS/Discovery/- Discovery implementationsSvrnty.CQRS.AspNetCore/Mvc/*FeatureProvider.cs- Dynamic controller generationSvrnty.CQRS.AspNetCore/Mvc/*Convention.cs- Naming conventions
ASP.NET Core Integration Options
There are two options for integrating commands and queries with ASP.NET Core:
Option 1: Minimal API (Recommended)
The Svrnty.CQRS.MinimalApi package provides modern Minimal API endpoints:
Registration:
var builder = WebApplication.CreateBuilder(args);
// Register CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Add your commands and queries
builder.Services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>();
builder.Services.AddQuery<PersonQuery, IQueryable<Person>, PersonQueryHandler>();
var app = builder.Build();
// Map endpoints (this creates routes automatically)
app.MapSvrntyCommands(); // Maps all commands to POST /api/command/{name}
app.MapSvrntyQueries(); // Maps all queries to POST/GET /api/query/{name}
app.Run();
How It Works:
- Extension methods iterate through
ICommandDiscoveryandIQueryDiscovery - For each command/query, creates Minimal API endpoints using
MapPost()/MapGet() - Applies same naming conventions as MVC (lowerCamelCase)
- Respects
[CommandControllerIgnore]and[QueryControllerIgnore]attributes - Integrates with
ICommandAuthorizationServiceandIQueryAuthorizationService - Supports OpenAPI/Swagger documentation
Features:
- Queries support both POST (with JSON body) and GET (with query string parameters)
- Commands only support POST with JSON body
- Authorization via authorization services (returns 401/403 status codes)
- Customizable route prefixes:
MapSvrntyCommands("my-prefix") - Automatic OpenAPI tags: "Commands" and "Queries"
Key Files:
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs- Main implementation
Option 2: MVC Controllers (Legacy)
The AspNetCore packages use dynamic controller generation (maintained for backward compatibility):
-
Feature Providers (
CommandControllerFeatureProvider,QueryControllerFeatureProvider):- Called during MVC startup
- Discover handlers via
ICommandDiscovery/IQueryDiscovery - Create generic controller types:
CommandController<TCommand>,QueryController<TQuery, TResult> - Skip handlers marked with
[CommandControllerIgnore]or[QueryControllerIgnore]
-
Conventions (
CommandControllerConvention,QueryControllerConvention):- Apply naming to generated controllers
- Default: Type name without suffix, converted to lowerCamelCase
- Custom: Use
[CommandName]or[QueryName]attribute
-
Controllers expose automatic endpoints:
- Commands:
POST /api/command/{name} - Queries:
POST /api/query/{name}orGET /api/query/{name}
- Commands:
Registration Pattern:
services
.AddControllers()
.AddSvrntyQueries() // Registers query feature provider
.AddSvrntyCommands(); // Registers command feature provider
Dynamic Query System
Dynamic queries provide OData-like filtering capabilities:
Core Components:
IDynamicQuery<TSource, TDestination>- Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates()IQueryableProvider<TSource>- Provides base IQueryable to query againstIAlterQueryableService<TSource, TDestination>- Middleware to modify queries (e.g., security filters)DynamicQueryHandler<TSource, TDestination>- Executes queries using PoweredSoft.DynamicQuery
Request Flow:
- HTTP request with filters/sorts/aggregates
- DynamicQueryController receives request
- DynamicQueryHandler gets base queryable from IQueryableProvider
- Applies alterations from all registered IAlterQueryableService instances
- Builds PoweredSoft query criteria
- Executes and returns IQueryExecutionResult
Registration Example:
services.AddDynamicQuery<Person, PersonDto>()
.AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
.AddAlterQueryable<Person, PersonDto, SecurityFilter>();
Key Files:
Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.csSvrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs- Request models
Package Configuration
All projects target .NET 10.0 and use C# 14, sharing common configuration:
- Target Framework:
net10.0(except DynamicQuery.Abstractions which multi-targetsnetstandard2.1;net10.0) - Language Version: C# 14
- IsAotCompatible: Currently set but not enforced (many dependencies are not AOT-compatible yet)
- Symbols: Portable debug symbols with source, published as
.snupkg - NuGet metadata: Icon, README, license (MIT), and repository URL included in packages
- Authors: David Lebee, Mathias Beaulieu-Duncan
- Repository: https://git.openharbor.io/Open-Harbor/dotnet-cqrs
Package Dependencies
- Microsoft.Extensions.DependencyInjection.Abstractions: 10.0.0-rc.2.25502.107 (will update to stable when .NET 10 is released)
- FluentValidation: 11.11.0
- Microsoft.CodeAnalysis.CSharp: 4.13.0
- PoweredSoft.DynamicQuery: 3.0.1
- Pluralize.NET: 1.0.2
Publishing
NuGet packages are published automatically via GitHub Actions when a release is created:
Workflow: .github/workflows/publish-nugets.yml
- Triggered on release publication
- Extracts version from release tag
- Runs
dotnet pack -c Release -p:Version={tag} - Pushes to NuGet.org using
NUGET_API_KEYsecret
Manual publish:
# Create packages with specific version
dotnet pack -c Release -o ./artifacts -p:Version=1.2.3
# Push to NuGet
dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key YOUR_KEY
Development Workflow
Adding a New Command/Query Handler:
- Create command/query POCO in consumer project
- Implement handler:
ICommandHandler<TCommand, TResult> - Register in DI:
services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>() - (Optional) Add validator:
services.AddTransient<IValidator<CreatePersonCommand>, Validator>() - Controller endpoint is automatically generated
Adding a New Feature to Framework:
- Add interface to appropriate Abstractions project
- Implement in corresponding implementation project
- Update ServiceCollectionExtensions with registration method
- Ensure all projects maintain AOT compatibility (unless AspNetCore-specific)
- Update package version and release notes
Naming Conventions:
- Commands/Queries: Use
[CommandName]or[QueryName]attribute for custom names - Default naming: Strips "Command"/"Query" suffix, converts to lowerCamelCase
- Example:
CreatePersonCommand->createPersonendpoint
C# 14 Language Features
The project now uses C# 14, which introduces several new features. Be aware of these breaking changes:
Potential Breaking Changes:
fieldkeyword: New contextual keyword in property accessors for implicit backing fieldsextensionkeyword: Reserved for extension containers; use@extensionfor identifierspartialreturn type: Cannot usepartialas return type without escaping- Span overload resolution: New implicit conversions may select different overloads
scopedas lambda modifier: Always treated as modifier in lambda parameters
New Features Available:
- Extension members (static extension members and extension properties)
- Implicit span conversions
- Unbound generic types with
nameof - Lambda parameter modifiers without type specification
- Partial instance constructors and events
- Null-conditional assignment (
?.=and?[]=)
The codebase currently compiles without warnings on C# 14.
Important Implementation Notes
-
AOT Compatibility: Currently not enforced. The
IsAotCompatibleproperty is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this. -
Async Everywhere: All handlers are async. Always support CancellationToken.
-
Generic Type Safety: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing.
-
Metadata Pattern: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta).
-
Feature Provider Timing: Controller generation happens during MVC startup. Discovery services must be registered before calling AddSvrntyCommands/Queries.
-
FluentValidation: This framework only REGISTERS validators. Actual validation execution requires separate middleware/filters in consumer applications.
-
DynamicQuery Interceptors: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior.
Common Code Locations
- Handler interfaces:
Svrnty.CQRS.Abstractions/ICommandHandler.cs,IQueryHandler.cs - Discovery implementations:
Svrnty.CQRS/Discovery/ - Service registration:
*/ServiceCollectionExtensions.csin each project - Controller templates:
Svrnty.CQRS.AspNetCore/Mvc/*Controller.cs - Dynamic query logic:
Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs - Naming/routing:
Svrnty.CQRS.AspNetCore/Mvc/*Convention.cs