In this post, we will see what is the Mediator pattern, why we should use it and then, we will implement all CRUD operations for an object “Book” with a Minimal API using MediatR, a popolar .NET library that implements this pattern.
But firsrt of all, what is Mediator?
“The Mediator pattern is a design pattern that helps us reduce the dependencies between objects. It restricts direct communications between objects and forces them to collaborate only through a mediator object. In this way, instead of each component talking directly to another, they all communicate through a mediator (a central hub) making it easier to maintain and modify their behaviors without changing other components.”
Using the Mediator Pattern can lead to several benefits:
- Cleaner Code: Reduces the dependency chain.
- Enhanced Testability: Each component can be tested independently.
- Maintainability: Changes are easier to implement because they’re isolated to a single place (the mediator).
MediatR is a lightweight framework in .NET that implements the Mediator pattern. It uses request/response objects (such as commands and queries) and handlers (the logic to process those requests). By adopting MediatR, it will avoid us to write all code from scratch, allowing us to focus on the core logic of our applications.
Now let’s create a Minimal APi project and start installing some NuGet packages that we will use in the code:
dotnet add package MediatR
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Then, we add the Book entity and the Book DbContext:
[BOOK.CS]
namespace TestMinimalAPI.Models;
public record Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
[BOOKDBCONTEXT.CS]
using Microsoft.EntityFrameworkCore;
using TestMinimalAPI.Models;
namespace TestMinimalAPI.Infrastructure;
public class BookDbContext(DbContextOptions<BookDbContext> options) : DbContext(options)
{
public DbSet<Book> Books { get; set; }
}
At this point, we can define all Requests (Command and Queries) and Handlers for the CRUD operations:
Create Book ->
[CREATEBOOKCOMMAND.CS]
using MediatR;
using TestMinimalAPI.Models;
namespace TestMinimalAPI.Requestes;
public record CreateBookCommand(string Title, string Author) : IRequest<Book>;
[CREATEBOOKCOMMANDHANDLER.CS]
using MediatR;
using TestMinimalAPI.Infrastructure;
using TestMinimalAPI.Models;
using TestMinimalAPI.Requestes;
namespace TestMinimalAPI.Handlers;
public class CreateBookCommandHandler(BookDbContext dbContext) : IRequestHandler<CreateBookCommand, Book>
{
public async Task<Book> Handle(CreateBookCommand command, CancellationToken token)
{
var book = new Book
{
// Normally we should generate the ID or let the DB do it
Id = new Random().Next(1, 1000),
Title = command.Title,
Author = command.Author
};
dbContext.Books.Add(book);
await dbContext.SaveChangesAsync(token);
return book;
}
}
Get all Books ->
[GETALLBOOKSQUERY.CS]
using MediatR;
using TestMinimalAPI.Models;
namespace TestMinimalAPI.Requestes;
public record GetAllBooksQuery() : IRequest<IEnumerable<Book>>;
[GETALLBOOKSQUERYHANDLER.CS]
using MediatR;
using Microsoft.EntityFrameworkCore;
using TestMinimalAPI.Infrastructure;
using TestMinimalAPI.Models;
using TestMinimalAPI.Requestes;
namespace TestMinimalAPI.Handlers;
public class GettAllBooksQueryHandler(BookDbContext dbContext): IRequestHandler<GetAllBooksQuery, IEnumerable<Book>>
{
public async Task<IEnumerable<Book>> Handle(GetAllBooksQuery command, CancellationToken token)
{
return await dbContext.Books.ToListAsync(token);
}
}
Get Book by Id ->
[GETBOOKBYIDQUERY.CS]
using MediatR;
using TestMinimalAPI.Models;
namespace TestMinimalAPI.Requestes;
public record GetBookByIdQuery(int Id) : IRequest<Book>;
[GETBOOKBYIDQUERYHANDLER.CS]
using MediatR;
using TestMinimalAPI.Infrastructure;
using TestMinimalAPI.Models;
using TestMinimalAPI.Requestes;
namespace TestMinimalAPI.Handlers;
public class GetBookByIdQueryHandler(BookDbContext dbContext): IRequestHandler<GetBookByIdQuery, Book>
{
public async Task<Book?> Handle(GetBookByIdQuery command, CancellationToken token)
{
return await dbContext.Books.FindAsync(command.Id, token);
}
}
Update Book ->
[UPDATEBOOKCOMMAND.CS]
using MediatR;
using TestMinimalAPI.Models;
namespace TestMinimalAPI.Requestes;
public record UpdateBookCommand(int Id, string Title, string Author): IRequest<Book?>;
[UPDATEBOOKCOMMANDHANDLER.CS]
using MediatR;
using TestMinimalAPI.Infrastructure;
using TestMinimalAPI.Models;
using TestMinimalAPI.Requestes;
namespace TestMinimalAPI.Handlers;
public class UpdateBookCommandHandler(BookDbContext dbContext): IRequestHandler<UpdateBookCommand, Book>
{
public async Task<Book> Handle(UpdateBookCommand command, CancellationToken token)
{
var bookToUpdate = await dbContext.Books.FindAsync(command.Id, token);
if(bookToUpdate == null)
return null;
bookToUpdate.Title = command.Title;
bookToUpdate.Author = command.Author;
await dbContext.SaveChangesAsync(token);
return bookToUpdate;
}
}
Delete Book ->
[DELETEBOOKCOMMAND.CS]
using MediatR;
namespace TestMinimalAPI.Requestes;
public record DeleteBookCommand(int Id) : IRequest<bool>;
[DELETEBOOKCOMMANDHANDLER.CS]
using MediatR;
using TestMinimalAPI.Infrastructure;
using TestMinimalAPI.Requestes;
namespace TestMinimalAPI.Handlers;
public class DeleteBookCommandHandler(BookDbContext dbContext): IRequestHandler<DeleteBookCommand, bool>
{
public async Task<bool> Handle(DeleteBookCommand command, CancellationToken token)
{
var bookToDelete = await dbContext.Books.FindAsync(command.Id, token);
if(bookToDelete == null)
return false;
dbContext.Books.Remove(bookToDelete);
await dbContext.SaveChangesAsync(token);
return true;
}
}
Finally, we modify the file Program.cs to define the endpoints:
[PROGRAM.CS]
using Microsoft.EntityFrameworkCore;
using TestMinimalAPI.Infrastructure;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using TestMinimalAPI.Requestes;
var builder = WebApplication.CreateBuilder(args);
// 1. Add DbContext with in-memory provider
builder.Services.AddDbContext<BookDbContext>(options =>
options.UseInMemoryDatabase("BooksDb"));
// 2. Add MediatR
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
// Build the app
var app = builder.Build();
// Endpoints
// Create Book
app.MapPost("/books", async ([FromBody] CreateBookCommand command, IMediator mediator) =>
{
var createdBook = await mediator.Send(command);
return Results.Ok();
});
// List of Books
app.MapGet("/books", async (IMediator mediator) =>
{
var lstBooks = await mediator.Send(new GetAllBooksQuery());
return Results.Ok(lstBooks);
});
// Get Book by Id
app.MapGet("/books/{id}", async (int id, IMediator mediator) =>
{
var book = await mediator.Send(new GetBookByIdQuery(id));
return book is not null ? Results.Ok(book) : Results.NotFound();
});
// Update Book
app.MapPut("/books/{id}", async (int id, [FromBody] UpdateBookCommand command, IMediator mediator) =>
{
if (id != command.Id)
return Results.BadRequest("ID mismatch");
var updatedBook = await mediator.Send(command);
return updatedBook is not null ? Results.Ok(updatedBook) : Results.NotFound();
});
// UpdDeleteate Book
app.MapDelete("/books/{id}", async (int id, IMediator mediator) =>
{
var result = await mediator.Send(new DeleteBookCommand(id));
return result ? Results.NoContent() : Results.NotFound();
});
app.Run();
If we run the application, the following will be the result:
Add Books
Get Books
Get Book by Id
Update Book
Delete Book