dotnet-cqrs/CLAUDE.md

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 attributes
  • Svrnty.CQRS.DynamicQuery.Abstractions - Dynamic query interfaces (multi-targets netstandard2.1 and net10.0)

Implementation:

  • Svrnty.CQRS - Core discovery and registration logic
  • Svrnty.CQRS.AspNetCore - MVC controller generation (legacy/backward compatibility)
  • Svrnty.CQRS.MinimalApi - Minimal API endpoint mapping (recommended for new projects)
  • Svrnty.CQRS.DynamicQuery - PoweredSoft.DynamicQuery integration
  • Svrnty.CQRS.DynamicQuery.AspNetCore - Dynamic query controllers
  • Svrnty.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 .Tests suffix (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:

  1. 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
  2. Discovery services (ICommandDiscovery, IQueryDiscovery) implemented in Svrnty.CQRS:

    • Query all registered metadata from DI container
    • Provide lookup methods: GetCommand(string name), GetCommands(), etc.
  3. 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 interfaces
  • Svrnty.CQRS/Discovery/ - Discovery implementations
  • Svrnty.CQRS.AspNetCore/Mvc/*FeatureProvider.cs - Dynamic controller generation
  • Svrnty.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:

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:

  1. Extension methods iterate through ICommandDiscovery and IQueryDiscovery
  2. For each command/query, creates Minimal API endpoints using MapPost()/MapGet()
  3. Applies same naming conventions as MVC (lowerCamelCase)
  4. Respects [CommandControllerIgnore] and [QueryControllerIgnore] attributes
  5. Integrates with ICommandAuthorizationService and IQueryAuthorizationService
  6. 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):

  1. 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]
  2. Conventions (CommandControllerConvention, QueryControllerConvention):

    • Apply naming to generated controllers
    • Default: Type name without suffix, converted to lowerCamelCase
    • Custom: Use [CommandName] or [QueryName] attribute
  3. Controllers expose automatic endpoints:

    • Commands: POST /api/command/{name}
    • Queries: POST /api/query/{name} or GET /api/query/{name}

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 against
  • IAlterQueryableService<TSource, TDestination> - Middleware to modify queries (e.g., security filters)
  • DynamicQueryHandler<TSource, TDestination> - Executes queries using PoweredSoft.DynamicQuery

Request Flow:

  1. HTTP request with filters/sorts/aggregates
  2. DynamicQueryController receives request
  3. DynamicQueryHandler gets base queryable from IQueryableProvider
  4. Applies alterations from all registered IAlterQueryableService instances
  5. Builds PoweredSoft query criteria
  6. Executes and returns IQueryExecutionResult

Registration Example:

services.AddDynamicQuery<Person, PersonDto>()
    .AddDynamicQueryWithProvider<Person, PersonQueryableProvider>()
    .AddAlterQueryable<Person, PersonDto, SecurityFilter>();

Key Files:

  • Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs
  • Svrnty.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-targets netstandard2.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

  1. Triggered on release publication
  2. Extracts version from release tag
  3. Runs dotnet pack -c Release -p:Version={tag}
  4. Pushes to NuGet.org using NUGET_API_KEY secret

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:

  1. Create command/query POCO in consumer project
  2. Implement handler: ICommandHandler<TCommand, TResult>
  3. Register in DI: services.AddCommand<CreatePersonCommand, CreatePersonCommandHandler>()
  4. (Optional) Add validator: services.AddTransient<IValidator<CreatePersonCommand>, Validator>()
  5. Controller endpoint is automatically generated

Adding a New Feature to Framework:

  1. Add interface to appropriate Abstractions project
  2. Implement in corresponding implementation project
  3. Update ServiceCollectionExtensions with registration method
  4. Ensure all projects maintain AOT compatibility (unless AspNetCore-specific)
  5. 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 -> createPerson endpoint

C# 14 Language Features

The project now uses C# 14, which introduces several new features. Be aware of these breaking changes:

Potential Breaking Changes:

  • field keyword: New contextual keyword in property accessors for implicit backing fields
  • extension keyword: Reserved for extension containers; use @extension for identifiers
  • partial return type: Cannot use partial as return type without escaping
  • Span overload resolution: New implicit conversions may select different overloads
  • scoped as 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

  1. AOT Compatibility: Currently not enforced. The IsAotCompatible property is set on some projects but many dependencies (including FluentValidation, PoweredSoft.DynamicQuery) are not AOT-compatible. Future work may address this.

  2. Async Everywhere: All handlers are async. Always support CancellationToken.

  3. Generic Type Safety: Framework relies heavily on generics for compile-time safety. When adding features, maintain strong typing.

  4. Metadata Pattern: When extending discovery, always create corresponding metadata classes (implement ICommandMeta/IQueryMeta).

  5. Feature Provider Timing: Controller generation happens during MVC startup. Discovery services must be registered before calling AddSvrntyCommands/Queries.

  6. FluentValidation: This framework only REGISTERS validators. Actual validation execution requires separate middleware/filters in consumer applications.

  7. 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.cs in 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