From f6e67986facd76702bfcfada0cd9a0a57c210aa2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Brule Date: Sun, 8 Mar 2026 14:02:19 -0400 Subject: [PATCH] docs(cqrs): add architecture diagram, package index, and getting started guide Co-Authored-By: Svrnty Inc. --- CHANGELOG.md | 4 + README.md | 25 +- docs/ARCHITECTURE.md | 178 ++++++++++++++ docs/GETTING_STARTED.md | 514 ++++++++++++++++++++++++++++++++++++++++ docs/PACKAGE_INDEX.md | 335 ++++++++++++++++++++++++++ 5 files changed, 1051 insertions(+), 5 deletions(-) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/GETTING_STARTED.md create mode 100644 docs/PACKAGE_INDEX.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bafb321..25e9cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `.library-manifest.yaml` for cross-repo discovery and dependency tracking - Initial project setup +- `docs/ARCHITECTURE.md` -- package dependency graph, CQRS data flows, saga flow, separation of concerns +- `docs/PACKAGE_INDEX.md` -- per-package reference for all 18 NuGet packages with key types and dependencies +- `docs/GETTING_STARTED.md` -- step-by-step guide covering handler registration, gRPC setup, MinimalApi, DynamicQuery, domain events, sagas, and notifications ### Changed +- Updated README.md to reflect correct package count (18), added links to new docs, added Related Libraries section linking to flutter_cqrs_datasource ### Fixed diff --git a/README.md b/README.md index bda9927..b57fd73 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,20 @@ dotnet test ## Architecture -10 NuGet packages organized by concern: +18 NuGet packages organized by concern: -- **Abstractions**: Core interfaces (ICommandHandler, IQueryHandler) -- **Core**: Discovery, registration, handler execution +- **Abstractions**: Core interfaces (ICommandHandler, IQueryHandler, IDomainEvent, ISaga, INotificationPublisher) +- **Core**: Discovery, registration, handler execution, CqrsBuilder fluent API - **MinimalApi**: HTTP endpoint mapping with RFC 7807 validation - **Grpc**: gRPC service support with Google Rich Error Model -- **Grpc.Generators**: Source generator for .proto files and service implementations -- **DynamicQuery**: PoweredSoft integration for filtering, sorting, paging +- **Grpc.Generators**: Roslyn source generator for .proto files and service implementations +- **DynamicQuery**: PoweredSoft integration for filtering, sorting, paging (with EF Core support) - **FluentValidation**: Validator registration helpers +- **Events**: Domain event publishing (with RabbitMQ transport) +- **Sagas**: Saga orchestration pattern with compensation and distributed execution (with RabbitMQ transport) +- **Notifications**: Real-time notification streaming (with gRPC transport) + +See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for a full dependency diagram and data flow. ## Configuration @@ -57,6 +62,16 @@ builder.Services.AddSvrntyCqrs(cqrs => app.UseSvrntyCqrs(); ``` +## Documentation + +- [Architecture](./docs/ARCHITECTURE.md) -- Package dependency graph, CQRS data flows, separation of concerns +- [Package Index](./docs/PACKAGE_INDEX.md) -- Per-package reference with key types and dependencies +- [Getting Started](./docs/GETTING_STARTED.md) -- Step-by-step guide covering commands, queries, gRPC, DynamicQuery, events, sagas, and notifications + +## Related Libraries + +- **[flutter_cqrs_datasource](https://git.openharbor.io/svrnty/flutter_cqrs_datasource)** -- Flutter/Dart counterpart for consuming Svrnty.CQRS services from mobile and desktop apps + ## Contributing See [CLAUDE.md](./CLAUDE.md) for development guidelines. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..d334c0a --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,178 @@ +# Architecture + +> Svrnty.CQRS is a modular CQRS/event-sourcing framework for .NET 10, organized as 18 NuGet packages with clear separation of concerns. + +## Package Dependency Graph + +``` + Svrnty.CQRS.Abstractions + (ICommandHandler, IQueryHandler) + | + +-----------------+-----------------+ + | | + Svrnty.CQRS Svrnty.CQRS.FluentValidation + (Discovery, Registration, (AbstractValidator binding) + CqrsBuilder, DI) depends on: Abstractions, Core + | + +------------+------------+---------------------------+ + | | | | + MinimalApi Grpc DynamicQuery Sagas + (HTTP REST) (gRPC) (Filtering, (Orchestrator, + | Sorting, Paging) Compensation) + | | | + Grpc.Abstractions DQ.Abstractions Sagas.Abstractions + (GrpcIgnore attr) (IQueryableProvider) (ISaga, ISagaBuilder, + | | | ISagaOrchestrator) + Grpc.Generators DQ.MinimalApi | | + (Source gen, (HTTP endpoints | Sagas.RabbitMQ + .proto gen) for DQ) | (RabbitMQ transport) + | + DQ.EntityFramework + (EF Core provider) + + Events.Abstractions Notifications.Abstractions + (IDomainEvent, (INotificationPublisher, + IDomainEventPublisher) StreamingNotificationAttribute) + | | + Events.RabbitMQ Notifications.Grpc + (RabbitMQ transport) (gRPC streaming) +``` + +## Dependency Matrix + +| Package | Depends On (internal) | +|---|---| +| `Svrnty.CQRS.Abstractions` | _(none)_ | +| `Svrnty.CQRS` | Abstractions | +| `Svrnty.CQRS.MinimalApi` | Abstractions, Core | +| `Svrnty.CQRS.Grpc` | Core | +| `Svrnty.CQRS.Grpc.Abstractions` | _(none)_ | +| `Svrnty.CQRS.Grpc.Generators` | _(none, Roslyn source gen)_ | +| `Svrnty.CQRS.FluentValidation` | Abstractions, Core | +| `Svrnty.CQRS.DynamicQuery.Abstractions` | _(none)_ | +| `Svrnty.CQRS.DynamicQuery` | DynamicQuery.Abstractions, Core | +| `Svrnty.CQRS.DynamicQuery.MinimalApi` | Abstractions, DynamicQuery.Abstractions, DynamicQuery | +| `Svrnty.CQRS.DynamicQuery.EntityFramework` | DynamicQuery | +| `Svrnty.CQRS.Events.Abstractions` | _(none)_ | +| `Svrnty.CQRS.Events.RabbitMQ` | Events.Abstractions | +| `Svrnty.CQRS.Sagas.Abstractions` | _(none)_ | +| `Svrnty.CQRS.Sagas` | Core, Sagas.Abstractions | +| `Svrnty.CQRS.Sagas.RabbitMQ` | Sagas | +| `Svrnty.CQRS.Notifications.Abstractions` | _(none)_ | +| `Svrnty.CQRS.Notifications.Grpc` | Notifications.Abstractions | + +## CQRS Data Flow + +### Command Flow + +``` +Client Request + | + v +[MinimalApi POST /api/command/{name}] or [gRPC CommandService/{name}] + | + v +FluentValidation (if validator registered) + | + |-- Validation fails --> RFC 7807 ProblemDetails (HTTP) / Google Rich Error (gRPC) + | + v +ICommandHandler.HandleAsync(command, ct) + | + v +Command Result (or void) + | + +--> (optional) IDomainEventPublisher.PublishAsync(event) + +--> (optional) INotificationPublisher.PublishAsync(notification) +``` + +### Query Flow + +``` +Client Request + | + v +[MinimalApi POST /api/query/{name}] or [gRPC QueryService/{name}] + | + v +IQueryHandler.HandleAsync(query, ct) + | + v +Query Result +``` + +### Dynamic Query Flow + +``` +Client Request (with filters, sorts, pagination) + | + v +[MinimalApi POST /api/dynamic-query/{entity}] + | + v +IQueryableProvider.GetQueryableAsync(query, ct) + | + v +PoweredSoft.DynamicQuery engine (applies filters, sorts, groups, aggregates) + | + v +IAlterQueryableService (optional interception) + | + v +Paged/Grouped result set +``` + +### Saga Flow + +``` +ISagaOrchestrator.StartAsync(data) + | + v +ISaga.Configure(builder) -- defines steps + | + v +Step 1: Execute --> Step 2: Execute --> Step 3: Execute --> Completed + | | | + | | +-- fails --> + | | | + | +-- compensate <-----------------+ + | | + +-- compensate <-----------------+ + | + v + Compensated (rolled back) +``` + +## Separation of Concerns + +The framework follows a layered architecture: + +1. **Abstractions layer** (4 packages) -- Pure interfaces and marker types with zero dependencies. Can be referenced by any project without pulling in implementation details. + - `Svrnty.CQRS.Abstractions` + - `Svrnty.CQRS.DynamicQuery.Abstractions` + - `Svrnty.CQRS.Events.Abstractions` + - `Svrnty.CQRS.Sagas.Abstractions` + - `Svrnty.CQRS.Grpc.Abstractions` + - `Svrnty.CQRS.Notifications.Abstractions` + +2. **Core layer** (1 package) -- Handler discovery, DI registration, and the `CqrsBuilder` fluent API. + - `Svrnty.CQRS` + +3. **Transport layer** (4 packages) -- Maps commands/queries to HTTP or gRPC endpoints. + - `Svrnty.CQRS.MinimalApi` + - `Svrnty.CQRS.Grpc` + - `Svrnty.CQRS.Grpc.Generators` + - `Svrnty.CQRS.DynamicQuery.MinimalApi` + +4. **Feature layer** (4 packages) -- Optional capabilities that can be composed in. + - `Svrnty.CQRS.FluentValidation` + - `Svrnty.CQRS.DynamicQuery` + - `Svrnty.CQRS.DynamicQuery.EntityFramework` + - `Svrnty.CQRS.Sagas` + +5. **Infrastructure layer** (3 packages) -- Concrete transport bindings for messaging and streaming. + - `Svrnty.CQRS.Events.RabbitMQ` + - `Svrnty.CQRS.Sagas.RabbitMQ` + - `Svrnty.CQRS.Notifications.Grpc` + +This layering ensures that application code depends only on abstractions, while transport and infrastructure concerns remain pluggable. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..7f5c5df --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,514 @@ +# Getting Started + +> Step-by-step guide to building a CQRS application with Svrnty.CQRS on .NET 10. + +## Prerequisites + +- .NET 10 SDK +- A text editor or IDE with C# support + +## 1. Create a New Project + +```bash +dotnet new web -n MyCqrsApp +cd MyCqrsApp +``` + +Add the required packages: + +```bash +dotnet add package Svrnty.CQRS +dotnet add package Svrnty.CQRS.Abstractions +dotnet add package Svrnty.CQRS.MinimalApi +dotnet add package Svrnty.CQRS.FluentValidation +``` + +## 2. Define Commands and Queries + +### Command with Result + +A command represents an action that changes state. Implement `ICommandHandler` for commands that return a value. + +```csharp +using Svrnty.CQRS.Abstractions; + +// The command (a plain record/class) +public record CreateUserCommand +{ + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public int Age { get; set; } +} + +// The handler +public class CreateUserCommandHandler : ICommandHandler +{ + public Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default) + { + // Your business logic here -- persist to database, etc. + return Task.FromResult(123); // Return the new user ID + } +} +``` + +### Command without Result + +For commands that do not return a value, implement `ICommandHandler`: + +```csharp +public record DeleteUserCommand +{ + public int UserId { get; set; } +} + +public class DeleteUserCommandHandler : ICommandHandler +{ + public Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken = default) + { + // Delete the user + return Task.CompletedTask; + } +} +``` + +### Query + +A query retrieves data without side effects. Implement `IQueryHandler`: + +```csharp +public record GetUserQuery +{ + public int UserId { get; set; } +} + +public record UserDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; +} + +public class GetUserQueryHandler : IQueryHandler +{ + public Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken = default) + { + return Task.FromResult(new UserDto + { + Id = query.UserId, + Name = "John Doe", + Email = "john@example.com" + }); + } +} +``` + +## 3. Register Handlers + +In `Program.cs`, register your handlers with the DI container: + +```csharp +using Svrnty.CQRS; +using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.MinimalApi; + +var builder = WebApplication.CreateBuilder(args); + +// Register command and query handlers +builder.Services.AddCommand(); +builder.Services.AddCommand(); +builder.Services.AddQuery(); + +// Configure CQRS with MinimalApi transport +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddMinimalApi(); +}); + +var app = builder.Build(); + +// Map all CQRS endpoints +app.UseSvrntyCqrs(); + +app.Run(); +``` + +This will expose: +- `POST /api/command/CreateUser` -- executes CreateUserCommand +- `POST /api/command/DeleteUser` -- executes DeleteUserCommand +- `POST /api/query/GetUser` -- executes GetUserQuery + +## 4. Add FluentValidation + +Add validators to enforce business rules before handler execution: + +```csharp +using FluentValidation; + +public class CreateUserCommandValidator : AbstractValidator +{ + public CreateUserCommandValidator() + { + RuleFor(x => x.Email) + .NotEmpty().WithMessage("Email is required") + .EmailAddress().WithMessage("Email must be valid"); + + RuleFor(x => x.Name) + .NotEmpty().WithMessage("Name is required"); + + RuleFor(x => x.Age) + .GreaterThan(0).WithMessage("Age must be greater than 0"); + } +} +``` + +Register the command with its validator using the 4-type-parameter overload: + +```csharp +builder.Services.AddCommand(); +``` + +Validation errors are returned as RFC 7807 Problem Details (HTTP) or Google Rich Error Model (gRPC). + +## 5. gRPC Setup + +Add the gRPC packages: + +```bash +dotnet add package Svrnty.CQRS.Grpc +dotnet add package Svrnty.CQRS.Grpc.Generators +dotnet add package Svrnty.CQRS.Grpc.Abstractions +``` + +Configure Kestrel for dual-protocol support and enable gRPC: + +```csharp +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Svrnty.CQRS.Grpc; + +var builder = WebApplication.CreateBuilder(args); + +// Configure dual ports +builder.WebHost.ConfigureKestrel(options => +{ + options.ListenLocalhost(6000, o => o.Protocols = HttpProtocols.Http2); // gRPC + options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); // HTTP API +}); + +// Register handlers (same as before) +builder.Services.AddCommand(); +builder.Services.AddQuery(); + +// Enable both gRPC and MinimalApi +builder.Services.AddSvrntyCqrs(cqrs => +{ + cqrs.AddGrpc(grpc => + { + grpc.EnableReflection(); // Enable gRPC reflection for tools like grpcurl + }); + + cqrs.AddMinimalApi(); +}); + +var app = builder.Build(); +app.UseSvrntyCqrs(); +app.Run(); +``` + +The `Svrnty.CQRS.Grpc.Generators` package automatically generates `.proto` files and gRPC service implementations from your registered command/query types at build time. + +### Excluding Commands from gRPC + +Use the `[GrpcIgnore]` attribute to prevent a command or query from being exposed via gRPC: + +```csharp +using Svrnty.CQRS.Grpc.Abstractions.Attributes; + +[GrpcIgnore] +public record InternalCommand +{ + public string Data { get; set; } = string.Empty; +} +``` + +## 6. DynamicQuery Usage + +Dynamic queries provide automatic filtering, sorting, grouping, and pagination for entity collections. + +Add the packages: + +```bash +dotnet add package Svrnty.CQRS.DynamicQuery +dotnet add package Svrnty.CQRS.DynamicQuery.Abstractions +dotnet add package Svrnty.CQRS.DynamicQuery.MinimalApi +``` + +### Define a Queryable Provider + +Implement `IQueryableProvider` to supply the data source: + +```csharp +using Svrnty.CQRS.DynamicQuery.Abstractions; + +public class UserQueryableProvider : IQueryableProvider +{ + private readonly MyDbContext _db; + + public UserQueryableProvider(MyDbContext db) + { + _db = db; + } + + public Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default) + { + return Task.FromResult(_db.Users.AsQueryable()); + } +} +``` + +### Register the Provider + +```csharp +using Svrnty.CQRS.DynamicQuery; + +// Register PoweredSoft dependencies +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +// Register the dynamic query provider +builder.Services.AddDynamicQueryWithProvider(); +``` + +This exposes a POST endpoint that accepts filter, sort, group, and pagination parameters, returning paged results automatically. + +### Entity Framework Integration + +For EF Core projects, add the EF integration package: + +```bash +dotnet add package Svrnty.CQRS.DynamicQuery.EntityFramework +``` + +This provides a ready-made `IAsyncQueryableService` backed by EF Core. + +## 7. Domain Events + +Domain events allow you to publish side effects after a command completes. + +Add the packages: + +```bash +dotnet add package Svrnty.CQRS.Events.Abstractions +dotnet add package Svrnty.CQRS.Events.RabbitMQ # or implement your own IDomainEventPublisher +``` + +### Define an Event + +```csharp +using Svrnty.CQRS.Events.Abstractions; + +public record UserCreatedEvent : IDomainEvent +{ + public Guid EventId { get; } = Guid.NewGuid(); + public DateTime OccurredAt { get; } = DateTime.UtcNow; + + public int UserId { get; init; } + public string Email { get; init; } = string.Empty; +} +``` + +### Publish from a Command Handler + +```csharp +public class CreateUserCommandHandler : ICommandHandler +{ + private readonly IDomainEventPublisher _events; + + public CreateUserCommandHandler(IDomainEventPublisher events) + { + _events = events; + } + + public async Task HandleAsync(CreateUserCommand command, CancellationToken ct = default) + { + var userId = 123; // persist user + + await _events.PublishAsync(new UserCreatedEvent + { + UserId = userId, + Email = command.Email + }, ct); + + return userId; + } +} +``` + +## 8. Saga Pattern + +Sagas orchestrate multi-step workflows with automatic compensation (rollback) on failure. + +Add the packages: + +```bash +dotnet add package Svrnty.CQRS.Sagas +dotnet add package Svrnty.CQRS.Sagas.Abstractions +dotnet add package Svrnty.CQRS.Sagas.RabbitMQ # for distributed sagas +``` + +### Define Saga Data + +```csharp +using Svrnty.CQRS.Sagas.Abstractions; + +public class CreateOrderSagaData : ISagaData +{ + public Guid CorrelationId { get; set; } + public int OrderId { get; set; } + public int PaymentId { get; set; } + public decimal Amount { get; set; } +} +``` + +### Define a Saga + +```csharp +public class CreateOrderSaga : ISaga +{ + public void Configure(ISagaBuilder builder) + { + builder + .Step("CreateOrder") + .Execute(async (data, ctx, ct) => + { + // Create the order + data.OrderId = 42; + }) + .Compensate(async (data, ctx, ct) => + { + // Cancel the order on rollback + }) + .Then() + + .Step("ProcessPayment") + .Execute(async (data, ctx, ct) => + { + // Charge payment + data.PaymentId = 99; + }) + .Compensate(async (data, ctx, ct) => + { + // Refund payment on rollback + }) + .Then(); + } +} +``` + +### Execute a Saga + +```csharp +using Svrnty.CQRS.Sagas.Abstractions; + +public class OrderCommandHandler : ICommandHandler +{ + private readonly ISagaOrchestrator _orchestrator; + + public OrderCommandHandler(ISagaOrchestrator orchestrator) + { + _orchestrator = orchestrator; + } + + public async Task HandleAsync(PlaceOrderCommand command, CancellationToken ct = default) + { + var state = await _orchestrator.StartAsync( + new CreateOrderSagaData { Amount = command.Amount }, ct); + + // state.Status will be Completed or Compensated + return state.Status == SagaStatus.Completed ? 1 : 0; + } +} +``` + +Saga statuses: `NotStarted` -> `InProgress` -> `Completed` (success) or `Failed` -> `Compensating` -> `Compensated` (rolled back). + +### Remote Steps (Distributed Sagas) + +For steps that execute on remote services via RabbitMQ: + +```csharp +builder + .SendCommand("ChargePayment") + .WithCommand((data, ctx) => new ChargePaymentCommand { Amount = data.Amount }) + .OnResponse(async (data, ctx, result, ct) => + { + data.PaymentId = result.PaymentId; + }) + .Compensate((data, ctx) => + new RefundPaymentCommand { PaymentId = data.PaymentId }) + .WithTimeout(TimeSpan.FromSeconds(30)) + .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(2)) + .Then(); +``` + +## 9. Real-Time Notifications + +For pushing real-time updates to clients via gRPC streaming: + +```bash +dotnet add package Svrnty.CQRS.Notifications.Abstractions +dotnet add package Svrnty.CQRS.Notifications.Grpc +``` + +### Define a Notification + +```csharp +using Svrnty.CQRS.Notifications.Abstractions; + +[StreamingNotification(SubscriptionKey = "user-updates")] +public record UserUpdatedNotification +{ + public int UserId { get; init; } + public string NewEmail { get; init; } = string.Empty; +} +``` + +### Publish a Notification + +```csharp +public class UpdateUserCommandHandler : ICommandHandler +{ + private readonly INotificationPublisher _notifications; + + public UpdateUserCommandHandler(INotificationPublisher notifications) + { + _notifications = notifications; + } + + public async Task HandleAsync(UpdateUserCommand command, CancellationToken ct = default) + { + // Update user... + + await _notifications.PublishAsync(new UserUpdatedNotification + { + UserId = command.UserId, + NewEmail = command.NewEmail + }, ct); + } +} +``` + +## Running the Sample App + +The repository includes a complete sample application: + +```bash +cd Svrnty.Sample +dotnet run +``` + +This starts: +- gRPC server on `http://localhost:6000` (HTTP/2) +- HTTP API on `http://localhost:6001` (HTTP/1.1) +- Swagger UI at `http://localhost:6001/swagger` + +The sample demonstrates commands with validation, queries, gRPC reflection, MinimalApi endpoints, and dynamic queries. diff --git a/docs/PACKAGE_INDEX.md b/docs/PACKAGE_INDEX.md new file mode 100644 index 0000000..f309d91 --- /dev/null +++ b/docs/PACKAGE_INDEX.md @@ -0,0 +1,335 @@ +# Package Index + +> Complete reference for all 18 NuGet packages in the Svrnty.CQRS framework. + +## Overview + +| # | Package | Path | NuGet Package | +|---|---------|------|:---:| +| 1 | [Svrnty.CQRS.Abstractions](#1-svrntycqrsabstractions) | `Svrnty.CQRS.Abstractions/` | Yes | +| 2 | [Svrnty.CQRS](#2-svrntycqrs) | `Svrnty.CQRS/` | Yes | +| 3 | [Svrnty.CQRS.MinimalApi](#3-svrntycqrsminimalapi) | `Svrnty.CQRS.MinimalApi/` | Yes | +| 4 | [Svrnty.CQRS.Grpc](#4-svrntycqrsgrpc) | `Svrnty.CQRS.Grpc/` | Yes | +| 5 | [Svrnty.CQRS.Grpc.Abstractions](#5-svrntycqrsgrpcabstractions) | `Svrnty.CQRS.Grpc.Abstractions/` | Yes | +| 6 | [Svrnty.CQRS.Grpc.Generators](#6-svrntycqrsgrpcgenerators) | `Svrnty.CQRS.Grpc.Generators/` | Yes | +| 7 | [Svrnty.CQRS.FluentValidation](#7-svrntycqrsfluentvalidation) | `Svrnty.CQRS.FluentValidation/` | Yes | +| 8 | [Svrnty.CQRS.DynamicQuery.Abstractions](#8-svrntycqrsdynamicqueryabstractions) | `Svrnty.CQRS.DynamicQuery.Abstractions/` | Yes | +| 9 | [Svrnty.CQRS.DynamicQuery](#9-svrntycqrsdynamicquery) | `Svrnty.CQRS.DynamicQuery/` | Yes | +| 10 | [Svrnty.CQRS.DynamicQuery.MinimalApi](#10-svrntycqrsdynamicqueryminimalapi) | `Svrnty.CQRS.DynamicQuery.MinimalApi/` | Yes | +| 11 | [Svrnty.CQRS.DynamicQuery.EntityFramework](#11-svrntycqrsdynamicqueryentityframework) | `Svrnty.CQRS.DynamicQuery.EntityFramework/` | Yes | +| 12 | [Svrnty.CQRS.Events.Abstractions](#12-svrntycqrseventsabstractions) | `Svrnty.CQRS.Events.Abstractions/` | Yes | +| 13 | [Svrnty.CQRS.Events.RabbitMQ](#13-svrntycqrseventsrabbitmq) | `Svrnty.CQRS.Events.RabbitMQ/` | Yes | +| 14 | [Svrnty.CQRS.Sagas.Abstractions](#14-svrntycqrssagasabstractions) | `Svrnty.CQRS.Sagas.Abstractions/` | Yes | +| 15 | [Svrnty.CQRS.Sagas](#15-svrntycqrssagas) | `Svrnty.CQRS.Sagas/` | Yes | +| 16 | [Svrnty.CQRS.Sagas.RabbitMQ](#16-svrntycqrssagasrabbitmq) | `Svrnty.CQRS.Sagas.RabbitMQ/` | Yes | +| 17 | [Svrnty.CQRS.Notifications.Abstractions](#17-svrntycqrsnotificationsabstractions) | `Svrnty.CQRS.Notifications.Abstractions/` | Yes | +| 18 | [Svrnty.CQRS.Notifications.Grpc](#18-svrntycqrsnotificationsgrpc) | `Svrnty.CQRS.Notifications.Grpc/` | Yes | + +--- + +## Package Details + +### 1. Svrnty.CQRS.Abstractions + +**Purpose**: Core interfaces that define the CQRS contract. This is the only package your domain/application layer needs to reference. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `ICommandHandler` -- Handler for commands with no return value +- `ICommandHandler` -- Handler for commands returning a result +- `IQueryHandler` -- Handler for queries +- `ICommandMeta` / `IQueryMeta` -- Discovery metadata +- `ICommandDiscovery` / `IQueryDiscovery` -- Service discovery interfaces +- `ICommandAuthorizationService` -- Per-command authorization +- `IQueryAuthorizationService` -- Per-query authorization +- `CommandNameAttribute` / `QueryNameAttribute` -- Custom naming +- `IgnoreCommandAttribute` / `IgnoreQueryAttribute` -- Exclude from auto-discovery + +**Internal Dependencies**: None + +--- + +### 2. Svrnty.CQRS + +**Purpose**: Core registration and discovery engine. Provides the `AddSvrntyCqrs()` fluent API and auto-discovers registered handlers. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `CqrsBuilder` -- Fluent builder for configuring transports and features +- `CqrsConfiguration` -- Configuration state +- `ServiceCollectionExtensions.AddSvrntyCqrs()` -- Entry point +- `ServiceCollectionExtensions.AddCommand()` -- Register a command handler +- `ServiceCollectionExtensions.AddQuery()` -- Register a query handler +- `CommandDiscovery` / `QueryDiscovery` -- Default discovery implementations + +**Internal Dependencies**: `Svrnty.CQRS.Abstractions` + +--- + +### 3. Svrnty.CQRS.MinimalApi + +**Purpose**: Maps registered commands and queries to ASP.NET Core Minimal API HTTP endpoints. Includes RFC 7807 Problem Details for validation errors. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- `CqrsBuilderExtensions.AddMinimalApi()` -- Enable HTTP endpoints +- `MinimalApiCqrsOptions` -- Configuration (route prefixes, etc.) +- `ValidationFilter` -- Endpoint filter for FluentValidation +- `WebApplicationExtensions.UseSvrntyCqrs()` -- Map endpoints at startup +- `EndpointRouteBuilderExtensions` -- Route mapping helpers + +**Internal Dependencies**: `Svrnty.CQRS.Abstractions`, `Svrnty.CQRS` + +**External Dependencies**: `FluentValidation 11.x`, `Microsoft.AspNetCore.App` + +--- + +### 4. Svrnty.CQRS.Grpc + +**Purpose**: Maps registered commands and queries to gRPC services. Uses Google Rich Error Model for structured validation errors. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- `CqrsBuilderExtensions.AddGrpc()` -- Enable gRPC endpoints +- `GrpcCqrsOptions` -- Configuration (reflection, etc.) + +**Internal Dependencies**: `Svrnty.CQRS` + +**External Dependencies**: `Grpc.AspNetCore 2.71.0` + +--- + +### 5. Svrnty.CQRS.Grpc.Abstractions + +**Purpose**: Attributes for controlling gRPC code generation behavior. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `GrpcIgnoreAttribute` -- Marks a command/query to be excluded from gRPC service generation + +**Internal Dependencies**: None + +--- + +### 6. Svrnty.CQRS.Grpc.Generators + +**Purpose**: Roslyn source generator that auto-generates `.proto` files and gRPC service implementations from registered command/query types. + +**Target**: `netstandard2.0` (Roslyn component) | **AOT**: N/A + +**Key Types**: +- Source generator (analyzer DLL) +- MSBuild `WriteProtoFileTask` -- Writes generated `.proto` files to disk +- Build targets and props for NuGet consumers + +**Internal Dependencies**: None (ships as analyzer) + +**External Dependencies**: `Microsoft.CodeAnalysis.CSharp 5.0.0`, `Microsoft.Build.Utilities.Core 17.0.0` + +--- + +### 7. Svrnty.CQRS.FluentValidation + +**Purpose**: Integrates FluentValidation with command/query registration. Validators are automatically invoked before handler execution. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `ServiceCollectionExtensions.AddCommand()` -- Register command with validator +- Automatic `AbstractValidator` binding + +**Internal Dependencies**: `Svrnty.CQRS`, `Svrnty.CQRS.Abstractions` + +**External Dependencies**: `FluentValidation 11.11.0` + +--- + +### 8. Svrnty.CQRS.DynamicQuery.Abstractions + +**Purpose**: Interfaces for the dynamic query subsystem. Defines how data sources are provided and queries are intercepted. + +**Target**: `netstandard2.1`, `net10.0` (multi-target) | **AOT**: Conditional + +**Key Types**: +- `IQueryableProvider` -- Provides an `IQueryable` data source +- `IQueryableProviderOverride` -- Override default provider +- `IAlterQueryableService` -- Intercept/modify queryables +- `IDynamicQuery` / `IDynamicQueryParams` -- Query parameter contracts +- `IDynamicQueryInterceptorProvider` -- Interceptor registration + +**Internal Dependencies**: None + +**External Dependencies**: `PoweredSoft.DynamicQuery.Core 3.0.1` + +--- + +### 9. Svrnty.CQRS.DynamicQuery + +**Purpose**: Implementation of dynamic query execution with filtering, sorting, grouping, pagination, and aggregation. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `ServiceCollectionExtensions.AddDynamicQueryWithProvider()` -- Register a queryable provider +- Dynamic query handler pipeline + +**Internal Dependencies**: `Svrnty.CQRS.DynamicQuery.Abstractions`, `Svrnty.CQRS` + +**External Dependencies**: `PoweredSoft.DynamicQuery 3.0.1`, `Pluralize.NET 1.0.2` + +--- + +### 10. Svrnty.CQRS.DynamicQuery.MinimalApi + +**Purpose**: HTTP Minimal API endpoints for dynamic queries. Exposes each registered entity as a POST endpoint with filter/sort/page parameters. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- Endpoint mapping for dynamic query routes (`/api/dynamic-query/{entity}`) + +**Internal Dependencies**: `Svrnty.CQRS.Abstractions`, `Svrnty.CQRS.DynamicQuery.Abstractions`, `Svrnty.CQRS.DynamicQuery` + +**External Dependencies**: `Microsoft.AspNetCore.App` + +--- + +### 11. Svrnty.CQRS.DynamicQuery.EntityFramework + +**Purpose**: Entity Framework Core integration for dynamic queries. Provides an EF-backed `IAsyncQueryableService`. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- EF Core queryable service adapter + +**Internal Dependencies**: `Svrnty.CQRS.DynamicQuery` + +**External Dependencies**: `PoweredSoft.Data.EntityFrameworkCore 3.0.0` + +--- + +### 12. Svrnty.CQRS.Events.Abstractions + +**Purpose**: Interfaces for domain event publishing. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `IDomainEvent` -- Marker interface (EventId, OccurredAt) +- `IDomainEventPublisher` -- Publish events to external systems + +**Internal Dependencies**: None + +--- + +### 13. Svrnty.CQRS.Events.RabbitMQ + +**Purpose**: RabbitMQ-backed implementation of domain event publishing. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- RabbitMQ event publisher implementation + +**Internal Dependencies**: `Svrnty.CQRS.Events.Abstractions` + +**External Dependencies**: `RabbitMQ.Client 7.0.0`, `Microsoft.Extensions.DependencyInjection.Abstractions`, `Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.Options` + +--- + +### 14. Svrnty.CQRS.Sagas.Abstractions + +**Purpose**: Interfaces and types for the saga orchestration pattern with compensation (rollback) support. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `ISaga` -- Define a saga with steps +- `ISagaBuilder` -- Fluent builder for local and remote steps +- `ISagaStepBuilder` -- Configure Execute/Compensate actions +- `ISagaRemoteStepBuilder` -- Remote command steps with timeout/retry +- `ISagaOrchestrator` -- Start sagas, query state +- `ISagaData` -- Marker interface (CorrelationId) +- `SagaState` -- Persistent saga state (status, completed steps, errors) +- `SagaStatus` -- Enum: NotStarted, InProgress, Completed, Failed, Compensating, Compensated +- `ISagaStateStore` -- Persistence abstraction +- `ISagaMessageBus` -- Messaging abstraction +- `SagaMessage` / `SagaStepResponse` -- Message types +- `ISagaContext` -- Step execution context + +**Internal Dependencies**: None + +--- + +### 15. Svrnty.CQRS.Sagas + +**Purpose**: Default saga orchestrator implementation with step execution, compensation, and state management. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- Saga orchestrator engine +- In-memory state store (default) + +**Internal Dependencies**: `Svrnty.CQRS`, `Svrnty.CQRS.Sagas.Abstractions` + +**External Dependencies**: `Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.Options` + +--- + +### 16. Svrnty.CQRS.Sagas.RabbitMQ + +**Purpose**: RabbitMQ-backed message bus for distributed saga step execution across microservices. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- RabbitMQ saga message bus implementation + +**Internal Dependencies**: `Svrnty.CQRS.Sagas` + +**External Dependencies**: `RabbitMQ.Client 7.0.0`, `Microsoft.Extensions.Hosting.Abstractions`, `Microsoft.Extensions.Options` + +--- + +### 17. Svrnty.CQRS.Notifications.Abstractions + +**Purpose**: Interfaces for real-time notification streaming to clients. + +**Target**: `net10.0` | **AOT**: Yes + +**Key Types**: +- `INotificationPublisher` -- Publish notifications to subscribed clients +- `StreamingNotificationAttribute` -- Marks a type as a streamable notification with a subscription key + +**Internal Dependencies**: None + +--- + +### 18. Svrnty.CQRS.Notifications.Grpc + +**Purpose**: gRPC server-streaming implementation for real-time notifications. + +**Target**: `net10.0` | **AOT**: No + +**Key Types**: +- gRPC notification streaming service + +**Internal Dependencies**: `Svrnty.CQRS.Notifications.Abstractions` + +**External Dependencies**: `Grpc.AspNetCore 2.71.0`, `Microsoft.Extensions.DependencyInjection.Abstractions`, `Microsoft.Extensions.Logging.Abstractions` + +--- + +## Additional Projects (not NuGet packages) + +| Project | Path | Purpose | +|---------|------|---------| +| `Svrnty.Sample` | `Svrnty.Sample/` | Sample web application demonstrating commands, queries, gRPC, MinimalApi, DynamicQuery, and validation | +| `Svrnty.CQRS.Tests` | `tests/Svrnty.CQRS.Tests/` | Unit and integration test suite |