CQRS and Mediator Design Pattern in ASP.NET Core Web API
Introduction
Implementing the Mediator Design Pattern in an ASP.NET Core Web API with a 3-tier architecture involves creating separate layers for presentation, business logic, and data access. The Mediator Design Pattern helps in decoupling the components by using a mediator to handle communication between them. In this example, I will focus on building a simple library books CRUD (Create, Read, Update, Delete) application.
CQRS and the Mediator Pattern
The MediatR library was built to facilitate two primary software architecture patterns: CQRS and the Mediator pattern. Whilst similar, let’s spend a moment understanding the principles behind each pattern.
CQRS
CQRS stands for “Command Query Responsibility Segregation”. As the acronym suggests, it’s all about splitting the responsibility of commands (saves) and queries (reads) into different models.
If we think about the commonly used CRUD pattern (Create-Read-Update-Delete), usually we have the user interface interacting with a datastore responsible for all four operations. CQRS would instead have us split these operations into two models, one for the queries (aka “R”), and another for the commands (aka “CUD”).
The following image illustrates how this works:
Install Required Packages
In each project, install the necessary packages.
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Domain Model
n the LibraryBooks.Domain project, define the models.
// LibraryBooks.Domain/Models/LibraryBook.cs
public class LibraryBook
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
Mediator Requests and Handlers
In the LibraryBooks.Application project, define the requests and handlers.
// LibraryBooks.Application/Requests/ReadBook.cs
public class ReadBookRequest : IRequest<LibraryBook>
{
public int BookId { get; set; }
// Add other properties as needed
}
// LibraryBooks.Application/Handlers/ReadBookHandler.cs
public class ReadBookHandler: IRequestHandler<ReadBookRequest, LibraryBook>
{
private readonly IBookRepository _bookRepository;
public ReadBookHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public async Task<LibraryBook> Handle(ReadBookRequest request, CancellationToken cancellationToken)
{
var book = await _bookRepository.GetAsync(request.BookId);
return book;
}
}
// LibraryBooks.Application/Requests/UpdateBook.cs
public class UpdateBookRequest: IRequest
{
public int BookId { get; set; }
public string Title { get; set; }
public string Author { get; set; }
}
// LibraryBooks.Application/Handlers/UpdateBookHandler.cs
public class UpdateBookHandler: IRequestHandler<UpdateBookRequest>
{
private readonly IBookRepository _bookRepository;
public UpdateBookHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public async Task<Unit> Handle(UpdateBookRequest request, CancellationToken cancellationToken)
{
var existingBook = await _bookRepository.GetAsync(request.BookId);
if (existingBook != null)
{
// Update book properties
existingBook.Title = request.Title;
existingBook.Author = request.Author;
// Update other properties as needed
await _bookRepository.UpdateAsync(request.BookId, existingBook);
}
return Unit. Value;
}
}
// LibraryBooks.Application/Requests/DeleteBook.cs
public class DeleteBookRequest: IRequest
{
public int BookId { get; set; }
}
// LibraryBooks.Application/Handlers/DeleteBookHandler.cs
public class DeleteBookHandler: IRequestHandler<DeleteBookRequest>
{
private readonly IBookRepository _bookRepository;
public DeleteBookHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public async Task<Unit> Handle(DeleteBookRequest request, CancellationToken cancellationToken)
{
await _bookRepository.DeleteAsync(request.BookId);
return Unit. Value;
}
}
Implement Data Access
In the LibraryBooks.Infrastructure project, implement the data access layer.
// LibraryBooks.Infrastructure/Repositories/BookRepository.cs
public class BookRepository: IBookRepository
{
private readonly ApplicationDbContext _context;
public BookRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<int> AddAsync(LibraryBook book)
{
_context.LibraryBooks.Add(book);
await _context.SaveChangesAsync();
return book.Id;
}
public async Task<LibraryBook> GetAsync(int bookId)
{
var book = await _context.LibraryBooks.FindAsync(bookId);
return book;
}
public async Task UpdateAsync(int bookId, LibraryBook updatedBook)
{
var existingBook = await _context.LibraryBooks.FindAsync(bookId);
if (existingBook != null)
{
// Update properties
existingBook.Title = updatedBook.Title;
existingBook.Author = updatedBook.Author;
// Update other properties as needed
await _context.SaveChangesAsync();
}
}
public async Task DeleteAsync(int bookId)
{
var book = await _context.LibraryBooks.FindAsync(bookId);
if (book != null)
{
_context.LibraryBooks.Remove(book);
await _context.SaveChangesAsync();
}
}
}
Configure Dependency Injection
In the Startup.cs file of the LibraryBooks.API project, configure dependency injection.
// LibraryBooks.API/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IBookRepository, BookRepository>();
services.AddMediatR(typeof(CreateBookHandler).Assembly);
}
Create Controllers
In this project, create controllers that use the mediator.
// LibraryBooks.API/Controllers/BookController.cs
[ApiController]
[Route("api/[controller]")]
public class BookController : ControllerBase
{
private readonly IMediator _mediator;
public BookController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateBook([FromBody] CreateBookRequest request)
{
var bookId = await _mediator.Send(request);
return Ok(bookId);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetBook(int id)
{
var request = new ReadBookRequest { BookId = id };
var book = await _mediator.Send(request);
if (book == null)
{
return NotFound();
}
return Ok(book);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateBook(int id, [FromBody] UpdateBookRequest request)
{
request.BookId = id;
await _mediator.Send(request);
return Ok();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBook(int id)
{
var request = new DeleteBookRequest { BookId = id };
await _mediator.Send(request);
return Ok();
}
}
This example provides a basic structure for implementing the Mediator Design Pattern in an ASP.NET Core Web API with a 3-tier architecture. Remember to implement the remaining CRUD operations and error handling as needed. Also, configure database connection strings and other settings in the appsettings.json file.
Conclusion
In conclusion, the provided example demonstrates the implementation of the Mediator Design Pattern in an ASP.NET Core Web API with a 3-tier architecture for a library books CRUD application. The key components include separate layers for presentation, business logic, and data access. The Mediator pattern helps decouple the application’s components, promoting maintainability and scalability.
By defining domain models, Mediator requests, and handlers, we’ve established a structured approach to handling Create, Read, Update, and Delete operations. The use of dependency injection and configuration in the startup file ensures that the application is well-organized and follows best practices.