315 lines
13 KiB
Markdown
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`
|