Download Image API Using MediatR Vertical Architecture

As I mentioned in one of my previous posts, I will be showing you how to make an API that downloads an image to a local path. 

But I will take this as an opportunity to show you an example of how to achieve that using vertical architecture with MediatR.


One of the most common problems with clean architecture is that there usually is a service class for each domain entity that encapsulates all application logic regarding this entity. 

Something that if you have applied on a larger scale project with a little too much business logic you would find that the class got too big, and maintainability becomes a little more difficult by each added logic. 

Also, data transfer objects (DTO) start to have weirder names because you can't have a one-size-fits-all model that accommodate the growing logic in your service class. 

(eg. CustomerDTO.cs, LoyalCustomerDTO.cs, CustomerLightWeightDTO.cs)

Not to mention of course pull requests and merging codes become a living nightmare. 

Well, how can we split the domain service? It's not like we can make a class called CustomerService.cs and another class called CustomerService2.cs.

Actually, the solution for these kinds of problems can be the CQRS approach in which you create a command that does something and a query that returns something and you create a handler for each type.

So instead of having CustomerService.cs you would have SaveCustomerCommand.cs and GetLoyalCustomersQuery.cs each has their own handler, and each class has their own fields which are required for this action to be completed.

MediatR is built on this concept and another important concept as well which is the mediator pattern. 
The mediator pattern basically allows loose coupling by allowing components to communicate through a central mediator which in this case is the MediatR library.

MediatR also provides a feature called pipeline behavior which allows you to process the request through a pipeline of your choice including logging, validation and exception handling.

So, let's see this library in action while we create an API that downloads an image to a local path.

The Command

First step let's add the MediatR nuget package to the solution then add a command that downloads an image.

using MediatR;

namespace Playground.Commands
{
    public class DownloadImageCommand(string url) : IRequest<string>
    {
        public string Url { get; set; } = url;
    }
}

This seems to the trick as we only need a url to perform our needed action. Also notice that in this case we need to specify that the command implements the IRequest interface and returns string which will be the local path of the saved image.

The Handler

Next step we need to add a handler which will basically perform the application logic.

using MediatR;
using Microsoft.Extensions.Configuration;
using Playground.Commands;

namespace Playground.Handlers
{
    public class DownloadImageCommandHandler(IConfiguration configuration) : IRequestHandler<DownloadImageCommand, string>
    {
        public async static Task<string> DownloadImageAndReturnPathAsync(string imageUrl, string fileName, string saveDirectory)
        {
            // Create the directory if it doesn't exist
            if (!Directory.Exists(saveDirectory))
            {
                Directory.CreateDirectory(saveDirectory);
            }

            string savePath = Path.Combine(saveDirectory, fileName); // Combine the directory and file name to get the full save path

            using HttpClient client = new();

            HttpResponseMessage response = await client.GetAsync(imageUrl);

            response.EnsureSuccessStatusCode();

            byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();

            await File.WriteAllBytesAsync(savePath, imageBytes);

            return savePath;
        }

        public async Task<string> Handle(DownloadImageCommand request, CancellationToken cancellationToken) =>
            await DownloadImageAndReturnPathAsync(request.Url, Guid.NewGuid() + ".jpg", configuration["SaveDirectory"]!);
    }
}

The handler must implement IRequestHandler and specify the request and response.

The Mediator


Now for the final step where we actually call the MediatR to send our request. Let's do that in our API layer in our ImageController.cs but first let's add MediatR in our program.cs/startup.cs

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DownloadImageCommand).Assembly));

All clear! Now MediatR is injected in our program, and we can use it as a mediator to literally handle our request.

using MediatR;
using Microsoft.AspNetCore.Mvc;
using Playground.Commands;

namespace HARDCODE.API.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ImageController : ControllerBase
    {
        private readonly IMediator _mediator;

        public ImageController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost]
        [Route("download-image")]
        public string DownloadImage(string url)
        {
            return _mediator.Send(new DownloadImageCommand(url)).Result;
        }
    }
}

Perfect! Now everything seems fine.

But wait! What if we want to validate the url before downloading the image? 

And what if we need to handle the exceptions globally so that even when any unplanned exceptions occur, we can log it or beautify the response instead of sending back 100 lines of unreadable gibberish?

Well, these questions will be answered in upcoming posts.


Comments

Popular posts

Google & Microsoft Token Validation Middleware in ASP.NET Applications

Publish and Consume Messages Using RabbitMQ as a Message Broker in 4 Simple Steps 🐰

Real Life Case Study: Synchronous Inter-Service Communication in Distributed Systems