Commands and Handlers with MediatR
Dispatch requests to dedicated handlers.
Commands and Handlers with MediatR is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Registering MediatR
MediatR scans your assemblies to discover handlers. Register it once at startup, pointing it at an assembly that contains your handlers.
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));IRequest
A message that expects a response implements IRequest<TResponse>. The type parameter is what the handler returns.
public record CreateOrderCommand(int CustomerId, string[] Items)
: IRequest<int>; // returns the new order idCommands Without a Result
If a command returns nothing meaningful, implement the non-generic IRequest (which is shorthand for IRequest<Unit>).
public record CancelOrderCommand(int OrderId) : IRequest;IRequestHandler
The handler implements IRequestHandler<TRequest, TResponse> and provides the Handle method where the work happens.
public class CreateOrderHandler
: IRequestHandler<CreateOrderCommand, int>
{
public Task<int> Handle(
CreateOrderCommand request, CancellationToken ct)
{
// create the order...
return Task.FromResult(newOrderId);
}
}Injecting Dependencies
Handlers are resolved from DI, so they can take a database context, repositories or other services through the constructor.
public class CreateOrderHandler(AppDbContext db)
: IRequestHandler<CreateOrderCommand, int>
{
public async Task<int> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = new Order { CustomerId = request.CustomerId };
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return order.Id;
}
}ISender
To dispatch a request, inject ISender (or the broader IMediator) and call Send. MediatR finds the matching handler.
public class OrdersController(ISender sender) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand cmd)
{
int id = await sender.Send(cmd);
return CreatedAtAction(nameof(Get), new { id }, null);
}
}Send in Minimal APIs
Minimal APIs inject ISender directly into the endpoint delegate.
app.MapPost("/orders", async (
CreateOrderCommand cmd, ISender sender) =>
{
var id = await sender.Send(cmd);
return Results.Created($"/orders/{id}", new { id });
});Queries Are Just Requests
Queries use the same IRequest / IRequestHandler machinery - the only difference is intent: the handler reads and never mutates.
public record GetOrderQuery(int Id) : IRequest<OrderDto?>;
public class GetOrderHandler(AppDbContext db)
: IRequestHandler<GetOrderQuery, OrderDto?>
{
public async Task<OrderDto?> Handle(
GetOrderQuery q, CancellationToken ct) =>
await db.Orders
.Where(o => o.Id == q.Id)
.Select(o => new OrderDto(o.Id, o.Total))
.SingleOrDefaultAsync(ct);
}One Handler per Request
For Send, exactly one handler must match a request type. Registering zero or two handlers for the same request is a configuration error.
// CreateOrderCommand -> exactly one CreateOrderHandlerReturning Rich Results
A response can be any type - a DTO, a result wrapper, even a discriminated outcome - letting handlers express success and failure explicitly.
public record Result(bool Ok, string? Error);
public record PlaceOrderCommand(int Id) : IRequest<Result>;Testing a Handler
Because a handler is a plain class with injected dependencies, you test it directly - no HTTP, no controller needed.
var handler = new CreateOrderHandler(inMemoryDb);
var id = await handler.Handle(
new CreateOrderCommand(1, ["sku-1"]), CancellationToken.None);
Assert.True(id > 0);Quick Check
Confirm the MediatR dispatch model.
Recap
You implemented commands and handlers:
AddMediatRscans assemblies to register handlers.- Messages implement
IRequest<T>; handlers implementIRequestHandler<TRequest, T>. - Inject
ISenderand callSendto dispatch; exactly one handler matches. - Queries reuse the same machinery and stay side-effect free.
Next: pipeline behaviors.
Frequently asked questions
Is the “Commands and Handlers with MediatR” lesson free?
Yes — the full text of “Commands and Handlers with MediatR” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Commands and Handlers with MediatR”?
Dispatch requests to dedicated handlers. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Commands and Handlers with MediatR” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- CQRS Concepts
- Commands and Handlers with MediatR
- Pipeline Behaviors
- Notifications and Events