Fluent Validation — Validando Modelos

Daniel Giraldo
11 min readJun 12, 2024

En el capítulo pasado hablamos acerca de CQRS y su implementación haciendo uso del paquete MediatR. En este capítulo continuaremos evolucionando la API que creamos e implementaremos validaciones sobre nuestros modelos. Si no has visto este capítulo acá te dejo el link a ese post y el enlace para que descargues el código.

Cada vez que procesamos una petición sobre nuestros componentes debemos asegurarnos que la data recibida cumpla con el contrato y, de esta forma garantizar que los datos recibidos puedan ser procesados. Algunas veces creamos validaciones individuales o hacemos uso de Data Annotations, lo que puede llevarnos a violar el principio de unica responsabilidad, adicional los Data Annotations no son tan dicientes y las validaciones se pueden tornar un poco complejas de entender. Por lo tanto, hoy aprenderemos otra alternativa y para mi, una mejor forma de hacer estas validaciones.

Image source: Created by Author

¿Qué es FluenValidation?

FluentValidation es una librería que nos permite realizar validaciones de forma clara y fluida, lo que nos permite indicar de una forma fácil y natural como se realiza la validación de un objeto.

Una de las ventajas al implementar FluentValidation es su sintaxis fluida y expresiva, lo que nos ayuda a definir y mantener de una forma más sencilla las diferentes validaciones que creemos. Adicionalmente a esto, tendremos una mejor separación entre las reglas de validación y los modelos de datos, lo que nos permitirá mejorar la modularidad y reutilización del código. Sin olvidar que estas validaciones se pueden realizar de forma asíncrona, lo que nos permite incrementar el rendimiento de nuestra aplicación.

La idea de esta serie de post es construir una aplicación en la cual hagamos uso de los diferentes temas vistos en cada capítulo. Por eso, a continuación usaremos la API de clientes en la cual implementamos CQRS en el capítulo anterior, si no tienes el código aquí te dejo el link para que lo descargues o veas el código en mi repositorio de GitHub. En este proyecto haremos la implementación de FluentValidation y empezaremos a hacer uso de las características básicas de este. Para esto puedes seguir los siguientes pasos o si lo deseas puedes descargar el código desde mi repositorio en GitHub:

  1. Abrir la solución previamente descargada.
  2. Una vez abierto el proyecto procederemos a instalar el paquete Nuget FluentValidation en el proyecto Application.

En el capítulo anterior agregamos validaciones de forma manual sobre el manejador del comando para crear un cliente, en este capítulo vamos a mejorar estas validaciones haciendo un refactor sobre la forma en la que hicimos estas.

3. Vamos a proceder a crear la clase CreateCustomerCommandValidator en la cual haremos uso del paquete FluentValidation para validar el comando de creación de clientes, obteniendo la siguiente implementación:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
}
}

Esta clase hereda la clase AbstractValidator<T> del paquete FluentValidation, donde T corresponde al modelo de datos que deseamos validar, en este caso el modelo es la clase CreateCustomerCommand.

4. Implementamos nuestras reglas de validación de la siguiente forma:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty();
}
}

Como vemos en el código anterior, las validaciones son implementadas en el constructor de nuestra clase de validación. En esta hacemos uso del método RuleFor, en el cual declaramos una expresión lambda donde indicamos la propiedad a validar y por último las validaciones que vamos a aplicar sobre este. Como lo indicamos al inicio, las validaciones con FluentValidation son mucho más fáciles de entender gracias a la sintaxis que usamos, en el caso anterior estamos validando que el campo no esté vacío. La librería nos da la capacidad de tener varias validaciones para un mismo campo, así que a continuación veremos un ejemplo de cómo hacerlo.

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty().MaximumLength(10);
}
}

Como podemos observar tenemos 2 validaciones anidadas, la primera en la cual validamos que la propiedad Identification no esté vacía y en la nueva validamos que la longitud máxima de caracteres de la propiedad sea 10.

Estas son solo algunas validaciones que tenemos disponibles al hacer uso de FluentValidation. En el siguiente link podemos ver todas las validaciones predefinidas que tiene la librería.

¿Qué sucede si ninguna de las validaciones se ajusta a lo que necesitamos? en este caso podemos hacer uso del método Must, el cual nos permite tener validaciones personalizadas, a continuación podemos ver un ejemplo de este:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
List<string> forbiddenNames = new List<string>() {"Daniel", "Peter"};
RuleFor(x => x.Identification).NotEmpty();
RuleFor(x => x.FirstName)
.NotEmpty()
.Must(firstName => !forbiddenNames.Contains(firstName));
}
}

En la validación que agregamos estamos validando que el nombre de nuestro cliente no se encuentre en la lista de nombres prohibidos (es solo un ejemplo para enseñar lo que podemos hacer con el paquete). El código anterior lo podemos mejorar encapsulando nuestra validación personalizada en un método de la siguiente forma:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty();
RuleFor(x => x.FirstName)
.NotEmpty()
.Must(firstName => !IsValidName(firstName));
}

private bool IsValidName(string firstName)
{
List<string> forbiddenNames = new List<string>() { "Daniel", "Peter" };
return !forbiddenNames.Contains(firstName);
}
}

En el ejemplo anterior creamos el método IsValidName en el cual encapsulamos la validación relacionada al nombre del cliente, haciendo el código más legible y mantenible. Otra de las características que nos ofrece FluentValidation son los mensajes personalizados sobre las validaciones a través del método WithMessage, en el caso de la validación que acabamos de implementar vamos a crear un mensaje de respuesta personalizado de la siguiente forma:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty();
RuleFor(x => x.FirstName)
.NotEmpty()
.Must(firstName => !IsValidName(firstName))
.WithMessage("FirstName must be a value different from Daniel and Peter");
}

private bool IsValidName(string firstName)
{
List<string> forbiddenNames = new List<string>() { "Daniel", "Peter" };
return !forbiddenNames.Contains(firstName);
}
}

FluentValidation nos da la posibilidad de retornar los mensajes de validación predeterminados en varios idiomas. Por defecto se utilizará el idioma especificado en la cultura del componente. En caso de querer retornar los mensajes en un idioma específico podemos seguir las instrucciones indicadas en este link.

¿Qué pasa si tenemos validaciones que dependen de una condición previa?

FluentValidaton nos provee el método When, a través del cual podemos implementar validaciones que son condicionales, para este caso vamos a agregar las propiedades Address, Country y City sobre el modelo CreateCustomerCommand y la entidad de dominio Customer. Después crearemos dos validaciones adicionales sobre el campo Country y City en nuestro validador. Ambas validaciones van a depender de la propiedad Address de la siguiente forma:

  • Si el valor de la propiedad Address es diferente de vacío, entonces, tanto el campo Country como City deben tener un valor, no pueden estar vacíos.
  • Si el valor de la propiedad Address es vacío, entonces, tanto el campo Country como City deben estar vacíos también.
public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty();
RuleFor(x => x.FirstName)
.NotEmpty()
.Must(firstName => !IsValidName(firstName))
.WithMessage("FirstName must be a value different from Daniel and Peter");
When(x => !string.IsNullOrEmpty(x.Address),
() => RuleFor(x => x.Country).NotEmpty())
.Otherwise(() => RuleFor(x => x.Country).Empty());
When(x => !string.IsNullOrEmpty(x.Address),
() => RuleFor(x => x.City).NotEmpty())
.Otherwise(() => RuleFor(x => x.City).Empty());
}

private bool IsValidName(string firstName)
{
List<string> forbiddenNames = new List<string>() { "Daniel", "Peter" };
return !forbiddenNames.Contains(firstName);
}
}

Las validaciones anteriores son legibles, pero podemos mejorarlas y hacer que nuestra clase de validación sea más limpia. FluentValidation nos provee el método Include, este método nos permite agregar reglas de validación de otro validador en un validador principal. Esto es muy útil cuando tenemos reglas que pueden aplicar para múltiples propiedades o tipos de datos y nos brinda la posibilidad de que nuestro código sea mucho más limpio, legible y mantenible. A continuación podemos ver un ejemplo de como hacer uso del método Include:

Crearemos 3 validadores en los cuales vamos a realizar de forma individual las validaciones sobre los campos FirstName, Country y City de la siguiente forma:

public class FirstNameValidator : AbstractValidator<CreateCustomerCommand>
{
public FirstNameValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty()
.Must(firstName => !IsValidName(firstName))
.WithMessage("FirstName must be a value different from Daniel and Peter.");
}

private bool IsValidName(string firstName)
{
List<string> forbiddenNames = new List<string>() { "Daniel", "Peter" };
return forbiddenNames.Contains(firstName);
}
}
public class CountryValidator : AbstractValidator<CreateCustomerCommand>
{
public CountryValidator()
{
When(x => !string.IsNullOrEmpty(x.Address),
() => RuleFor(x => x.City).NotEmpty())
.Otherwise(() => RuleFor(x => x.City).Empty());
}
}
public class CityValidator : AbstractValidator<CreateCustomerCommand>
{
public CityValidator()
{
When(x => !string.IsNullOrEmpty(x.Address),
() => RuleFor(x => x.Country).NotEmpty())
.Otherwise(() => RuleFor(x => x.Country).Empty());
}
}

Luego de crear los validadores agregaremos estos a nuestra clase de validación principal haciendo uso del método Include de la siguiente forma:

public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand>
{
public CreateCustomerCommandValidator()
{
RuleFor(x => x.Identification).NotEmpty();
Include(new FirstNameValidator());
Include(new CityValidator());
Include(new CountryValidator());
}
}

Como podemos observar el código en nuestro validador principal quedó mucho más limpio, incluso podríamos mejorarlo creando un validador adicional para el campo Identification y así tendríamos todas las validaciones independientes.

public class IdentificationValidator : AbstractValidator<CustomerBase>
{
public IdentificationValidator()
{
RuleFor(x => x.Identification).NotEmpty();
}
}

Crear validaciones por campo nos permite segregar nuestro código y con esto potenciar el principio de única responsabilidad. Pero hay algo adicional, y es que estas validaciones podrían ser reutilizadas por otros validadores. Por ejemplo, si en la aplicación con la que estamos haciendo esta práctica tuviéramos un comando de actualización de usuario, sería muy probable que necesitáramos hacer las mismas validaciones del comando de creación de usuario o unas muy similares, en este caso podríamos reutilizar los validadores que creamos. Sin embargo debemos tener algo muy importante en cuenta, y es que en un validador solo podemos incluir validadores que validen el mismo modelo del validador principal, suena un poco enredado, lo sé. Para explicártelo de una mejor forma te enseño un ejemplo: el validador CreateCustomerCommandValidator valida el modelo CreateCustomerCommand. Por lo tanto, los validadores que incluyamos en este validador también deben validar el modelo CreateCustomerCommand, eso fue lo que hicimos. Ahora, en caso de tener el comando de actualización de usuario y querer reutilizar estos validadores, entonces deberíamos hacer un refactor sobre nuestro código, este consistiría en lo siguiente:

  • Creamos una clase base para nuestro modelo de datos con las propiedades comunes.
  • Heredamos esa clase desde ambos comandos, Crear Usuario y Actualizar Usuario.
  • Cambiamos el modelo que validamos sobre nuestros validadores secundarios, en este caso, ya no validaríamos el modelo CreateCustomerCommand, sino que validaríamos la clase base que creamos en el primer punto.
  • El modelo que validamos en nuestros validadores principales continuaría siendo el comando, CreateCustomerCommand para el validador del comando de creación de usuario y UpdateCustomerCommand para el validador del comando de actualización de usuario.

De esta forma podríamos reutilizar las validaciones y sacarle el máximo provecho a estas, pero es tu decisión cómo hacerlo.

¿Y si queremos validar alguna propiedad del modelo con una base de datos?

Hasta el momento no tenemos ninguna validación sobre nuestra API para identificar si un cliente ya existe o no. Por lo tanto, un cliente se puede repetir y esa no es la idea. Para validar si un cliente ya existe es necesario consultar en base de datos si la identificación ya se encuentra registrada. ¿Cómo podemos hacer eso? FluentValidation nos permite hacer uso de inyección de dependencias, gracias a esto podemos inyectar nuestro repositorio en el validador donde requerimos hacer esta validación. Así que manos a la obra y vamos a organizar nuestro código para agregar esta validación.

Como primer paso inyectamos nuestro repositorio en el validador, luego haremos uso del método MustAsync que nos provee FluentValidation. Este método recibe 2 parámetros, el primer parámetro es el valor de la propiedad que se está validando y el segundo parámetro es CancellationToken. A continuación podremos ver como sería la implementación de nuestra validación:

public class IdentificationValidator : AbstractValidator<CustomerBase>
{
private readonly ICustomersRepository _customersRepository;
public IdentificationValidator(ICustomersRepository customersRepository)
{
_customersRepository = customersRepository;
RuleFor(x => x.Identification)
.NotEmpty()
.MustAsync(IsUniqueIdentification).WithMessage("'{PropertyName}' Customer with identification {PropertyValue} already exist.");
}

private async Task<bool> IsUniqueIdentification(string identification, CancellationToken token)
{
var customer = await _customersRepository.GetById(identification);
return customer == null;
}
}

El código te lanzará un error ya que este validador lo estamos incluyendo en nuestro validador principal a través de una nueva instancia. Por lo tanto, requerimos inyectar el repositorio en nuestro validador principal y, desde este, inyectarlo a la instancia del validador de la propiedad Identification.

De esta forma tendremos implementada nuestra validación para saber si el usuario ya existe. Como puedes observar hay algo nuevo en el mensaje personalizado que aún no hemos visto, estos son los placeholders y nos sirven para obtener el valor de algunos atributos sobre nuestras validaciones, en el ejemplo anterior estamos usando los placeholders PropertyName y PropertyValue, que corresponden al nombre de la propiedad y su valor respectivamente. En el siguiente link puedes encontrar más información al respecto sobre los placeholders.

Nota: Es posible implementar las validaciones de negocio agregándolas sobre nuestros validadores como lo acabamos de hacer con la validación de la identificación del cliente. Personalmente me gusta separar las validaciones de contrato de las validaciones de negocio. Por lo general, FluentValidation lo utilizó para validar las peticiones sobre la aplicación y, las reglas de negocio las implemento directamente en la capa de dominio sin hacer uso de ningún paquete. Esto solo es un gusto personal, yo te muestro las diferentes alternativas y tu decides cual es la que más te gusta y te conviene para tu proyecto.

5. Una vez tenemos nuestras reglas implementadas procederemos a ejecutarlas para validar nuestro modelo. En este caso haremos un refactor sobre el handler del comando, para que las validaciones que realizamos de forma manual en el capítulo anterior, ahora se ejecuten mediante la implementación que hicimos con FluentValidation. Obteniendo la siguiente implementación:

public async Task<Result<CreateCustomerCommandResponse>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
CreateCustomerCommandValidator customerValidator = new CreateCustomerCommandValidator();
var validationResult = await customerValidator.ValidateAsync(request);
if (validationResult.IsValid)
{
var customer = new Customer(request.Identification, request.FirstName, request.LastName, request.Email, request.BirthDate, request.Address, request.City, request.Country);
var result = await _createCustomerUseCase.Execute(customer);
return Result<CreateCustomerCommandResponse>.Success(MapToResponse(result));
}

return Result<CreateCustomerCommandResponse>.Failure(validationResult.ToString());
}

De esta forma estaremos ejecutando las validaciones sobre nuestro modelo, sin embargo podemos mejorar algo en nuestro código y es realizar la inyección de nuestro validador a través del contenedor de dependencias siguiendo las siguientes instrucciones:

A. Instalar el paquete FluentValidation.DependencyInjectionExtensions sobre el proyecto Application.

B. Registrar nuestros validadores en el contenedor de dependencias. En este caso lo haremos a través del método de extensión que creamos sobre la interfaz IServiceCollection en la clase ApplicationDI agregando la siguiente línea de código:

services.AddValidatorsFromAssembly(typeof(CreateCustomerCommandValidator).Assembly);

De esta forma estaremos inyectando todos los validadores que se encuentren en el Assembly donde se encuentra ubicada la clase CreateCustomerCommandValidator.

C. Inyectar nuestro validador en el manejador del comando que es la clase donde haremos uso de este. El código final sería el siguiente:

public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, Result<CreateCustomerCommandResponse>>
{
private readonly ICreateCustomerUseCase _createCustomerUseCase;
private readonly IValidator<CreateCustomerCommand> _customerValidator;
public CreateCustomerCommandHandler(ICreateCustomerUseCase createCustomerUseCase, IValidator<CreateCustomerCommand> customerValidator)
{
_createCustomerUseCase = createCustomerUseCase;
_customerValidator = customerValidator;
}

public async Task<Result<CreateCustomerCommandResponse>> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
var validationResult = await _customerValidator.ValidateAsync(request);
if (validationResult.IsValid)
{
var customer = new Customer(request.Identification, request.FirstName, request.LastName, request.Email, request.BirthDate, request.Address, request.City, request.Country);
var result = await _createCustomerUseCase.Execute(customer);
return Result<CreateCustomerCommandResponse>.Success(MapToResponse(result));
}

return Result<CreateCustomerCommandResponse>.Failure(validationResult.ToString());
}

private CreateCustomerCommandResponse MapToResponse(Customer customer)
{
return new CreateCustomerCommandResponse(customer.Identification, customer.FirstName, customer.LastName, customer.Email, customer.BirthDate, request.Address, request.City, request.Country);
}
}

Este fue un ejemplo básico acerca de cómo implementar el paquete FluentValidation, en próximos capítulos veremos cómo realizar la implementación de pruebas unitarias sobre nuestros validadores y cómo podemos evolucionar nuestras validaciones para que se realicen de forma automática al ejecutar nuestras consultas y comandos.

Los temas explicados son una serie de consejos y buenas prácticas en el desarrollo de software, pero al final eres tú quien decide que usar o no de acuerdo a las necesidades del proyecto en el que te encuentres participando. En ocasiones al usar muchos paquetes en tus proyectos puedes estar agregando complejidad innecesaria a estos. Por lo tanto, te recomiendo siempre evaluar si es necesario o no usar el paquete y los beneficios y desventajas que te trae el implementarlo.

Gracias por leerme, espero te haya gustado y te sea de utilidad este capítulo, recuerda que el código del proyecto lo puedes encontrar en este link y, en mi perfíl de GitHub podrás encontrar otros recursos que te pueden ser de utilidad.

--

--