How to Handle Validation Checks in C# with MediatR and FluentValidation

Kostiantyn Bilous
SharpAssembly
Published in
7 min readJun 18, 2024

In enterprise software development, maintaining clean and maintainable code is a non-functional requirement. One critical aspect of achieving this is effectively handling validation errors, especially in complex applications where separating concerns and adhering to principles like the Single Responsibility Principle (SRP) can significantly enhance overall code quality. This article will explore how to handle validation errors using FluentValidation and MediatR within a .NET application. By leveraging these libraries, we can ensure that application logic is robust, scalable, and easy to maintain.

Throughout this article, we will demonstrate how to implement validation logic for creating a new Party in the personal investment tracker application—InWestMan, ensuring that all necessary rules are enforced before the entity is persisted in the database.

Validation in Software Development

Validation is a fundamental aspect of software development, ensuring that data meets specific criteria before it is processed or stored. Proper validation helps prevent errors, maintain data integrity, and improve user experience by providing immediate feedback on incorrect inputs. There are various levels at which validation can be implemented, including at the domain level, application level, and even at the user interface level.

The Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) is one of the SOLID principles of object-oriented design. It states that a class should have only one reason to change: one job or responsibility. By adhering to SRP, we can create more modular and maintainable code. In our example, separating validation logic from command handling logic helps us adhere to SRP, making our code cleaner and more testable.

Introduction to FluentValidation

FluentValidation is a popular .NET library for building strongly typed validation rules. It allows developers to define validation logic fluently and expressively, making the code easier to read and maintain. FluentValidation supports various validation scenarios, including synchronous and asynchronous validation, particularly useful for complex applications.

Key features of FluentValidation include:

  • Fluent API: Define validation rules using a fluent interface, which improves readability.
  • Custom Validators: Create custom validation rules for specific business requirements.
  • Asynchronous Validation: Support for asynchronous validation is crucial for validating data against external resources like databases.

Introduction to MediatR

MediatR is a simple mediator library for .NET that facilitates the implementation of the mediator pattern. It helps decouple the sending of requests from handling them, promoting a more modular and maintainable architecture. Using MediatR, we can centralize the handling of commands, queries, and notifications, making managing complex workflows in an application more accessible.

Key features of MediatR include:

  • Decoupling: Decouples request sending from handling, promoting loose coupling between components.
  • Pipeline Behaviors: Supports pipeline behaviors, allowing features like validation, logging, and caching to be handled centrally.
  • Scalability: Scales well with complex applications by organizing request-handling logic in a structured way.

Combining FluentValidation and MediatR

Combining FluentValidation and MediatR can achieve a robust validation mechanism within our .NET application. FluentValidation handles the validation logic, ensuring that all necessary rules are enforced, while MediatR manages the flow of commands and queries, decoupling the request sending from the handling process. This combination creates a clean, maintainable, and scalable architecture.

The following sections will explore a practical example of implementing validation for a Party entity in our application. We will demonstrate how to define validation rules using FluentValidation, integrate these rules with MediatR's command handling, and ensure that our application adheres to best practices in software development.

Practical Example

Let's look into the Party domain consists of the party name, country code, and party type properties. The Party is an Entity and an Aggregate Root; thus, it is stored and accessible through the repository.

using Ardalis.GuardClauses;
using InWestMan.DomainEvents;
using InWestMan.Entities;
using InWestMan.Globals.Countries;
using InWestMan.Repositories;

public class Party : BaseEntity<Guid>, IAggregateRoot
{
private Party() // EF constructor
{
}

public Party(string name, CountryCode countryCode, PartyType type)
{
Guard.Against.NullOrWhiteSpace(name, nameof(name));
Guard.Against.Null(type, nameof(type));

Name = name;
CountryCode = countryCode;
Type = type;
}

public string Name { get; set; } = string.Empty;
public CountryCode CountryCode { get; set; }
public PartyType Type { get; set; }

public override string ToString()
{
return Name;
}
}

To add a new Party to DB, we must create an AddPartyCommand, which implements MediatR's IRequest<> interface.

using FluentResults;
using InWestMan.Globals.Countries;
using MediatR;

public class AddPartyCommand : IRequest<Result<Guid>>
{
public required string Name { get; init; }
public required CountryCode CountryCode { get; init; }
public required PartyType Type { get; init; }
}

According to the command domain logic, the command requires the party name, country code, and party type to be specified. After executing the command, it returns a result with the GUID of a newly created party.

Consequently, the AddPartyCommandHandler implements the logic of how the AddPartyCommand should be executed.

using FluentResults;
using InWestMan.MultiTenancy;
using InWestMan.Repositories;
using MediatR;
using Microsoft.Extensions.Logging;

public class AddPartyCommandHandler : IRequestHandler<AddPartyCommand, Result<Guid>>
{
private readonly ILogger<AddPartyCommand> _logger;
private readonly IMultiTenancyService _multiTenancyService;
private readonly IRepository<Party> _repository;

public AddPartyCommandHandler(IRepository<Party> repository, ILogger<AddPartyCommand> logger,
IMultiTenancyService multiTenancyService)
{
_repository = repository;
_logger = logger;
_multiTenancyService = multiTenancyService;
}

public async Task<Result<Guid>> Handle(AddPartyCommand request, CancellationToken cancellationToken)
{
try
{
_logger.LogInformation($"Creating new Party in InWestMan database: {request.Name}");

var party = new Party(request.Name, request.CountryCode, request.Type);
party.AddTenantId(_multiTenancyService.CurrentTenant);
await _repository.AddAsync(party, cancellationToken);

_logger.LogInformation($"New Party in InWestMan database is created: {request.Name}");

return Result.Ok(party.Id);
}
catch (Exception ex)
{
return Result.Fail<Guid>(ex.Message);
}
}
}

In the command handler implementation, we log the start and end of the new party creation process, create a new Party domain, and add it to the repository. In the success case, we return the new Party's GUID inside the Result<Guid> object. In the case of an exception, we wrap the code in the try-catch block and return the Exception message as an error message for the failed Result object.

You can read more about the Result Pattern in my Medium article below:

Now, let's switch to validation. As you probably have seen, we already have some basic input validations in the Party domain class; besides, I would like a more robust validation on the application level. I want to check that the user is not trying to create a Party with the same name. Indeed, I can add this logic in the Command Handler, but I would like to separate concerns and adhere to the Single Responsibility Principle (SRP). Thus, I plan to use the FluentValidation library to create an AddPartyCommandValidator.

using FluentValidation;
using InWestMan.Parties.Specifications;
using InWestMan.Repositories;

namespace InWestMan.Parties.Commands.AddParty;

public class AddPartyCommandValidator : AbstractValidator<AddPartyCommand>
{
private readonly IReadRepository<Party> _partyRepository;

public AddPartyCommandValidator(IReadRepository<Party> partyRepository)
{
_partyRepository = partyRepository;

RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required.")
.Length(1, 100).WithMessage("Name must be between 1 and 100 characters.")
.MustAsync(BeUniqueName).WithMessage("The party with the same name already exists");

RuleFor(x => x.CountryCode)
.IsInEnum().WithMessage("Invalid country code.");

RuleFor(x => x.Type)
.IsInEnum().WithMessage("Invalid party type.");
}

private async Task<bool> BeUniqueName(string name, CancellationToken cancellationToken)
{
var partyByNameSpecification = new PartyByNameSpecification(name);
return !await _partyRepository.AnyAsync(partyByNameSpecification, cancellationToken);
}
}

There, we validate each property of AddPartyCommand separately by specifying validation rules. We check that enum values are within the range of the enum type, and what is more important is that the name is not empty, its length is between 1 and 100 characters, and it is unique in the repository.

Now, I need to set my MediatR's command execution so that the validator validates the command before executing the command handler. For this, I will create a new general MediatR's Pipeline Behavior, executing all validators associated with the specific command before command handler execution.

using FluentResults;
using FluentValidation;
using FluentValidation.Results;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace InWestMan.PipelineBehaviors;

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>
where TResponse : class
{
private readonly bool _hasValidators;
private readonly ILogger<ValidationBehavior<TRequest, TResponse>> _logger;
private readonly IEnumerable<IValidator<TRequest>> _validators;

public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidationBehavior<TRequest, TResponse>> logger)
{
_validators = validators;
_logger = logger;
_hasValidators = _validators.Any();
}

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
_logger.LogTrace("Handling {RequestType}", typeof(TRequest).Name);

if (!_hasValidators)
{
_logger.LogTrace("No validators configured for {RequestType}", typeof(TRequest).Name);
return await next();
}

var context = new ValidationContext<TRequest>(request);
ValidationResult[] validationResults =
await Task.WhenAll(_validators.Select(v =>
v.ValidateAsync(context, cancellationToken)));

List<ValidationFailure> failures =
validationResults.SelectMany(r => r.Errors)
.Where(f => f != null).ToList();

if (!failures.Any())
{
_logger.LogTrace("Validation passed for {RequestType}", typeof(TRequest).Name);
return await next();
}

var errorJson = JsonConvert.SerializeObject(failures);
_logger.LogWarning("Validation failed for {RequestType}: {Errors}", typeof(TRequest).Name, errorJson);

if (IsResultType(typeof(TResponse)))
return (dynamic)Result.Fail(errorJson);

throw new ValidationException(failures);
}

private static bool IsResultType(Type responseType)
{
return responseType == typeof(Result) ||
(responseType.IsGenericType
&& responseType.GetGenericTypeDefinition() == typeof(Result<>));
}
}

Now, this is one of the most complicated parts. When a new command is sent to a MediatR for handling, it previously goes through the ValidationBehavior pipeline. At first, it checks if the command has any validators associated with it. If yes, it executes all validators and waits for the results. In the case of any failure, it checks if the command response is a Result type to create a new failed Result object. If not, the ValidationException will be thrown.

The last thing we must do is register in the service collection Dependency Injection (DI) container.

using FluentValidation;
using InWestMan.PipelineBehaviors;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

namespace InWestMan;

public static class ApplicationServiceRegistration
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies());

return services;
}
}

We add MediatR with all Requests in the assembly, ValidationBehavior, and all FluentValidation classes.

Conclusion

Effectively handling validation errors is crucial to maintaining an application's integrity and reliability. Using FluentValidation and MediatR, we can create a clean and maintainable architecture, ensuring that validation logic is robust and separate from business logic. This approach adheres to the Single Responsibility Principle (SRP) and enhances an application's modularity and scalability.

Using these tools, we can handle validation errors more effectively, provide immediate feedback to users, and ensure that only valid data is processed and stored. This improves the user experience and helps maintain the integrity of the application's data.

Credits: DALL·E generated

Subscribe for more insights and in-depth analysis:

#SOLID #CQRS #DDD #InWestMan

--

--

Kostiantyn Bilous
SharpAssembly

Senior Software Engineer (.Net/C#) at SimCorp, Ph.D. in Finance