DEV Community

mohamed Tayel
mohamed Tayel

Posted on

What is Clean Architecture: Part 19 -Add Controllers

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;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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. The CreateCategoryCommand 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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the GetEventDetailQuery.
  • 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 using DeleteEventCommand.

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)

Collapse
 
shinac01 profile image
Miguel David González del Toro

Thanks man for getting to this point! I've been following the series and it's very awesome. Keep it up!

Collapse
 
moh_moh701 profile image
mohamed Tayel

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!