17 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:
- CQRS pattern implementation with command/query handlers exposed via HTTP or gRPC
- Automatic HTTP endpoint generation via Minimal API
- Automatic gRPC endpoint generation with source generators and Google Rich Error Model validation
- Dynamic query capabilities (filtering, sorting, grouping, aggregation)
- FluentValidation support with RFC 7807 Problem Details (HTTP) and Google Rich Error Model (gRPC)
- AOT (Ahead-of-Time) compilation compatibility for core packages (where dependencies allow)
Solution Structure
The solution contains 11 projects organized by responsibility (10 packages + 1 sample project):
Abstractions (interfaces and contracts only):
Svrnty.CQRS.Abstractions- Core interfaces (ICommandHandler, IQueryHandler, discovery contracts)Svrnty.CQRS.DynamicQuery.Abstractions- Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)Svrnty.CQRS.Grpc.Abstractions- gRPC-specific interfaces and contracts
Implementation:
Svrnty.CQRS- Core discovery and registration logicSvrnty.CQRS.MinimalApi- Minimal API endpoint mapping for commands/queries (recommended for HTTP)Svrnty.CQRS.DynamicQuery- PoweredSoft.DynamicQuery integration for advanced filteringSvrnty.CQRS.DynamicQuery.MinimalApi- Minimal API endpoint mapping for dynamic queriesSvrnty.CQRS.FluentValidation- Validation integration helpersSvrnty.CQRS.Grpc- gRPC service implementation supportSvrnty.CQRS.Grpc.Generators- Source generator for .proto files and gRPC service implementations
Sample Projects:
Svrnty.Sample- Comprehensive demo project showcasing both HTTP and gRPC endpoints
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.
-
Endpoint mapping (HTTP and gRPC) uses discovery to:
- Enumerate all registered commands/queries
- Dynamically generate endpoints at application startup
- Apply naming conventions (convert to lowerCamelCase)
- Generate gRPC service implementations via source generators
Key Files:
Svrnty.CQRS.Abstractions/Discovery/- Metadata interfacesSvrnty.CQRS/Discovery/- Discovery implementationsSvrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs- HTTP endpoint generationSvrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs- Dynamic query endpoint generationSvrnty.CQRS.Grpc.Generators/- gRPC service generation via source generators
Integration Options
There are two primary integration options for exposing commands and queries:
Option 1: gRPC (Recommended for performance-critical scenarios)
The Svrnty.CQRS.Grpc package with Svrnty.CQRS.Grpc.Generators source generator provides high-performance gRPC 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<AddUserCommand, int, AddUserCommandHandler>();
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
// Add gRPC support
builder.Services.AddGrpc();
var app = builder.Build();
// Map auto-generated gRPC service implementations
app.MapGrpcService<CommandServiceImpl>();
app.MapGrpcService<QueryServiceImpl>();
// Enable gRPC reflection for tools like grpcurl
app.MapGrpcReflectionService();
app.Run();
How It Works:
- Define
.protofiles inProtos/directory with your commands/queries as messages - Source generator automatically creates
CommandServiceImplandQueryServiceImplimplementations - Property names in C# commands must match proto field names (case-insensitive)
- FluentValidation is automatically integrated with Google Rich Error Model for structured validation errors
- Validation errors return
google.rpc.StatuswithBadRequestcontainingFieldViolations
Features:
- High-performance binary protocol
- Automatic service implementation generation at compile time
- Google Rich Error Model for structured validation errors
- Full FluentValidation integration
- gRPC reflection support for development tools
- Suitable for microservices, internal APIs, and low-latency scenarios
Key Files:
Svrnty.CQRS.Grpc/- Runtime support for gRPC servicesSvrnty.CQRS.Grpc.Generators/- Source generator for service implementations
Option 2: HTTP via Minimal API (Recommended for web/browser scenarios)
The Svrnty.CQRS.MinimalApi package provides HTTP endpoints for CQRS commands and queries:
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>();
// Add Swagger (optional)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
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 naming conventions (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"
- RFC 7807 Problem Details for validation errors
- Full Swagger/OpenAPI support
Key Files:
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs- Main implementation
Option 3: Both gRPC and HTTP (Dual Protocol Support)
You can enable both protocols simultaneously, allowing clients to choose their preferred protocol:
var builder = WebApplication.CreateBuilder(args);
// Register CQRS services
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultCommandDiscovery();
builder.Services.AddDefaultQueryDiscovery();
// Add commands and queries
AddCommands(builder.Services);
AddQueries(builder.Services);
// Add both gRPC and HTTP support
builder.Services.AddGrpc();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Map both gRPC and HTTP endpoints
app.MapGrpcService<CommandServiceImpl>();
app.MapGrpcService<QueryServiceImpl>();
app.MapGrpcReflectionService();
app.MapSvrntyCommands();
app.MapSvrntyQueries();
app.Run();
Benefits:
- Single codebase supports multiple protocols
- gRPC for high-performance, low-latency scenarios (microservices, internal APIs)
- HTTP for web browsers, legacy clients, and public APIs
- Same commands, queries, and validation logic for both protocols
- Swagger UI available for HTTP endpoints, gRPC reflection for gRPC clients
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
- Minimal API endpoint 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:
// Register dynamic query
services.AddDynamicQuery<Person, PersonDto>()
.AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
.AddAlterQueryable<Person, PersonDto, SecurityFilter>();
// Map dynamic query endpoints
app.MapSvrntyDynamicQueries(); // Creates POST/GET /api/query/{queryName} endpoints
Key Files:
Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs- Query execution logicSvrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs- HTTP endpoint mapping
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/svrnty/dotnet-cqrs
Package Dependencies
Core 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
- PoweredSoft.DynamicQuery: 3.0.1
- Pluralize.NET: 1.0.2
gRPC Dependencies (for Svrnty.CQRS.Grpc):
- Grpc.AspNetCore: 2.68.0 or later
- Grpc.AspNetCore.Server.Reflection: 2.71.0 or later (optional, for reflection)
- Grpc.StatusProto: 2.71.0 or later (for Rich Error Model validation)
- Grpc.Tools: 2.76.0 or later (for .proto compilation)
Source Generator Dependencies (for Svrnty.CQRS.Grpc.Generators):
- Microsoft.CodeAnalysis.CSharp: 5.0.0-2.final
- Microsoft.CodeAnalysis.Analyzers: 3.11.0
- Microsoft.Build.Utilities.Core: 17.0.0
- Targets: netstandard2.0 (for Roslyn compatibility)
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).
-
Endpoint Mapping Timing: Endpoints are mapped at application startup. Discovery services must be registered before calling
MapSvrntyCommands()/MapSvrntyQueries()or mapping gRPC services. -
FluentValidation Integration:
- For HTTP: Validation happens automatically in the Minimal API pipeline. Errors return RFC 7807 Problem Details.
- For gRPC: Validation happens automatically via source-generated services. Errors return Google Rich Error Model with structured FieldViolations.
- The framework REGISTERS validators in DI; actual validation execution is handled by the endpoint implementations.
-
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 - HTTP endpoint mapping:
Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs - Dynamic query logic:
Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs - Dynamic query endpoints:
Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs - gRPC support:
Svrnty.CQRS.Grpc/runtime,Svrnty.CQRS.Grpc.Generators/source generators - Sample application:
Svrnty.Sample/- demonstrates both HTTP and gRPC integration