PoC .NET - CQRS
π¬ Proof of Concept of CQRS pattern in .NET using RabbitMQ, ReBus, State Machine, MediatR and Docker
Proof of Concept demonstrating the CQRS pattern in .NET 10 using MediatR, Rebus, RabbitMQ, Stateless State Machine, EF Core, and Docker.
Table of Contents
- Architecture Overview
- Tech Stack
- Project Structure
- Order Lifecycle β State Machine
- CQRS Flow
- Getting Started
- API Reference
- Design Decisions
Architecture Overview
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Client β
βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β REST
βββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β CqrsPoC.API β
β OrdersController β IMediator β
ββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ-β
β Commands β Queries
ββββββββββΌβββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β CqrsPoC.Application β
β β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββ β
β β Command Handlers β β Query Handlers β β
β β CreateOrder β β GetOrderQueryHandler β β
β β ConfirmOrder β β GetAllOrdersQueryHandler β β
β β ShipOrder β ββββββββββββββββ¬βββββββββββββββ β
β β CompleteOrder β β β
β β CancelOrder β ββββββββββββββββΌβββββββββββββββ β
β ββββββββββ¬βββββββββββββ β IOrderRepository (read) β β
β β βββββββββββββββββββββββββββββββ β
β β IEventPublisher β
β ββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββ β
β β LoggingBehavior (pipeline) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CqrsPoC.Infrastructure β
β β
β ββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ β
β β AppDbContext β β RebusEventPublisher β β
β β (EF Core InMemory) β β β IBus (Rebus) β β
β β OrderRepository β β β RabbitMQ exchange β β
β ββββββββββββββββββββββββ ββββββββββββββββ¬βββββββββββββββββ β
β β publishes β
β ββββββββββββββββΌβββββββββββββββββ β
β β Event Handlers (Rebus subs) β β
β β OrderCreatedEventHandler β β
β β OrderConfirmedEventHandler β β
β β OrderShippedEventHandler β β
β β OrderCompletedEventHandler β β
β β OrderCancelledEventHandler β β
β βββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CqrsPoC.Domain β
β β
β Order (Aggregate Root) β
β ββ Stateless State Machine β
β Pending β Confirmed β Shipped β Completed β
β Pending/Confirmed β Cancelled β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tech Stack
| Concern | Library / Tool | Version |
|---|---|---|
| Framework | .NET / ASP.NET Core | 10.0 |
| CQRS Mediator | MediatR | 12.x |
| Message Bus | Rebus | 8.x |
| Message Transport | Rebus.RabbitMq (RabbitMQ) | 10.x |
| State Machine | Stateless | 5.x |
| ORM / Persistence | EF Core (InMemory for PoC) | 10.0 |
| API Docs | Swashbuckle / Swagger | 7.x |
| Containerisation | Docker + Docker Compose | β |
Project Structure
CqrsPoC/
βββ CqrsPoC.sln
βββ Dockerfile
βββ docker-compose.yml
βββ Src/
βββ CqrsPoC.Contracts/ # Shared integration event records
β βββ Events/
β βββ OrderCreatedEvent.cs
β βββ OrderConfirmedEvent.cs
β βββ OrderShippedEvent.cs
β βββ OrderCompletedEvent.cs
β βββ OrderCancelledEvent.cs
β
βββ CqrsPoC.Domain/ # Pure domain β no framework deps
β βββ Entities/
β β βββ Order.cs β Aggregate root + state machine
β βββ Enums/
β β βββ OrderState.cs
β β βββ OrderTrigger.cs
β βββ Exceptions/
β βββ DomainException.cs
β βββ OrderNotFoundException.cs
β
βββ CqrsPoC.Application/ # Use-cases, CQRS handlers
β βββ Commands/
β β βββ CreateOrder/
β β βββ ConfirmOrder/
β β βββ ShipOrder/
β β βββ CompleteOrder/
β β βββ CancelOrder/
β βββ Queries/
β β βββ GetOrder/
β β βββ GetAllOrders/
β βββ Behaviors/
β β βββ LoggingBehavior.cs β MediatR pipeline (cross-cutting)
β βββ Interfaces/
β β βββ IOrderRepository.cs
β β βββ IEventPublisher.cs
β βββ DependencyInjection.cs
β
βββ CqrsPoC.Infrastructure/ # EF Core + Rebus implementations
β βββ Persistence/
β β βββ AppDbContext.cs
β β βββ Repositories/
β β βββ OrderRepository.cs
β βββ Messaging/
β β βββ RebusEventPublisher.cs
β β βββ Handlers/
β β βββ OrderCreatedEventHandler.cs
β β βββ OrderConfirmedEventHandler.cs
β β βββ OrderShippedEventHandler.cs
β β βββ OrderCompletedEventHandler.cs
β β βββ OrderCancelledEventHandler.cs
β βββ DependencyInjection.cs
β
βββ CqrsPoC.API/ # HTTP entry point
βββ Controllers/
β βββ OrdersController.cs
βββ Program.cs
βββ appsettings.json
βββ appsettings.Development.json
Order Lifecycle β State Machine
The Order aggregate embeds a Stateless state machine that enforces
all valid lifecycle transitions at the domain level. Invalid transitions
throw a DomainException, which the API maps to 400 Bad Request.
βββββββββββ
β Pending β ββββ Initial state on creation
ββββββ¬βββββ
[confirm] β [cancel]
βββββββββββββββββββββββββββ
βΌ βΌ
βββββββββββββ βββββββββββββ
β Confirmed β β Cancelled β (terminal)
βββββββ¬ββββββ βββββββββββββ
[ship] β [cancel] β²
ββββββββββββββββββββββββββ
βΌ
ββββββββββ
βShipped β
ββββββ¬ββββ
[complete] β
βΌ
βββββββββββββ
β Completed β (terminal)
βββββββββββββ
Each OrderDto response includes a permittedTriggers array so clients
always know which transitions are available in the current state.
CQRS Flow
Command flow (write side)
HTTP PUT /api/orders/{id}/confirm
βββΊ OrdersController.Confirm()
βββΊ IMediator.Send(ConfirmOrderCommand)
βββΊ LoggingBehavior (pipeline)
βββΊ ConfirmOrderCommandHandler
βββΊ IOrderRepository.GetByIdAsync()
βββΊ order.Confirm() β state machine fires
βββΊ IOrderRepository.SaveChangesAsync()
βββΊ IEventPublisher.PublishAsync(OrderConfirmedEvent)
βββΊ Rebus IBus.Publish()
βββΊ RabbitMQ exchange
βββΊ OrderConfirmedEventHandler (subscriber)
Query flow (read side)
HTTP GET /api/orders/{id}
βββΊ OrdersController.GetById()
βββΊ IMediator.Send(GetOrderQuery)
βββΊ LoggingBehavior (pipeline)
βββΊ GetOrderQueryHandler
βββΊ IOrderRepository.GetByIdAsync()
βββΊ OrderDto (projection)
Getting Started
Prerequisites
- Docker Desktop (or Docker + Compose plugin)
- .NET 10 SDK β only needed for local development without Docker
Run with Docker Compose
# Clone / navigate to the repo root
git clone <repo-url>
cd CqrsPoC
# Build and start both services (RabbitMQ + API)
docker compose up --build
# API Swagger UI β http://localhost:8080
# RabbitMQ UI β http://localhost:15672 (guest / guest)
To stop and remove containers:
docker compose down -v
Run Locally (without Docker)
- Start RabbitMQ via Docker:
docker run -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:3.13-management-alpine
- Run the API:
cd Src/CqrsPoC.API
dotnet run
# Swagger UI β https://localhost:5001 (or check the console output)
API Reference
| Method | Endpoint | Description | Transition |
|---|---|---|---|
GET |
/api/orders |
List all orders | β |
GET |
/api/orders/{id} |
Get a single order | β |
POST |
/api/orders |
Create a new order | β Pending |
PUT |
/api/orders/{id}/confirm |
Confirm a pending order | Pending β Confirmed |
PUT |
/api/orders/{id}/ship |
Ship a confirmed order | Confirmed β Shipped |
PUT |
/api/orders/{id}/complete |
Complete a shipped order | Shipped β Completed |
PUT |
/api/orders/{id}/cancel |
Cancel a pending/confirmed order | β Cancelled |
Example: Create Order
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerName": "Guilherme",
"productName": "Mechanical Keyboard",
"amount": 149.99
}'
# β 201 Created { "id": "3fa85f64-..." }
Example: Full lifecycle
ID="<paste-id-from-create>"
curl -X PUT http://localhost:8080/api/orders/$ID/confirm
curl -X PUT http://localhost:8080/api/orders/$ID/ship
curl -X PUT http://localhost:8080/api/orders/$ID/complete
# Check final state
curl http://localhost:8080/api/orders/$ID
Error responses
Domain and transition errors return RFC 7807 Problem Details:
{
"status": 400,
"title": "DomainException",
"detail": "Cannot apply trigger 'Ship' when order is in state 'Pending'."
}
Design Decisions
Clean Architecture layers
Dependencies flow inward: API β Application β Domain.
Infrastructure implements interfaces defined in Application β so the
domain and use-cases have zero framework dependencies.
MediatR pipeline behaviours
LoggingBehavior<TRequest,TResponse> is registered as an open generic
pipeline behaviour, giving structured logging + timing for every
Command and Query without touching individual handlers.
Rebus + RabbitMQ
Rebus acts as the integration event bus. After a command mutates state,
the handler publishes a typed event record (IEventPublisher), which Rebus
routes to RabbitMQ. Subscribers (also in Infrastructure) react asynchronously β
decoupling side-effects from the command path.
The IEventPublisher abstraction in the Application layer means handlers
never reference Rebus directly, keeping the transport swappable.
Stateless state machine inside the aggregate
The state machine lives inside the Order aggregate as a private field.
It is rebuilt on every instantiation (including EF Core hydration), reads the
persisted State column, and mutates it only through domain methods
(Confirm(), Ship(), etc.).
This keeps the machine as an enforcement mechanism β not just documentation.
EF Core InMemory
Used for simplicity in this PoC. Swap UseInMemoryDatabase for
UseSqlServer / UseNpgsql / etc. in Infrastructure/DependencyInjection.cs
and add a migration to go production-ready.
Testing
The solution contains three test projects under the /Tests folder, covering the full testing pyramid.
Tests/
βββ CqrsPoC.Tests.Unit/ # Fast, isolated β no I/O, pure logic
β βββ Domain/
β β βββ OrderStateMachineTests.cs (26 tests)
β βββ Application/
β βββ Commands/CommandHandlerTests.cs (17 tests)
β βββ Queries/QueryHandlerTests.cs (8 tests)
β βββ Behaviors/LoggingBehaviorTests.cs (4 tests)
β
βββ CqrsPoC.Tests.Integration/ # Real EF Core + real Rebus (in-memory transport)
β βββ Persistence/
β β βββ OrderRepositoryTests.cs (8 tests)
β βββ Messaging/
β β βββ RebusEventHandlerTests.cs (5 tests)
β βββ Infrastructure/
β βββ ApplicationPipelineTests.cs (10 tests)
β
βββ CqrsPoC.Tests.E2E/ # Full HTTP stack via WebApplicationFactory
βββ Endpoints/
βββ OrdersApiTests.cs (20 tests)
Run all tests
dotnet test
Run by project
# Unit only
dotnet test Tests/CqrsPoC.Tests.Unit
# Integration only
dotnet test Tests/CqrsPoC.Tests.Integration
# E2E only
dotnet test Tests/CqrsPoC.Tests.E2E
Test strategy
| Layer | Whatβs tested | Mocked |
|---|---|---|
| Unit | Domain state machine transitions, command/query handler orchestration, MediatR pipeline behaviour | IOrderRepository, IEventPublisher |
| Integration | EF Core repository CRUD, Rebus in-memory event delivery, full MediatR+handler pipeline | IEventPublisher only (no RabbitMQ) |
| E2E | All HTTP endpoints, status codes, response bodies, Problem Details errors, full lifecycle | IEventPublisher (Moq), RabbitMQ (InMemory Rebus transport), EF Core (InMemory DB) |