From a4525bad6acf00310c2d8d1f700092d43c470a98 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Thu, 12 Mar 2026 03:30:27 -0400 Subject: [PATCH] Add Claude Code harness: rules, skills, hooks, and editorconfig - Add path-specific rules for commands/queries, dynamic queries, validation, and gRPC - Add /add-command, /add-query, /add-dynamic-query scaffolding skills - Add project settings with post-edit formatting, proto validation, and build-gate hooks - Add .editorconfig codifying existing code style conventions - Trim CLAUDE.md from 414 to 130 lines (domain details moved to rules) - Add .harness-version tracking for the shared claude-harness repo Co-Authored-By: Claude Opus 4.6 --- .claude/.harness-version | 1 + .claude/rules/commands-queries.md | 100 ++++++ .claude/rules/dynamic-query.md | 97 ++++++ .claude/rules/grpc.md | 109 +++++++ .claude/rules/validation.md | 60 ++++ .claude/settings.json | 79 +++++ .claude/skills/add-command/SKILL.md | 103 ++++++ .claude/skills/add-dynamic-query/SKILL.md | 125 ++++++++ .claude/skills/add-query/SKILL.md | 86 ++++++ .editorconfig | 96 ++++++ CLAUDE.md | 361 +++------------------- 11 files changed, 903 insertions(+), 314 deletions(-) create mode 100644 .claude/.harness-version create mode 100644 .claude/rules/commands-queries.md create mode 100644 .claude/rules/dynamic-query.md create mode 100644 .claude/rules/grpc.md create mode 100644 .claude/rules/validation.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/add-command/SKILL.md create mode 100644 .claude/skills/add-dynamic-query/SKILL.md create mode 100644 .claude/skills/add-query/SKILL.md create mode 100644 .editorconfig diff --git a/.claude/.harness-version b/.claude/.harness-version new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/.claude/.harness-version @@ -0,0 +1 @@ +1.0.0 diff --git a/.claude/rules/commands-queries.md b/.claude/rules/commands-queries.md new file mode 100644 index 0000000..19a62eb --- /dev/null +++ b/.claude/rules/commands-queries.md @@ -0,0 +1,100 @@ +--- +paths: + - "*Command*.cs" + - "*Query*.cs" + - "*Handler*.cs" + - "**/Program.cs" + - "**/ServiceCollectionExtensions.cs" +--- + +# Commands & Queries + +## Commands Are Domain Actions, Not CRUD + +Commands express **user intent**. Name them as the business action being performed. + +- `PlaceOrderCommand` not `CreateOrderCommand` +- `ApproveExpenseCommand` not `UpdateExpenseCommand` +- `DeactivateAccountCommand` not `DeleteAccountCommand` + +One CRUD "Update" often becomes multiple distinct commands, each with its own validation and side effects. + +## File Structure + +Command, handler, and validator live in the **same file**, organized by feature: + +```csharp +// File: Features/Orders/PlaceOrderCommand.cs +public record PlaceOrderCommand +{ + public string CustomerId { get; set; } = string.Empty; + public List Items { get; set; } = []; +} + +public class PlaceOrderCommandValidator : AbstractValidator +{ + public PlaceOrderCommandValidator() + { + RuleFor(x => x.CustomerId).NotEmpty(); + RuleFor(x => x.Items).NotEmpty(); + } +} + +public class PlaceOrderCommandHandler : ICommandHandler +{ + public Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken = default) + { + // implementation + } +} +``` + +## Handler Interfaces + +```csharp +// Command with no result +ICommandHandler + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Command with result +ICommandHandler + Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + +// Query (always returns result) — only for single-entity lookups or non-queryable data +IQueryHandler + Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) +``` + +## Registration + +```csharp +// Command without result +services.AddCommand(); + +// Command with result +services.AddCommand(); + +// Command with result + validator (from Svrnty.CQRS.FluentValidation) +services.AddCommand(); + +// Regular query — ONLY for single-entity lookups or non-queryable results +services.AddQuery(); +``` + +## When to Use Regular IQueryHandler vs Dynamic Query + +**Use `IQueryHandler`** (rare): +- Single entity by ID: `FetchOrderByIdQuery` +- Non-entity results: `GetDashboardStatsQuery` +- Complex aggregation not expressible as IQueryable + +**Use Dynamic Query** (default — see dynamic-query.md rule): +- Any list/collection query +- Anything that needs pagination, filtering, or sorting + +## Rules + +- Always use `CancellationToken` — never omit it +- Commands/queries are `record` types with `{ get; set; }` properties +- Default string properties to `string.Empty`, collections to `[]` +- Naming: endpoint name is auto-derived by stripping `Command`/`Query` suffix and converting to lowerCamelCase diff --git a/.claude/rules/dynamic-query.md b/.claude/rules/dynamic-query.md new file mode 100644 index 0000000..5cb3fec --- /dev/null +++ b/.claude/rules/dynamic-query.md @@ -0,0 +1,97 @@ +--- +paths: + - "*QueryableProvider*.cs" + - "*AlterQueryable*.cs" + - "*DynamicQuery*.cs" + - "*Interceptor*.cs" +--- + +# Dynamic Queries — The Default Query Pattern + +**99% of queries should be dynamic queries.** They provide pagination, filtering, sorting, grouping, and aggregation for free. + +## The Standard Pattern: IQueryableProviderOverride + +```csharp +// File: Features/Orders/OrderQueryableProvider.cs +public class OrderQueryableProvider : IQueryableProviderOverride +{ + private readonly AppDbContext _db; + public OrderQueryableProvider(AppDbContext db) => _db = db; + + public Task> GetQueryableAsync(object query, CancellationToken ct = default) + => Task.FromResult(_db.Orders.AsQueryable()); +} +``` + +Registration — one line: +```csharp +builder.Services.AddDynamicQueryWithProvider(); +``` + +This automatically creates endpoints with full filtering, sorting, and pagination support. + +## Required Dependencies + +These must be registered before `AddSvrntyCqrs`: +```csharp +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +## Alter Queryable Services (Security Filters, Tenant Isolation) + +Use `IAlterQueryableService` to modify the queryable before execution — for security filters, tenant isolation, default ordering, etc. + +```csharp +public class OrderTenantFilter : IAlterQueryableService +{ + private readonly ITenantContext _tenant; + public OrderTenantFilter(ITenantContext tenant) => _tenant = tenant; + + public Task> AlterQueryableAsync( + IQueryable query, + IDynamicQuery dynamicQuery, + CancellationToken ct = default) + { + return Task.FromResult(query.Where(o => o.TenantId == _tenant.Id)); + } +} +``` + +Registration: +```csharp +builder.Services.AddAlterQueryable(); +``` + +## Interceptors + +Up to 5 interceptors per query type. These modify the PoweredSoft DynamicQuery criteria at query build time. + +```csharp +builder.Services.AddDynamicQueryInterceptor(); +``` + +## Source to Destination Mapping + +When the entity type differs from the DTO: +```csharp +// Provider returns the source entity queryable +public class OrderQueryableProvider : IQueryableProviderOverride { ... } + +// Registration maps source -> destination +builder.Services.AddDynamicQueryWithProvider(); +``` + +## Key Interfaces + +```csharp +IQueryableProvider + Task> GetQueryableAsync(object query, CancellationToken ct) + +IQueryableProviderOverride : IQueryableProvider + // Marker interface — same method, signals override registration + +IAlterQueryableService + Task> AlterQueryableAsync(IQueryable query, IDynamicQuery dynamicQuery, CancellationToken ct) +``` diff --git a/.claude/rules/grpc.md b/.claude/rules/grpc.md new file mode 100644 index 0000000..a2472ce --- /dev/null +++ b/.claude/rules/grpc.md @@ -0,0 +1,109 @@ +--- +paths: + - "**/*.proto" + - "**/Protos/**" + - "*Grpc*" +--- + +# gRPC Integration + +## Proto File Conventions + +Proto files live in `Protos/` directory. The source generator (`Svrnty.CQRS.Grpc.Generators`) auto-generates service implementations at compile time. + +### Service Structure +```protobuf +service CommandService { + rpc PlaceOrder (PlaceOrderCommandRequest) returns (PlaceOrderCommandResponse); +} + +service QueryService { + rpc FetchUserById (FetchUserByIdQueryRequest) returns (FetchUserByIdQueryResponse); +} + +service DynamicQueryService { + rpc QueryOrders (DynamicQueryOrdersRequest) returns (DynamicQueryOrdersResponse); +} +``` + +### Naming Rules +- RPC method name: command/query name without the `Command`/`Query` suffix +- Request message: `{Name}CommandRequest` or `{Name}QueryRequest` +- Response message: `{Name}CommandResponse` or `{Name}QueryResponse` +- Dynamic query request: `DynamicQuery{PluralEntity}Request` +- Dynamic query response: `DynamicQuery{PluralEntity}Response` +- Fields use `snake_case` (proto convention), mapped case-insensitively to C# `PascalCase` properties + +### Field Mapping +C# property names must match proto field names (case-insensitive): + +| C# Property | Proto Field | +|---------------|-----------------| +| `UserId` | `user_id` | +| `Name` | `name` | +| `EmailAddress`| `email_address` | +| `OrderItems` | `order_items` | + +### Type Mapping +| C# | Proto | +|-------------|-------------| +| `int` | `int32` | +| `long` | `int64` | +| `string` | `string` | +| `bool` | `bool` | +| `double` | `double` | +| `float` | `float` | +| `List` | `repeated T`| +| complex type| `message` | + +### Dynamic Query Messages (standard, reuse across entities) +```protobuf +message DynamicQueryFilter { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType + string value = 3; + repeated DynamicQueryFilter and = 4; + repeated DynamicQueryFilter or = 5; +} + +message DynamicQuerySort { + string path = 1; + bool ascending = 2; +} + +message DynamicQueryGroup { + string path = 1; +} + +message DynamicQueryAggregate { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType +} +``` + +## Source Generator Behavior + +The generator in `Svrnty.CQRS.Grpc.Generators` auto-creates: +- `CommandServiceImpl` — implements `CommandService.CommandServiceBase` +- `QueryServiceImpl` — implements `QueryService.QueryServiceBase` +- `DynamicQueryServiceImpl` — implements `DynamicQueryService.DynamicQueryServiceBase` +- `GrpcServiceRegistration` — auto-registration code + +Generated implementations handle: +1. Request-to-POCO property mapping +2. Validator invocation (if registered) with Google Rich Error Model errors +3. Handler invocation with proper `CancellationToken` +4. DI scope management via `IServiceScopeFactory` + +## Validation in gRPC + +Validation errors return `google.rpc.Status` with `BadRequest` detail containing `FieldViolations`. This is automatic — do not manually validate in handlers. + +## Registration + +```csharp +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddGrpc(grpc => grpc.EnableReflection()); // reflection for grpcurl/grpcui +}); +``` diff --git a/.claude/rules/validation.md b/.claude/rules/validation.md new file mode 100644 index 0000000..cf89688 --- /dev/null +++ b/.claude/rules/validation.md @@ -0,0 +1,60 @@ +--- +paths: + - "*Validator*.cs" + - "*Validation*" +--- + +# FluentValidation Integration + +## Validator Pattern + +Validators live in the **same file** as their command/query, named `{CommandName}Validator`: + +```csharp +public class PlaceOrderCommandValidator : AbstractValidator +{ + public PlaceOrderCommandValidator() + { + RuleFor(x => x.CustomerId) + .NotEmpty() + .WithMessage("Customer is required"); + + RuleFor(x => x.Items) + .NotEmpty() + .WithMessage("At least one item is required"); + } +} +``` + +## Registration + +Validators are registered alongside their handler using the FluentValidation overloads: + +```csharp +// Command without result + validator +services.AddCommand(); + +// Command with result + validator +services.AddCommand(); + +// Query + validator +services.AddQuery(); +``` + +These come from `Svrnty.CQRS.FluentValidation.ServiceCollectionExtensions` — they call the base `AddCommand`/`AddQuery` then register `IValidator`. + +## Error Formats by Protocol + +The framework handles validation execution automatically. Errors are returned as: + +- **HTTP (Minimal API)**: RFC 7807 Problem Details — `400 Bad Request` with structured error body +- **gRPC**: Google Rich Error Model — `google.rpc.Status` with `BadRequest` detail containing `FieldViolations` + +You do NOT need to manually invoke validation in handlers. The endpoint layer (Minimal API or gRPC source-generated service) handles it. + +## Rules + +- Inherit from `AbstractValidator`, not `IValidator` directly +- Define all rules in the constructor +- Always include `.WithMessage()` for user-facing error messages +- Validator constructor can inject services for async/database validation diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..486f8aa --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,79 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build:*)", + "Bash(dotnet clean:*)", + "Bash(dotnet restore:*)", + "Bash(dotnet run:*)", + "Bash(dotnet test:*)", + "Bash(dotnet format:*)", + "Bash(dotnet add:*)", + "Bash(dotnet remove:*)", + "Bash(dotnet sln:*)", + "Bash(dotnet --list-sdks:*)", + "Bash(dotnet tool install:*)", + "Bash(dotnet ef:*)", + "Bash(grpcurl:*)", + "Bash(protogen:*)", + "Bash(mkdir:*)", + "Bash(ls:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git diff:*)", + "Bash(git branch:*)", + "Bash(git stash:*)", + "Bash(git add:*)", + "Bash(git checkout:*)", + "WebSearch", + "WebFetch(domain:learn.microsoft.com)", + "WebFetch(domain:github.com)", + "WebFetch(domain:www.nuget.org)", + "WebFetch(domain:stackoverflow.com)" + ], + "ask": [ + "Bash(dotnet publish:*)", + "Bash(dotnet pack:*)", + "Bash(git push:*)", + "Bash(git commit:*)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash -c 'INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r \".tool_input.file_path // empty\"); if [[ -n \"$FILE\" && \"$FILE\" == *.cs ]]; then dotnet format --include \"$FILE\" --no-restore --diagnostics IDE0005 IDE0055 IDE0161 2>/dev/null || true; fi'", + "timeout": 15, + "statusMessage": "Formatting..." + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash -c 'INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r \".tool_input.file_path // empty\"); if [[ -n \"$FILE\" && \"$FILE\" == *.proto ]]; then dotnet build --no-restore 2>&1 | grep -E \"(error|warning)\" | head -10; fi'", + "timeout": 30, + "statusMessage": "Validating proto..." + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash -c 'BUILD=$(dotnet build --no-restore 2>&1); if echo \"$BUILD\" | grep -q \"Build FAILED\"; then echo \"BUILD FAILED — errors below:\" >&2; echo \"$BUILD\" | grep -E \"error CS\" >&2; exit 2; fi'", + "timeout": 60, + "statusMessage": "Verifying build..." + } + ] + } + ] + } +} diff --git a/.claude/skills/add-command/SKILL.md b/.claude/skills/add-command/SKILL.md new file mode 100644 index 0000000..fc1f105 --- /dev/null +++ b/.claude/skills/add-command/SKILL.md @@ -0,0 +1,103 @@ +--- +name: add-command +description: Scaffold a new CQRS command with handler, validator, and DI registration. Use when the user wants to add a new command to a project. +argument-hint: +--- + +# Add Command + +Scaffold a new command based on: $ARGUMENTS + +## Instructions + +### 1. Determine the command name + +The name must express **domain intent**, not CRUD. If the user's description sounds like CRUD, translate it: +- "create a user" → `RegisterUserCommand` +- "update the order status" → contextual: `ShipOrderCommand`, `CancelOrderCommand`, `ApproveOrderCommand` +- "delete an account" → `DeactivateAccountCommand` or `CloseAccountCommand` + +Ask the user to clarify if the intent is ambiguous (e.g., "update order" could mean many things). + +### 2. Determine the feature folder + +Place the file in the appropriate feature folder: `Features/{DomainArea}/` +If the folder doesn't exist, create it. If the project doesn't use `Features/` yet, check the existing structure and follow the same pattern. + +### 3. Create the file + +Create a single file `Features/{DomainArea}/{CommandName}Command.cs` containing all three classes: + +```csharp +using FluentValidation; +using Svrnty.CQRS.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +// 1. Command POCO — record type, properties with defaults +public record {CommandName}Command +{ + // Properties based on user description + // Strings default to string.Empty, collections to [] +} + +// 2. Validator — rules in constructor, always include .WithMessage() +public class {CommandName}CommandValidator : AbstractValidator<{CommandName}Command> +{ + public {CommandName}CommandValidator() + { + // Validation rules based on command properties + } +} + +// 3. Handler — always async with CancellationToken +public class {CommandName}CommandHandler : ICommandHandler<{CommandName}Command, {ResultType}> +{ + public Task<{ResultType}> HandleAsync({CommandName}Command command, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} +``` + +If the command has **no return value**, use `ICommandHandler<{CommandName}Command>` (single generic param) with `Task HandleAsync(...)`. + +### 4. Register in DI + +Find the project's service registration (typically `Program.cs` or a dedicated registration method) and add: + +```csharp +// With validator (preferred) +builder.Services.AddCommand<{CommandName}Command, {ResultType}, {CommandName}CommandHandler, {CommandName}CommandValidator>(); + +// Without validator +builder.Services.AddCommand<{CommandName}Command, {ResultType}, {CommandName}CommandHandler>(); + +// No return value + validator +builder.Services.AddCommand<{CommandName}Command, {CommandName}CommandHandler, {CommandName}CommandValidator>(); +``` + +Ensure the `using Svrnty.CQRS.FluentValidation;` namespace is imported if using the validator overload. + +### 5. Proto message (if project uses gRPC) + +If the project has a `Protos/` directory, add the corresponding proto message: + +```protobuf +// In the CommandService definition +rpc {CommandName} ({CommandName}CommandRequest) returns ({CommandName}CommandResponse); + +// Request message +message {CommandName}CommandRequest { + // fields matching command properties, snake_case, numbered sequentially +} + +// Response message +message {CommandName}CommandResponse { + {result_type} result = 1; // omit if command has no return value +} +``` + +### 6. Summary + +After creating everything, list what was created and where. diff --git a/.claude/skills/add-dynamic-query/SKILL.md b/.claude/skills/add-dynamic-query/SKILL.md new file mode 100644 index 0000000..9bc4c67 --- /dev/null +++ b/.claude/skills/add-dynamic-query/SKILL.md @@ -0,0 +1,125 @@ +--- +name: add-dynamic-query +description: Scaffold a dynamic query with IQueryableProviderOverride for automatic pagination, filtering, and sorting. This is the DEFAULT choice for any list/collection query. +argument-hint: +--- + +# Add Dynamic Query + +Scaffold a dynamic query based on: $ARGUMENTS + +**This is the default query pattern.** It gives pagination, filtering, sorting, grouping, and aggregation for free. + +## Instructions + +### 1. Identify the entity + +Determine the source entity type from the user's description. If the project uses a DAL with EF Core, the entity should already exist in the DbContext. + +### 2. Determine the feature folder + +Place the file in: `Features/{DomainArea}/` + +### 3. Create the queryable provider + +Create `Features/{DomainArea}/{Entity}QueryableProvider.cs`: + +```csharp +using Svrnty.CQRS.DynamicQuery.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +public class {Entity}QueryableProvider : IQueryableProviderOverride<{Entity}> +{ + private readonly {DbContextType} _db; + public {Entity}QueryableProvider({DbContextType} db) => _db = db; + + public Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default) + => Task.FromResult(_db.{EntityPlural}.AsQueryable()); +} +``` + +If the entity needs a DTO projection (different shape for the API), note this for the registration step. + +### 4. Create alter queryable service (if needed) + +If the entity needs security filtering, tenant isolation, or default ordering, create `Features/{DomainArea}/{Entity}AlterQueryable.cs`: + +```csharp +using Svrnty.CQRS.DynamicQuery.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +public class {Entity}AlterQueryable : IAlterQueryableService<{Entity}, {EntityOrDto}> +{ + public Task> AlterQueryableAsync( + IQueryable<{Entity}> query, + IDynamicQuery dynamicQuery, + CancellationToken cancellationToken = default) + { + // Add default ordering, security filters, etc. + return Task.FromResult(query.OrderByDescending(x => x.CreatedAt)); + } +} +``` + +### 5. Register in DI + +Find the project's service registration and add: + +```csharp +// Basic — entity returned as-is +builder.Services.AddDynamicQueryWithProvider<{Entity}, {Entity}QueryableProvider>(); + +// With DTO projection — entity mapped to DTO +builder.Services.AddDynamicQueryWithProvider<{Entity}, {EntityDto}, {Entity}QueryableProvider>(); + +// With alter service (add after provider registration) +builder.Services.AddAlterQueryable<{Entity}, {EntityOrDto}, {Entity}AlterQueryable>(); +``` + +### 6. Ensure dynamic query dependencies are registered + +Check that these are registered (typically once per project, before `AddSvrntyCqrs`): + +```csharp +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +If they're already present, don't add duplicates. + +### 7. Proto message (if project uses gRPC) + +```protobuf +// In the DynamicQueryService definition +rpc Query{EntityPlural} (DynamicQuery{EntityPlural}Request) returns (DynamicQuery{EntityPlural}Response); + +// Entity message (if not already defined) +message {Entity} { + // fields matching entity properties +} + +// Request — uses standard dynamic query fields +message DynamicQuery{EntityPlural}Request { + int32 page = 1; + int32 page_size = 2; + repeated DynamicQueryFilter filters = 3; + repeated DynamicQuerySort sorts = 4; + repeated DynamicQueryGroup groups = 5; + repeated DynamicQueryAggregate aggregates = 6; +} + +// Response +message DynamicQuery{EntityPlural}Response { + repeated {Entity} data = 1; + int64 total_records = 2; + int32 number_of_pages = 3; +} +``` + +The standard `DynamicQueryFilter`, `DynamicQuerySort`, `DynamicQueryGroup`, and `DynamicQueryAggregate` messages should already be defined — reuse them. + +### 8. Summary + +After creating everything, list what was created and where. diff --git a/.claude/skills/add-query/SKILL.md b/.claude/skills/add-query/SKILL.md new file mode 100644 index 0000000..9dc4155 --- /dev/null +++ b/.claude/skills/add-query/SKILL.md @@ -0,0 +1,86 @@ +--- +name: add-query +description: Scaffold a new regular CQRS query with handler and DI registration. Use ONLY for single-entity lookups or non-queryable results. For list/collection queries, use /add-dynamic-query instead. +argument-hint: +--- + +# Add Query + +Scaffold a new regular query based on: $ARGUMENTS + +## Important: Is This the Right Skill? + +**This skill is for the rare case.** Most queries should be dynamic queries (`/add-dynamic-query`). + +Only use this for: +- Single entity by ID (e.g., `FetchOrderByIdQuery`) +- Non-entity results (e.g., `GetDashboardStatsQuery`, `CheckAvailabilityQuery`) +- Complex aggregation not expressible as IQueryable + +If the user is asking for a list/collection query with filtering, sorting, or pagination → suggest `/add-dynamic-query` instead. + +## Instructions + +### 1. Determine the query name + +Use descriptive, domain-oriented names: +- `FetchOrderByIdQuery` — fetching a single entity +- `GetSystemHealthQuery` — non-entity result +- `CheckInventoryAvailabilityQuery` — domain-specific check + +### 2. Determine the feature folder + +Place the file in: `Features/{DomainArea}/` + +### 3. Create the file + +Create `Features/{DomainArea}/{QueryName}Query.cs`: + +```csharp +using Svrnty.CQRS.Abstractions; + +namespace {ProjectNamespace}.Features.{DomainArea}; + +// 1. Query POCO +public record {QueryName}Query +{ + // Parameters — e.g., Id for lookups +} + +// 2. Handler +public class {QueryName}QueryHandler : IQueryHandler<{QueryName}Query, {ResultType}> +{ + public Task<{ResultType}> HandleAsync({QueryName}Query query, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} +``` + +### 4. Register in DI + +```csharp +builder.Services.AddQuery<{QueryName}Query, {ResultType}, {QueryName}QueryHandler>(); + +// With validator (if needed) +builder.Services.AddQuery<{QueryName}Query, {ResultType}, {QueryName}QueryHandler, {QueryName}QueryValidator>(); +``` + +### 5. Proto message (if project uses gRPC) + +```protobuf +// In the QueryService definition +rpc {QueryName} ({QueryName}QueryRequest) returns ({QueryName}QueryResponse); + +message {QueryName}QueryRequest { + // fields matching query properties +} + +message {QueryName}QueryResponse { + {ResultMessage} result = 1; +} +``` + +### 6. Summary + +After creating everything, list what was created and where. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..68897d2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,96 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{csproj,props,targets,xml}] +indent_size = 2 + +[*.{json,yml,yaml}] +indent_size = 2 + +[*.proto] +indent_size = 2 + +[*.cs] +# Namespace +csharp_style_namespace_declarations = file_scoped:warning + +# Braces — Allman style +csharp_new_line_before_open_brace = all + +# Usings +dotnet_sort_system_directives_first = true +csharp_using_directive_placement = outside_namespace:warning + +# var preferences — use var when type is apparent +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Expression bodies — prefer for simple members +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null checking +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences — exclude interface members (netstandard2.1 compat) +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Field naming — _camelCase for private fields +dotnet_naming_rule.private_fields_should_be_camel_case.severity = warning +dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_underscore + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_style.camel_case_underscore.required_prefix = _ +dotnet_naming_style.camel_case_underscore.capitalization = camel_case + +# Constants — PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.pascal_case.capitalization = pascal_case + +# Interfaces — I prefix +dotnet_naming_rule.interfaces_should_begin_with_i.severity = warning +dotnet_naming_rule.interfaces_should_begin_with_i.symbols = interfaces +dotnet_naming_rule.interfaces_should_begin_with_i.style = begins_with_i + +dotnet_naming_symbols.interfaces.applicable_kinds = interface + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# Async methods — Async suffix +dotnet_naming_rule.async_methods_should_end_with_async.severity = suggestion +dotnet_naming_rule.async_methods_should_end_with_async.symbols = async_methods +dotnet_naming_rule.async_methods_should_end_with_async.style = ends_with_async + +dotnet_naming_symbols.async_methods.applicable_kinds = method +dotnet_naming_symbols.async_methods.required_modifiers = async + +dotnet_naming_style.ends_with_async.required_suffix = Async +dotnet_naming_style.ends_with_async.capitalization = pascal_case diff --git a/CLAUDE.md b/CLAUDE.md index cb9cfe8..50ac60d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,9 @@ This file provides guidance to AI agents when working with code in this repository. +For domain-specific patterns (commands, queries, validation, gRPC, dynamic queries), see `.claude/rules/`. +For scaffolding commands, see `.claude/skills/` (`/add-command`, `/add-query`, `/add-dynamic-query`). + ## 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: @@ -11,11 +14,10 @@ This is Svrnty.CQRS, a modern implementation of Command Query Responsibility Seg - 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): +The solution contains projects organized by responsibility: **Abstractions (interfaces and contracts only):** - `Svrnty.CQRS.Abstractions` - Core interfaces (ICommandHandler, IQueryHandler, discovery contracts) @@ -24,81 +26,42 @@ The solution contains 11 projects organized by responsibility (10 packages + 1 s **Implementation:** - `Svrnty.CQRS` - Core discovery and registration logic -- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping for commands/queries (recommended for HTTP) +- `Svrnty.CQRS.MinimalApi` - Minimal API endpoint mapping for commands/queries - `Svrnty.CQRS.DynamicQuery` - PoweredSoft.DynamicQuery integration for advanced filtering - `Svrnty.CQRS.DynamicQuery.MinimalApi` - Minimal API endpoint mapping for dynamic queries - `Svrnty.CQRS.FluentValidation` - Validation integration helpers - `Svrnty.CQRS.Grpc` - gRPC service implementation support - `Svrnty.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 +**Sample:** +- `Svrnty.Sample` - 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. +**Key Design Principle:** Abstractions projects contain ONLY interfaces/attributes with minimal dependencies. Implementation projects depend on abstractions. ## 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 +dotnet restore # Restore dependencies +dotnet build # Build entire solution +dotnet build -c Release # Build in Release mode +dotnet pack -c Release -o ./artifacts -p:Version=1.0.0 # Create NuGet packages ``` ## Testing -This repository does not currently contain test projects. When adding tests: -- Place them in a `tests/` directory or alongside source projects +No test projects currently exist. When adding tests: +- Place them in a `tests/` directory - 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 - Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) - -// Command with result -ICommandHandler - Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) - -// Query (always returns result) -IQueryHandler - Task 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()`, it: - - Registers the handler in DI as `ICommandHandler` - - 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. 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 +1. `services.AddCommand()` registers the handler in DI and creates `ICommandMeta` metadata as a singleton +2. Discovery services (`ICommandDiscovery`, `IQueryDiscovery`) query all registered metadata from DI +3. Endpoint mapping (HTTP and gRPC) uses discovery to dynamically generate endpoints at startup **Key Files:** - `Svrnty.CQRS.Abstractions/Discovery/` - Metadata interfaces @@ -107,307 +70,77 @@ The framework uses a **metadata pattern** for runtime discovery: - `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - Dynamic query endpoint generation - `Svrnty.CQRS.Grpc.Generators/` - gRPC service generation via source generators -### Integration Options +### Integration -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:** -```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(); -builder.Services.AddCommand(); - -// Add gRPC support -builder.Services.AddGrpc(); - -var app = builder.Build(); - -// Map auto-generated gRPC service implementations -app.MapGrpcService(); -app.MapGrpcService(); - -// Enable gRPC reflection for tools like grpcurl -app.MapGrpcReflectionService(); - -app.Run(); -``` - -**How It Works:** -1. Define `.proto` files in `Protos/` directory with your commands/queries as messages -2. Source generator automatically creates `CommandServiceImpl` and `QueryServiceImpl` implementations -3. Property names in C# commands must match proto field names (case-insensitive) -4. FluentValidation is automatically integrated with **Google Rich Error Model** for structured validation errors -5. Validation errors return `google.rpc.Status` with `BadRequest` containing `FieldViolations` - -**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 services -- `Svrnty.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:** -```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(); -builder.Services.AddQuery, 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:** -1. Extension methods iterate through `ICommandDiscovery` and `IQueryDiscovery` -2. For each command/query, creates Minimal API endpoints using `MapPost()`/`MapGet()` -3. Applies naming conventions (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" -- 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: +Commands and queries can be exposed via HTTP (Minimal API), gRPC, or both simultaneously. The fluent configuration API handles all wiring: ```csharp -var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddGrpc(grpc => grpc.EnableReflection()); + cqrs.AddMinimalApi(); +}); -// 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(); -app.MapGrpcService(); -app.MapGrpcReflectionService(); - -app.MapSvrntyCommands(); -app.MapSvrntyQueries(); - -app.Run(); +app.UseSvrntyCqrs(); // Maps all endpoints ``` -**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` - Interface with GetFilters(), GetSorts(), GetGroups(), GetAggregates() -- `IQueryableProvider` - Provides base IQueryable to query against -- `IAlterQueryableService` - Middleware to modify queries (e.g., security filters) -- `DynamicQueryHandler` - Executes queries using PoweredSoft.DynamicQuery - -**Request Flow:** -1. HTTP request with filters/sorts/aggregates -2. Minimal API endpoint 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 -// Register dynamic query -services.AddDynamicQuery() - .AddDynamicQueryWithProvider() - .AddAlterQueryable(); - -// Map dynamic query endpoints -app.MapSvrntyDynamicQueries(); // Creates POST/GET /api/query/{queryName} endpoints -``` - -**Key Files:** -- `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` - Query execution logic -- `Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs` - HTTP endpoint mapping +See `Svrnty.Sample/Program.cs` for a complete working example. ## 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`) +- **Target Framework**: `net10.0` (except DynamicQuery.Abstractions: `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/svrnty/dotnet-cqrs -### Package Dependencies +### Key Dependencies -**Core Dependencies:** - **Microsoft.Extensions.DependencyInjection.Abstractions**: 10.0.0 - **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) +- **Grpc.AspNetCore**: 2.68.0+ +- **Grpc.StatusProto**: 2.71.0+ (Rich Error Model) +- **Microsoft.CodeAnalysis.CSharp**: 5.0.0-2.final (source generators, targets netstandard2.0) ## Publishing -NuGet packages are published automatically via GitHub Actions when a release is created: +NuGet packages publish automatically via GitHub Actions (`.github/workflows/publish-nugets.yml`) when a release is created. Tag becomes the version. -**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 +# Manual publish 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` -3. Register in DI: `services.AddCommand()` -4. (Optional) Add validator: `services.AddTransient, Validator>()` -5. Controller endpoint is automatically generated - -**Adding a New Feature to Framework:** +**Adding a New Feature to the 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) +4. When extending discovery, create corresponding metadata classes (implement ICommandMeta/IQueryMeta) 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. +The project uses C# 14. Be aware of these reserved keywords: +- **`field`**: Contextual keyword in property accessors for implicit backing fields +- **`extension`**: Reserved for extension containers; use `@extension` for identifiers ## 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. **Endpoint Mapping Timing**: Endpoints are mapped at application startup. Discovery services must be registered before calling `MapSvrntyCommands()`/`MapSvrntyQueries()` or mapping gRPC services. - -6. **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. - -7. **DynamicQuery Interceptors**: Support up to 5 interceptors per query type. Interceptors modify PoweredSoft DynamicQuery behavior. +1. **Async Everywhere**: All handlers are async. Always support CancellationToken. +2. **Generic Type Safety**: Framework relies heavily on generics. Maintain strong typing. +3. **Endpoint Mapping Timing**: Discovery services must be registered before calling `UseSvrntyCqrs()`. +4. **AOT Compatibility**: `IsAotCompatible` is set but not enforced — many dependencies are not AOT-compatible. ## Common Code Locations - Handler interfaces: `Svrnty.CQRS.Abstractions/ICommandHandler.cs`, `IQueryHandler.cs` -- Discovery implementations: `Svrnty.CQRS/Discovery/` +- Discovery: `Svrnty.CQRS/Discovery/` - Service registration: `*/ServiceCollectionExtensions.cs` in 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 +- HTTP endpoints: `Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs` +- Dynamic queries: `Svrnty.CQRS.DynamicQuery/DynamicQueryHandler.cs` +- gRPC generators: `Svrnty.CQRS.Grpc.Generators/` +- Sample: `Svrnty.Sample/`