Masterclass: CQRS Pattern, .NET 8 Setup, & Clean Architecture
Architecture Objective
Implementing a Command Query Responsibility Segregation (CQRS) pipeline using MediatR decouples write operations (Commands) from read operations (Queries), establishing a production-grade foundation for distributed e-commerce microservices.
1. What is CQRS and Why Do We Use It?
Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates the data mutation model (Commands) from the data retrieval model (Queries).
Traditional vs. CQRS Architecture
In a standard CRUD architecture, the same data model, repository, and database schema handle both reading and writing. As an application scales, this creates friction:
- Asymmetric Scaling: Read traffic in e-commerce (e.g., product browsing, order status checking) vastly outweighs write traffic (e.g., placing an order). CQRS allows you to scale read and write databases independently.
- Complex Domain Logic: Write operations require strict business validations, invariants, and aggregate consistency. Read operations often require denormalized, flattened projections optimized for UI rendering.
- Decoupled Handlers: By encapsulating requests into self-contained message objects, frameworks like MediatR eliminate tightly coupled controller-to-service dependencies.
2. Implementation Walkthrough
A. The Directory Structure
Following Clean Architecture principles, our solution separates responsibilities into logical layers:
OrderService/
├── Application/ # CQRS Handlers, Commands, DTOs (MediatR)
├── Controllers/ # Presentation layer (API Endpoints)
├── Domain/ # Core entities, value objects, domain events
└── Infrastructure/ # EF Core persistence, External integrations (SNS/SQS)
B. Defining the Command (Write Model)
Commands represent an intent to change system state. They are immutable C# record types implementing MediatR’s IRequest<TResponse> interface.
Benefits of using Records ?
TODO
using MediatR;
namespace OrderService.Application.Command;
public record CreateOrderCommand(Guid CustomerId, decimal TotalAmount) : IRequest<Guid>;C. Implementing the Command Handler
The handler contains the isolated business logic for processing the command. With C# 12 primary constructors, dependencies are injected cleanly into the class signature.
using MediatR;
namespace OrderService.Application.Command;
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// 1. Generate Aggregate ID
var orderId = Guid.NewGuid();
// 2. Execute core domain business logic & persistence (to be expanded with EF Core)
// 3. Return the generated identifier
return await Task.FromResult(orderId);
}
}D. Exposing the Endpoint via Minimal APIs & MediatR
In Program.cs, we register MediatR and wire up an endpoint that delegates incoming requests directly to the mediator pipeline:
using MediatR;
using OrderService.Application.Command;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register MediatR handlers from the current assembly
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Map POST endpoint for order creation
app.MapPost("/api/orders", async (IMediator mediator, CreateOrderCommand command) =>
{
var orderId = await mediator.Send(command);
return Results.Created($"/api/orders/{orderId}", new { orderId });
});
app.Run();3. Request Execution Flow
The following Mermaid sequence diagram illustrates how an HTTP POST request traverses through the API layer, invokes MediatR, executes the handler, and returns a response.
sequenceDiagram autonumber actor Client participant API as Program.cs (Minimal API) participant Mediator as MediatR Dispatcher participant Handler as CreateOrderCommandHandler Client->>API: POST /api/orders (JSON Payload) API->>Mediator: mediator.Send(CreateOrderCommand) Mediator->>Handler: Handle(CreateOrderCommand, CancellationToken) Handler-->>Mediator: Returns Guid (OrderId) Mediator-->>API: Returns Guid API-->>Client: 201 Created (/api/orders/{orderId})
4. Key Takeaways & Best Practices
Immutability Rule
Always define commands and queries as C#
recordtypes. Immutability prevents unintended state mutation as messages flow through the pipeline middlewares.
Testing with
.httpFilesUtilize Rider’s built-in
.httpscratch files or Swagger UI to execute and validate your endpoints instantly without external dependencies.
Q&A
Q1 : What are these codes doing :
public record CreateOrderCommand(Guid CustomerId, decimal TotalAmount) : IRequest<Guid>;public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>{
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
return await Task.FromResult(orderId); The two code snippets work together as the Write Model (Command) and its corresponding Handler in a CQRS pattern using MediatR.
1. The Command
public record CreateOrderCommand(Guid CustomerId, decimal TotalAmount) : IRequest<Guid>;-
C# Record (
record): A specialized, immutable reference type optimized for holding data. Once instantiated, its properties (CustomerIdandTotalAmount) cannot be changed, ensuring thread safety and data integrity as the request flows through the pipeline. -
IRequest<Guid>: A marker interface from the MediatR library. It tells the framework: ”This object is a request that will mutate state, and when handled, it will return aGuid(the newly generated Order ID).” -
Java Analog: This is structurally similar to an immutable DTO or a command payload class in a CQRS-based Java application (like a Spring command object).
2. The Command Handler
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var orderId = Guid.NewGuid();
return await Task.FromResult(orderId);
}
}-
IRequestHandler<CreateOrderCommand, Guid>: The MediatR interface that binds the command to its execution logic. It expects aHandlemethod matching the request type and return type. -
Handle(...)method: The entry point executed automatically by MediatR whenmediator.Send(command)is called. It receives the immutable command record containing the input data. -
Asynchronous Execution (
async/Task): .NET uses Tasks for asynchronous programming (similar to Java’sCompletableFutureor reactive monos).Task.FromResultwraps the synchronousGuidinto an asynchronous task as a placeholder until you plug in asynchronous database operations like Entity Framework Core.
Q2: CreateOrderCommand is a record (which is immutable), so how does it mutate state?
The command record itself does not mutate state directly—its immutability is actually a feature, not a limitation.
-
The Command as a Message: A command is a data-transfer object (DTO) that represents an intent or a payload (e.g., “Create an order for this customer with this amount”).
-
The Role of Immutability: Because commands are immutable (
record), they safely transport data across threads and layers without risk of being accidentally modified mid-flight by other components. -
Where the Mutation Happens: The immutable command is passed to the Command Handler. The handler extracts the data from the command, applies the business rules, creates/modifies a mutable domain entity (like an
Orderaggregate), and saves those changes to the database via Entity Framework Core.
Q3: Why is the output of Handle specified as Task<Guid> instead of Task<OrderId>?
The return type depends on whether you choose to use a primitive type or a strongly-typed ID pattern in your domain model:
-
Why
Guid? In our current code,IRequest<Guid>specifies that the handler returns a rawGuidprimitive (the underlying unique identifier of the newly created order).Task<Guid>is simply the asynchronous wrapper required by C# tasks around thatGuidvalue. -
Why not
OrderId? In advanced Domain-Driven Design (DDD), developers often create a strongly-typed ID wrapper (e.g.,public record OrderId(Guid Value);) to prevent bugs like accidentally passing aCustomerIdinto a method expecting anOrderId. If you define a strongly-typedOrderIdrecord, your signature would change toIRequest<OrderId>andTask<OrderId>. Returning a rawGuidis simpler for initial API scaffolding, while a strongly-typed ID offers stricter compile-time type safety.
Q4 : Record ‘CreateOrderCommand’ is never instantiated

This warning appears because Rider’s static code analysis scans your codebase and notices that your C# code never explicitly calls new CreateOrderCommand(...).
Why This Happens
-
Framework Magic: Your
CreateOrderCommandis instantiated dynamically behind the scenes. When an incoming HTTP POST request hits/api/orders, ASP.NET Core’s built-in model binder reads the incoming JSON payload, matches the properties (customerId,totalAmount), and instantiates the record via reflection before handing it to MediatR. -
The Blind Spot: Because ASP.NET Core and MediatR handle this instantiation using runtime reflection rather than explicit C# code, Rider’s static analyzer cannot “see” that it’s being used, prompting the warning.
How to Fix or Suppress It
-
Ignore it safely: You can completely disregard this warning. It is just an IDE hint and will not prevent your application from compiling or running successfully.
-
Suppress it with an attribute: If the green squiggly line bothers you, you can decorate the record with the
[UsedImplicitly]attribute from JetBrains Annotations so Rider knows the framework is instantiating it:using JetBrains.Annotations; using MediatR; namespace OrderService.Application.Command; [UsedImplicitly] public record CreateOrderCommand(Guid CustomerId, decimal TotalAmount) : IRequest<Guid>;(Note: This requires the
JetBrains.AnnotationsNuGet package, so letting Rider ignore it or leaving it as-is is usually the simplest path).
Q4.1 is this issue ide inpendnet ? do we need to instantiate record too ? what is UsedImplicitly ?
1. Is this issue IDE-independent?
No, this specific warning is IDE-dependent. It is generated by JetBrains Rider’s (and ReSharper’s) aggressive static analysis engine, which scans your codebase for unused types. If you open the exact same project in Visual Studio Code (using the standard C# extension) or Visual Studio, you typically will not see this warning because standard Roslyn analyzers don’t flag DTOs or command records as “unused” when they match framework routing patterns.
2. Do we need to instantiate a record manually?
No, you do not need to manually write new CreateOrderCommand(...) in your application code.
-
ASP.NET Core’s request pipeline handles the instantiation automatically via model binding when an HTTP POST request comes in with JSON data.
-
MediatR then receives that instantiated object. You would only ever manually instantiate a command if you were writing a unit test for your handler.
3. What is [UsedImplicitly]?
[UsedImplicitly] is an attribute provided by JetBrains (JetBrains.Annotations).
-
What it does: It acts as a hint or instruction to Rider’s static analyzer. It tells the IDE: “Even though you don’t see any explicit C# code instantiating this record, trust me—it is being used implicitly by an external framework (like ASP.NET Core reflection or MediatR).”
-
Why use it: Applying this attribute suppresses the false-positive green squiggly warning so your error/warning panel stays clean.