dotnet-cqrs/CLAUDE.md

315 lines
13 KiB
Markdown

# 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
```bash
# 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:
```csharp
// 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:
#### Option 1: Minimal API (Recommended)
The **Svrnty.CQRS.MinimalApi** package provides modern Minimal API endpoints:
**Registration:**
```csharp
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:**
```csharp
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:**
```csharp
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:**
```bash
# 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<T> 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`