Mediator Design Pattern in ASP.NET Core Web API with 3-Tier Architecture

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.

1. Create the Solution and Project Structure

Create a new ASP.NET Core Web API project with the following structure.

  • LibraryBooks.API (Presentation Layer - Web API)
  • LibraryBooks.Application (Business Logic Layer - Mediator Implementation)
  • LibraryBooks.Domain (Domain Layer - Models and Interfaces)
  • LibraryBooks.Infrastructure (Data Access Layer - Database operations)

2. Install Required Packages

In each project, install the necessary packages.

//Sardar Mudassar Ali Khan
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

3. Define Domain Models

In the LibraryBooks.Domain project, define the models.

// Sardar Mudassar Ali Khan
// LibraryBooks.Domain/Models/LibraryBook.cs
public class LibraryBook
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}

4. Define Mediator Requests and Handlers

In the LibraryBooks.Application project, define the requests and handlers.

// Sardar Mudassar Ali Khan
// LibraryBooks.Application/Requests/ReadBook.cs
public class ReadBookRequest : IRequest<LibraryBook>
{
    public int BookId { get; set; }
    // Add other properties as needed
}

// Sardar Mudassar Ali Khan
// 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;
    }
}
//Sardar Mudassar Ali Khan
// LibraryBooks.Application/Requests/UpdateBook.cs
public class UpdateBookRequest: IRequest
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}
// Sardar Mudassar Ali Khan
// 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;
    }
}

5. Implement Data Access

In the LibraryBooks.Infrastructure project, implement the data access layer.

// Sardar Mudassar Ali Khan
// 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();
        }
    }

}

6. Configure Dependency Injection

In the Startup.cs file of the LibraryBooks.API project, configure dependency injection.

// Sardar Mudassar Ali Khan
// 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);

}

7. Create Controllers

In this project, create controllers that use the mediator.

// Sardar Mudassar Ali Khan
// 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.

It's important to note that this is a foundational example, and in a real-world scenario, additional considerations such as input validation, error handling, and security measures should be implemented. This example serves as a starting point for building robust and scalable web applications using the Mediator Design Pattern in an ASP.NET Core environment.