Cleaner code with MediatR and Minimal APIs in .NET

Cleaner code with MediatR and Minimal APIs in .NET

APIs in .NET are typically structured where controllers hold hard dependencies on services. With time, the code becomes tightly coupled, and scaling eventually becomes a problem.  If you've ever participated in the development of larger APIs in .NET, you might be familiar with the lifecycle of projects like these.

MediatR - The seperation savior

MediatR is a .NET implementation of the mediator pattern which provides in-process messaging with no dependencies. Instead of communication going directly between objects, all communication goes through the mediator, seperating and uncoupling logic. MediatR support different methods of communication: notifications, requests, and pipelines.

Notifications

In-process notifications with a single initiator and up to multiple consumers. Each consumer handles the notification in whichever way they like. The main concern with in-process notifications is the fact that it is in-process. If e.g. the first handler breaks, the succeeding handlers will not run. Error handling then becomes a nightmare and out-of-process is a much better fit.

using MediatR;

public record MyNotification(string Message) : INotification;

public class MyNotificationHandler:INotificationHandler<MyNotification>
{
    public async Task Handle(
    	MyNotification notification, 
    	CancellationToken cancellationToken)
    {
        // Some logic to handle the notification
    }
}


// Controller code

private static async Task<IResult> Notify(IMediator mediator)
{
	await mediator.Publish(new MyNotification("Hello!")):
	return Results.Ok("Notified");
}
MediatR notification implementation

The above example demonstrates how easily the notification pattern can be implemented. I would recommend organizing the code into separate folders and files.  

Requests

Where MediatR shines is with the request pattern - one consumer, one handler. The consumer sends a request to the mediator which routes it to the correct handler and returns the response.

using MediatR;

public record MyRequest(string Message) : IRequest<MyResponse>;

public class MyRequestHandler : IRequestHandler<MyRequest, MyResponse>
{
    public async Task<MyResponse> Handle(
    	MyRequest request, 
    	CancellationToken cancellationToken)
    {
        // Some logic to handle the request
    }
}


// Controller code

private static async Task<IResult> Notify(IMediator mediator)
{
	var response = await mediator.Send(new MyRequest()):
	return Results.Ok(response);
}
MediatR request implementation

The request implementation is quite similar to the notification pattern, the only difference being that you can only register one handler per request.

Pipelines

If you need to handle one-to-one requests in multiple stages, kind of like (actually exactly like) .NET middleware, MediatR supports pipeline behavior which allows you to handle requests in a middleware type of fashion.

CQRS - Even more complexity

CQRS is another pattern and stands for Command query responsibility segregation. We seperate read and write operations into queries and commands. This pattern is great for single responsibility and separation of concern, and is a great fit with the mediator pattern. The downside is the added complexity.

In the aforementioned project where I introduced mediator, I also use CQRS to explore the pattern. In its current state there is not much added benefit in terms of performance as both read and write handlers in the end use the same repository and database. Implementing this pattern from the start does however open the opportunity to segregating in the future. I also see the benefit of segregation in terms of readability.

The added complexity of Mediator and CQRS will drown out smaller projects and only starts to become useful in larger projects.

Keeping it clean

Adding complexity can lead to noise, stressing the importance of an organized project structure. Mixing all queries, commands and handlers in a single folder would be horrible for readability and a poor experience for any developers continuing your work.

Features

Using features to define business areas for the application is a convenient way to separate code in an intuitive manner.  Models, services, repositories and handlers  that interact with a certain business area are all grouped together within a single feature folder in your project.

Further, create folders for queries, commands, handlers, models .. for each of your features. Set a common structure and use it for all your features, it's extremely easy to navigate!

An added bonus of using features is that if you can avoid creating dependencies on the internal logic of the feature, carving out single features to microservices are extremely easy.

Introducing DTOs for each feature also decouples the internal logic even further and you're essentially left with microservices within your monolith that are easy to work with.

Minimap APIs - The finishing touch

I'm used to controllers, and controllers exist in the controllers folder. You could of course move the controllers to the feature folders and call it a day. I for one appreciate a design which lets me more easily navigate and maintain the code, and controllers tend to grow beyond maintainability.  Enters: the minimal API. Minimal APIs have no need for controllers, and you can create endpoints as easily as:

app.MapGet("/user", async (IMediator mediator) => 
{ 
    var user = await mediator.Send(new GetUserQuery());
    return Results.Ok(user);
});

Only a couple of lines inside Program.cs is enough to create an endpoint to retrieve user information. We do not however want to overload the Program.cs file with endless endpoints, that would defeat the whole purpose of neatly organizing our project.

Simply create an extension method to register the middleware and the handlers you need for the endpoints

    public static void MapUserEndpoints(this WebApplication app)
    {
        app.MapGet("/user/me", Get);
        app.MapPost("/user", Create);
        app.MapPut("/user", Update);
    }

    private static async Task<IResult> Get(
    IMediator mediator,
    IHttpContextAccessor httpContextAccessor)
    {
        var userId = HttpContextUtility.GetUserId(httpContextAccessor);
        var user = await mediator.Send(new GetUserProfileQuery(userId));
        return Results.Ok(user);
    }
    
    // other endpoints ...

And in Program.cs:

app.MapUserEndpoints();

You can even decouple it further, one file for each endpoint and one file to register the endpoints. For now, I've resulted to keeping all endpoints for a domain in a single file as my endpoints are dense and there are few of them. Eventually I might feel the need to split them into their own files.

So what now..?

Well.. it's really up to you. I believe that design patterns and best practices are good to know about and keep in mind, but adjust it to your specific need if it makes  sense. If best practice makes complete sense in your case, great! If you're a bit concerned about certain aspects, you're free to make adjustments.

I do find it tedious at times to scroll through countless of files only containing a single record and find this to be a flaw in my system, but a manageable compromise.

These opinions are my own, and I'm always open to discussing them. Feel free to contact me for a chat on anything I discuss in my posts!