In this article, we’ll see how to build lightweight and loosely coupled API controllers using MediatR in an ASP.NET Core 8 application. We’ve already set up the essential layers—Application, Infrastructure, and Persistence—and now it’s time to expose functionality through an API that communicates with the core of our application using the MediatR pattern.
Why Use MediatR?
MediatR is a powerful library that helps decouple the application’s layers by acting as a mediator between the controllers and the business logic (commands and queries). It promotes the Single Responsibility Principle (SRP) by separating the concerns of business logic from HTTP request handling, making our code easier to maintain, test, and extend.
Setting Up the API Project
In our project, we have already registered MediatR in the Application Layer via the AddApplicationServices()
method, so there’s no need to add it again directly in the Program.cs file. Here's a reminder of how we set it up in the StartupExtensions.cs:
using GloboTicket.TicketManagement.Application;
using GloboTicket.TicketManagement.Infrastructure;
using GloboTicket.TicketManagement.Persistence;
namespace GloboTicket.TicketManagement.Api
{
public static class StartupExtensions
{
public static WebApplication ConfigureServices(
this WebApplicationBuilder builder)
{
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddPersistenceServices(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddCors(
options => options.AddPolicy(
"open",
policy => policy.WithOrigins([builder.Configuration["ApiUrl"] ?? "https://localhost:7020",
builder.Configuration["BlazorUrl"] ?? "https://localhost:7080"])
.AllowAnyMethod()
.SetIsOriginAllowed(pol => true)
.AllowAnyHeader()
.AllowCredentials()));
builder.Services.AddSwaggerGen();
return builder.Build();
}
public static WebApplication ConfigurePipeline(this WebApplication app)
{
app.UseCors("open");
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
return app;
}
}
}
This ensures that MediatR is available throughout the application via dependency injection. Now, let’s move on to creating the controllers.
Creating the CategoryController
The CategoryController will expose endpoints to handle various category-related API requests, like fetching all categories or adding a new category. Each request will be handled by sending the appropriate query or command to MediatR, which in turn delegates the logic to the correct handler.
using GloboTicket.TicketManagement.Application.Features.Categories.Commands.CreateCategory;
using GloboTicket.TicketManagement.Application.Features.Categories.Queries.GetCategoriesList;
using GloboTicket.TicketManagement.Application.Features.Categories.Queries.GetCategoriesListWithEvents;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace YourApp.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CategoryController : Controller
{
private readonly IMediator _mediator;
public CategoryController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("all", Name = "GetAllCategories")]
public async Task<ActionResult<List<CategoryListVm>>> GetAllCategories()
{
var dtos = await _mediator.Send(new GetCategoriesListQuery());
return Ok(dtos);
}
[HttpGet("allwithevents", Name = "GetCategoriesWithEvents")]
public async Task<ActionResult<List<CategoryEventListVm>>> GetCategoriesWithEvents(bool includeHistory)
{
var query = new GetCategoriesListWithEventsQuery { IncludeHistory = includeHistory };
var dtos = await _mediator.Send(query);
return Ok(dtos);
}
[HttpPost(Name = "AddCategory")]
public async Task<ActionResult<CreateCategoryCommandResponse>> Create([FromBody] CreateCategoryCommand command)
{
var response = await _mediator.Send(command);
return Ok(response);
}
}
}
Key Points of CategoryController:
-
GetAllCategories: Sends a
GetCategoriesListQuery
to MediatR. The query handler processes the logic and returns the result, which is sent back as an HTTP 200 response. -
GetCategoriesWithEvents: Similar to
GetAllCategories
, this query includes an additional Boolean parameter (includeHistory
) to determine whether historical data should be included. -
Create: Handles
POST
requests to create a new category. TheCreateCategoryCommand
is passed to MediatR, which sends it to the appropriate handler, returning the response from the handler.
Creating the EventsController
Similarly, the EventsController will handle event-related API requests such as fetching all events, retrieving event details, creating, updating, and deleting events. Each action is decoupled from the controller logic by using MediatR.
using GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent;
using GloboTicket.TicketManagement.Application.Features.Events.Commands.DeleteEvent;
using GloboTicket.TicketManagement.Application.Features.Events.Commands.UpdateEvent;
using GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventDetail;
using GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsList;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace YourApp.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class EventsController : Controller
{
private readonly IMediator _mediator;
public EventsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet(Name = "GetAllEvents")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public async Task<ActionResult<List<EventListVm>>> GetAllEvents()
{
var result = await _mediator.Send(new GetEventsListQuery());
return Ok(result);
}
[HttpGet("{id}", Name = "GetEventById")]
public async Task<ActionResult<EventDetailVm>> GetEventById(Guid id)
{
var query = new GetEventDetailQuery { Id = id };
return Ok(await _mediator.Send(query));
}
[HttpPost(Name = "AddEvent")]
public async Task<ActionResult<Guid>> Create([FromBody] CreateEventCommand command)
{
var id = await _mediator.Send(command);
return Ok(id);
}
[HttpPut(Name = "UpdateEvent")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult> Update([FromBody] UpdateEventCommand command)
{
await _mediator.Send(command);
return NoContent();
}
[HttpDelete("{id}", Name = "DeleteEvent")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult> Delete(Guid id)
{
var command = new DeleteEventCommand { EventId = id };
await _mediator.Send(command);
return NoContent();
}
}
}
Key Points of EventsController:
-
GetAllEvents: Retrieves all events using
GetEventsListQuery
and returns them to the client. -
GetEventById: Fetches the details of a specific event by sending an
id
in theGetEventDetailQuery
. -
Create: Handles the creation of new events. A
CreateEventCommand
is passed from the client, and MediatR forwards it to the handler responsible for event creation. -
Update: Updates an existing event by sending an
UpdateEventCommand
. The handler processes the update logic, and a 204 No Content response is returned. -
Delete: Deletes an event based on its
id
usingDeleteEventCommand
.
Conclusion
By leveraging MediatR, we have created lightweight, loosely coupled controllers for handling categories and events in an ASP.NET Core 8 application. The key advantage of using MediatR is the separation of concerns: the controllers focus solely on handling HTTP requests and responses, while business logic is handled by specific handlers in the application layer.
This approach ensures that our API remains maintainable, scalable, and easy to extend as the application grows. In future articles, we’ll explore advanced MediatR features, such as pipeline behaviors, validation, and more complex business logic.
For the complete source code, feel free to visit the GitHub repository here.
Top comments (2)
Thanks man for getting to this point! I've been following the series and it's very awesome. Keep it up!
Thank you so much! I'm really glad you're finding the series helpful. Your support means a lot, and it motivates me to keep going. Stay tuned for more content coming soon!