Commands and Handlers with MediatR
Dispatch requests to dedicated handlers.
Commands and Handlers with MediatR is a free C# Academy lesson on CoddyKit. This is 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, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.
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.
Learn C# with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 93
- Lessons
- 346
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. 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. The C# Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
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