DEV Community

Niklas Engberg
Niklas Engberg

Posted on • Originally published at hackernoon.com on

5 1

MediatR behaviors to validate API Resource existence

MediatR logo borrowed from https://bit.ly/2N4Jz6S

When building robust APIs it it really important with validation and descriptive error messages and status codes.

In this post I am going to show you how you can get rid of duplicated code in your Request Handlers by adding MediatR behaviors to take care of validation for resource existence.

MediatR is an in-process messaging library in .NET that supports requests/responses, commands, queries, events etc. Go check it out if you haven’t.

In MediatR there is something called behaviors which allow you to build your own pipeline for requests. I use them for cross cutting concerns such as logging and validation of requests parameters.

Recently when some endpoints were developed for a microservice I struggled with duplicated code in my Request Handlers that was necessary to have in place.

To give you an example lets start with a couple of API endpoints, and also the implementation of the Request Handlers with the duplicated code.

The endpoints in this example are:

PUT /groups/{id}

POST /groups/{id}/users

The controller:

[Route("[controller]")]
public class GroupsController : BaseController
{
private readonly IMediator _mediator;
public GroupsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPut]
[Route("{id}")]
[Authorize]
public async Task<IActionResult> Update(Guid id, [FromBody]UpdateGroupRequest request)
{
request.Id = id;
var response = await _mediator.Send(request);
return ActionResultBasedOnResponse(response);
}
[HttpPost]
[Route("{id}/users")]
[Authorize]
public async Task<IActionResult> AddUserToGroup(Guid id, [FromBody]AddUserToGroupRequest request)
{
request.Id = id;
var response = await _mediator.Send(request);
return ActionResultBasedOnResponse(response);
}
}

Both endpoints need to verify that the id-parameter actually matches a given group to be able to update a group / add user to a group (in my example I’m using EF Core as persistence, but you can safely ignore that).

public class UpdateGroupRequestHandler : IRequestHandler<UpdateGroupRequest, Result>
{
private readonly DbContext _dbContext;
public UpdateGroupRequestHandler(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(UpdateGroupRequest request, CancellationToken cancellationToken)
{
var group = await _dbContext.Groups
.SingleOrDefaultAsync(c => c.Id == request.Id, cancellationToken);
if(group == null)
{
//Fail request
}
group.ChangeName(request.Name);
_dbContext.Update(group);
await _dbContext.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
}
public class AddUserToGroupRequestHandler : IRequestHandler<AddUserToGroupRequest, Result>
{
private readonly DbContext _dbContext;
public AddUserToGroupRequestHandler(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(AddUserToGroupRequest request, CancellationToken cancellationToken)
{
var group = await GetGroup(request, cancellationToken);
var user = await GetUser(request, cancellationToken);
if(group == null)
{
//Fail request
}
//add group to user
}
}

As you can see there is an if-check for group existence in both the handlers.

How should we get rid of this duplication? Wouldn’t it be good to use some sort of “validation” here as a pre-step, so that the Request Handlers could look like this?

public class UpdateGroupRequestHandler : IRequestHandler<UpdateGroupRequest, Result>
{
private readonly DbContext _dbContext;
public UpdateGroupRequestHandler(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(UpdateGroupRequest request, CancellationToken cancellationToken)
{
var group = await _dbContext.Groups
.SingleAsync(c => c.Id == request.Id, cancellationToken);
group.ChangeName(request.Name);
_dbContext.Update(group);
await _dbContext.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
}
public class AddUserToGroupRequestHandler : IRequestHandler<AddUserToGroupRequest, Result>
{
private readonly DbContext _dbContext;
public AddUserToGroupRequestHandler(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(AddUserToGroupRequest request, CancellationToken cancellationToken)
{
var group = await GetGroup(request, cancellationToken);
var user = await GetUser(request, cancellationToken);
//add group to user
}
}

Thank behaviors for that! Let’s introduce an interface that both the requests: UpdateGroupRequest and AddUserToGroupRequest implement.

public interface IGroupRequest
{
Guid Id { get; set; }
}

Then create a behavior implementation class with generic constraint to the interface defined above so that the behavior only applies to requests implementing the IGroupRequest interface.

public class ValidateGroupExistsPipelineBehavior<TRequest> : IPipelineBehavior<TRequest, Result>
where TRequest : IGroupRequest
{
private readonly DbContext _dbContext;
public ValidateGroupExistsPipelineBehavior(DbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<Result> next)
{
var group = await _dbContext.Groups
.SingleOrDefaultAsync(c => c.Id == request.Id, cancellationToken);
if(group == null)
{
//Fail request and break pipeline
}
return await next();
}
}

And a registration example using Autofac:

var builder = new ContainerBuilder();
builder
.RegisterGeneric(typeof(ValidateGroupExistsPipelineBehavior<>))
.As(typeof(IPipelineBehavior<,>))
.InstancePerLifetimeScope();
//other registrations
var container = builder.Build();

Now we can rewrite our Request Handlers without the if statements, but still get the benefit of validation!

Lets imagine we now add a couple of more endpoints:

GET /groups/{id}

POST /groups/{id}/permissions

Add the IGroupRequest interface to your requests and you will get this feature out of the box. Pretty simple! I find this kind of approach really useful. What do you think?

We have to keep in mind that there are both some cons and pros to this approach. What I’ve found out is:

Cons

  • Multiple roundtrips if you have a database that you validate the existence against.
  • Validation as a separate class which might confuse some of the developers

Pros

  • Cleaner code in the actual request handler
  • No duplicated code scattered all over all Request Handlers that need to validate an entity’s existence

If you liked this post. You know what to do! 👏


Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay