Enmilocalfunciona

Thoughts, stories and ideas.

Valida tu modelo de datos con FluentValidation en C#

Publicado por Daniel Pazos el

Microsoft.NET5Desarrollo Aplicaciones

Introducción

Como programadores somos conscientes de la importancia que tiene validar los datos que los usuarios introducen en nuestra aplicación, bien a través de un formulario en una aplicación o de una petición a nuestra API.

Si algo nos ha dado la experiencia es que no puedes fiarte de los datos introducidos por los usuarios, esos seres malignos que tarde o temprano encontrarán ese caso de uso que no habías contemplado; o peor aún, del QA, que sin lugar a duda usarán los valores límite y más allá para comprobar la robustez de nuestro software.

¿Fluent Validation o Data Annotations?

Seguramente leyendo el título de este artículo tu primer pensamiento haya sido: “Mis modelos ya se validan y limitan con Data Annotations. ¿Qué me aportaría usar Fluent Validation?”.

Data Annotations es perfectamente válido y te aporta sencillez a la hora de hacer la validación de tus modelos pero, desde mi punto de vista, tiene ciertas carencias e inconvenientes que no encontraremos en Fluent Validation.

Uno de estos inconvenientes de los que hablamos es que desde el momento en que usas Data Annotations con tu modelo estás creando una dependencia entre tus reglas de validación y tu modelo, incumpliendo así uno de los principios SOLID: el Separation of Concerns.

Otro “pero” es que el atributo [Required], por ejemplo, no te aporta mucha flexibilidad sobre la validación. ¿Cómo harías si una propiedad es obligatoria en función de otra?, ¿O si necesitas aplicar cierta lógica para saber que el campo es correcto? Con Fluent Validation veremos cómo hacerlo de una forma muy sencilla.

Y, por último, las pruebas unitarias de tu modelo con Data Annotations son más complejas que con Fluent Validation, donde tu validación es una clase separada y totalmente testeable.

La idea de este artículo es mostrarte las funcionalidades más destacadas y las ventajas de usar Fluent Validation. No veremos todo su potencial ni funcionalidades pero espero que sirva como una base sólida con la que comenzar a usarlo. Así que basta de cháchara y enseñemos algo de código.

Creando nuestro proyecto de prueba

Para ilustrar las distintas posibilidades de Fluent Validation, vamos a crear una sencilla aplicación de escritorio que nos permitirá dar de alta usuarios en nuestra compañía de seguros de coche.

NOTA: Este ejemplo, que podrás descargar de mi github aquí, ha sido creado usando el framework .Net 5 pero podrás usar cualquier framework de .NET que quieras.

Una vez creado el proyecto, lo primero será importar el paquete nuget correspondiente. Si tu proyecto es un proyecto web de tipo ASP.Net Core, deberás seleccionar el paquete FluentValidation.AspNetCore, para el resto de proyectos FluentValidation se ajustará a lo que necesites.

Bien con el Gestor de paquetes nuget:

O desde la Consola de paquetes:

Install-Package FluentValidation  
Install-Package FluentValidation.AspNetCore  

Empezaremos por lo importante y crearemos el modelo de datos de usuarios a validar. En este caso, tendremos el nombre del asegurado, su apellido, fecha de nacimiento, si tiene coche y, si es así, la matrícula del coche.

public class UserModel  
{
    public string Name { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool HasCar { get; set; }
    public string PlateNumber { get; set; }
}

Y finalmente, un formulario desde el que dar de alta los clientes y que nos mostrará los errores de validación que se irán produciendo.

Con esto ya tenemos la base para empezar a hacer nuestras primeras validaciones y "trastear" un poco con Fluent Validation.

Un ejemplo sencillo

Las validaciones con Fluent Validation se crean en una clase que herede de AbstractValidator<T>, donde T es el tipo de la clase que queremos validar.

using FluentValidation;

public class UserValidator : AbstractValidator<UserModel>  
{

}

Todas las reglas que vayamos creando se declararán dentro del constructor de nuestra clase de validación.

Para crear una regla de validación para una propiedad usaremos el método RuleFor con una expresión lambda donde se especifica la propiedad a validar y la validación que aplicaremos sobre esa propiedad, bien sea una validación predefinida o una personalizada como veremos más adelante.

Empecemos con una validación sencilla, que se podría corresponder al [Required] de Data Annotations, que será comprobar que se haya indicado el nombre de nuestro asegurado.

using FluentValidation;

public class UserValidator : AbstractValidator<UserModel>  
{
    public UserValidator () 
    { 
        RuleFor(user => user.Name).NotEmpty();
    }
} 

Como puedes ver, al usar una sintaxis fluida el método es muy sencillo de leer. De un solo vistazo y sin saber nada de Fluent Validation sabemos qué propiedad estamos validando y qué validación es.

¡Y mejor aún! Nuestro modelo de datos no tiene ninguna dependencia ni vínculo con esta validación.

NotEmpty es solo una de las muchas validaciones que Fluent Validation tiene predefinidas para nosotros. Seguiremos viendo algunas más en este artículo, pero si quieres ver la lista completa de validaciones puedes consultarlas aquí

Ampliemos un poco más nuestra validación. Solo viendo el código, ¿qué crees que estamos validando ahora mismo?

using FluentValidation;

public class UserValidator : AbstractValidator<UserModel>  
{
    public UserValidator () 
    {
        RuleFor(user => user.Name).NotEmpty().Length(2,50);
    }
}

Lo sé, ha sido insultantemente sencillo. El nombre de nuestro usuario no podrá ser vacío y deberá tener entre 2 y 50 caracteres.

Con esto quería mostrarte como Fluent Validation nos permite encadenar tantas reglas de validación para una misma propiedad como queramos.

Llamando a nuestra validación

Para poder ejecutar la validación, invocaremos el método Validate que heredamos en nuestra clase UserValidator. Para ello, en nuestro ejemplo añadiremos al botón “Validar” de nuestro formulario el evento Click con el siguiente código:

private void ValidateButton_Click(object sender, EventArgs e)  
{
    //Limpiamos los mensajes de error de anteriores validaciones
    ErrorTextBox.Clear();

    var user = new UserModel()
    {
        Name = NameTextBox.Text,
        LastName = LastnameTextBox.Text,
        HasCar = HasCarCheckBox.Checked,
        PlateNumber = PlateTextBox.Text
    };

    var validator = new UserValidator();

    ValidationResult result = validator.Validate(user);

    if (result.IsValid)
    {
        ErrorTextBox.AppendText("Todo correcto");
    }
    else
    {
        //Incluimos todos los errores de validación en nuestra caja de texto de errores
        foreach (var error in result.Errors)
        {
            ErrorTextBox.AppendText(error.ErrorMessage);
            ErrorTextBox.AppendText(Environment.NewLine);
        }
    }
}

Lo realmente importante de este código, y con lo que debes de quedarte principalmente, es con la llamada al método Validate y la respuesta del método, que es un objeto de tipo ValidationResult.

El ValidationResult nos indica si la instancia de nuestro modelo que le pasamos cumple todas nuestras reglas (IsValid) o, de no cumplirlas, una lista con los errores de validación.

¿Y cuál es el resultado de ejecutar nuestro pequeño programa sin indicarle ningún valor en ninguno de los campos del formulario?

Lo primero que nos llama la atención es que, aunque no hemos especificado ningún mensaje de error, ya se nos devuelven 2 mensajes predefinidos en función de las reglas de validación que hemos usado (NotEmpty y Length) usando como idioma el que tengamos especificado en CultureInfo.CurrentUICulture de nuestro framework.

Personalizando los mensajes de respuesta

Si no quieres usar los mensajes de respuesta predefinidos de Fluent Validation puedes personalizar el mensaje de respuesta con el método WithMessage con el texto deseado de la siguiente forma:

RuleFor(user => user.Name)  
    .NotEmpty().WithMessage("No ha indicado el nombre de usuario.")
    .Length(2,50).WithMessage("El nombre debe tener una longitud entre 2 y 50 caracteres");

Fíjate que hemos repetido la llamada al método .WithMessage() por cada una de las validaciones que hacemos. Si solo quisiésemos usar un único mensaje de error bastaría con colocarlo al final de todas las validaciones.

Volvamos a hacer la misma prueba de antes y veamos cómo ha cambiado.

Podemos ir un poco más allá y personalizar nuestros mensajes con el uso de placeholders dentro del texto.

Algunos de estos placeholders son:

  • {PropertyName} que contiene el nombre de la propiedad a validar.
  • {PropertyValue} que contiene el valor de la propiedad a validar
  • {MinLength} que en las validaciones de tipo Length indica el nº de caracteres mínimo a introducir.
  • {MaxLength} que en las validaciones de tipo Length indica el nº de caracteres máximo a introducir.
  • {TotalLength} que en las validaciones de tipo Length indica el nº de caracteres introducido
Si quieres consultar la lista completa puedes hacerlo aquí

Hagamos uso de estos placeholders modificando nuestro mensaje de error para la validación de la longitud del nombre:

RuleFor(user => user.Name)  
    .NotEmpty().WithMessage("No ha indicado el nombre de usuario.")
    .Length(2,50).WithMessage("{PropertyName} tiene {TotalLength} letras. Debe tener una longitud entre {MinLength} y {MaxLength} letras.");

Si probamos a romper esta validación introduciendo un solo carácter en el nombre, el resultado será algo como esto:

La ventaja de usar estos placeholders es que nos evitan cometer errores por omisión, es decir, si cambiásemos el rango permitido para el nombre a 5 y 100 no tendríamos que tocar el texto de error ya que coge esos valores directamente de la regla.

Mensajes en cascada

Con nuestro código actual, si no introducimos el nombre del usuario, nos devuelve todos los mensajes de error de las reglas para esa propiedad. ¿Pero podríamos hacer que solo nos mostrase el primer mensaje? Podemos, usando el método Cascade.

Cascade nos permite indicarle el comportamiento que queremos para nuestras reglas pasándole como parámetro uno de los dos valores permitidos:

  • CascadeMode.Continue => Se validan todas las reglas y se devuelven todos los errores (valor por defecto).
  • CascadeMode.Stop => La validación se detiene en el primer error producido.

Así para conseguir que nuestra aplicación solo muestre el primer mensaje necesitaríamos cambiar nuestra regla a esto:

RuleFor(user => user.Name)  
    .Cascade(CascadeMode.Stop)
    .NotEmpty()
    .WithMessage("No ha indicado el nombre de usuario.")
    .Length(2,50)
    .WithMessage("{PropertyName} tiene {TotalLength} letras. Debe tener una longitud entre {MinLength} y {MaxLength} letras.");

De esta forma, en cuanto se incumpla la primera regla de validación de las que tengamos definidas, se detiene la validación y se devuelve el mensaje de error.

Validaciones dependientes de otros campos

Una de las cosas que considero más interesantes de Fluent Validation es la posibilidad de poder aplicar reglas en función de otras propiedades.

Por ejemplo, podemos comparar dos propiedades para evitar que sean iguales o, al contrario, que sean iguales como cuando confirmamos una contraseña al darnos de alta en una web. Para ello haremos uso de otra regla predefinida: NotEqual.

Veamos un ejemplo de esta regla en nuestra aplicación validando que el nombre y apellido de nuestro usuario no sean iguales.

RuleFor(user => user.Name).NotEqual(user => user.LastName);  

De esta forma mostraremos un mensaje cada vez que nombre y apellido sean iguales evitando que Jar Jar pueda darse de alta en nuestra aplicación.

Lo siento, Jar Jar. No en nuestro sistema.

Otro uso muy interesante de validaciones dependientes entre propiedades es el hecho de poder aplicar o no una regla en función del valor de otra propiedad.

Para ello disponemos de dos métodos: When y Unless.

Si recuerdas, uno de los requisitos de nuestra aplicación era que si marcábamos la opción de 'Tiene coche', teníamos que indicar la matrícula del coche del usuario.

Esto podemos conseguirlo haciendo uso del método When de esta forma:

RuleFor(user => user.PlateNumber).Length(7,12).When(user => user.HasCar);  

De una forma muy sencilla hemos creado una regla para validar que la matrícula tenga entre 7 y 12 caracteres solo si el usuario ha marcado la opción de que tiene coche (HasCar).

Extendiendo nuestras validaciones

Hasta el momento hemos podido ver parte de las validaciones predefinidas de Fluent Validation y como enlazarlas entre propiedades. ¿Pero qué pasa si ninguna de las validaciones predefinidas cumple con tus necesidades?

Para estos casos podemos crear validaciones personalizadas. Hay varias formas de crear validaciones personalizadas pero la más sencilla es usando el método Must. Veamos un ejemplo:

List<string> blackListWords = new List<string>() {“caca”, “culo,”pedo”, “pis”};  
RuleFor(user => user.LastName).Must(lastname => !blackListWords.Contains(lastname));  

Con este ejemplo hemos implementado una regla para evitar que algún usuario troll intente introducir algún texto para el apellido que consideremos inapropiado y tengamos en nuestra lista negra.

Otro uso interesante del método Must es la posibilidad de pasarle como parámetro una función con nuestra validación para que nuestro código sea más limpio y poder reaprovechar una validación en otra propiedad. Como ejemplo, veamos como indicarle a nuestro sistema que solo los mayores de 18 años pueden darse alta.

public class UserValidator : AbstractValidator<UserModel>  
{       
    public UserValidator()
    {
        RuleFor(user => user.BirthDate)
            .Must(IsOver18)
            .WithMessage("Tiene que ser mayor de edad para poder registrarse.");
    }

    private bool IsOver18(DateTime birthDate)
    {
        return DateTime.Now.AddYears(-18) >= birthDate;
    }
}

Agrupando validaciones

Espero que llegados a este punto sepas cómo validar tu modelo con las reglas predefinidas, crear tus propias validaciones si las necesitases y mostrar textos de error acordes, tanto genéricos como personalizados. Si hacemos un repaso de nuestro código debería ser algo así:

public class UserValidator : AbstractValidator<UserModel>  
{       
    public UserValidator()
    {
        RuleFor(user => user.Name)
            .Cascade(CascadeMode.Stop)
            .NotEmpty()
            .WithMessage("No ha indicado el nombre de usuario.")
            .Length(2,50)
            .WithMessage("{PropertyName} tiene {TotalLength} letras. Debe tener una longitud entre {MinLength} y {MaxLength} letras.");

        RuleFor(user => user.Name).NotEqual(user => user.LastName);

        RuleFor(user => user.PlateNumber).Length(7,12).When(user => user.HasCar);

        List<string> blackListWords = new List<string>();
        RuleFor(user => user.LastName).Must(name => !blackListWords.Contains(name));

        RuleFor(user => user.BirthDate)
                .Must(IsOver18)
                .WithMessage("Tiene que ser mayor de edad para pder registrarse.");
        }

        private bool IsOver18(DateTime birthDate)
        {
            return DateTime.Now.AddYears(-18) >= birthDate;
        }
    }
}

Más allá de ser un ejemplo básico para iniciarte en FluentValidation no hay nada malo con nuestro código pero sí que hay un punto de mejora.

Como firmes defensores del Clean Code que somos vamos a refactorizar nuestro ejemplo para evitar, por un lado que el constructor tenga un numero de lineas excesivo, y para seguir el Principio de responsabilidad única. Para ello separaremos nuestras reglas en distintas clases que luego agruparemos e incluiremos en nuestra clase de validación.

La manera de hacer esto es a través del uso del método Include que nos permite realizar esta agrupación de reglas dentro de nuestra clase.

public class UserValidator : AbstractValidator<UserModel>  
{       
    public UserValidator()
    {
        Include(new UserNameIsSpecified());
        Include(new LastNameDistinctThanName());
        Include(new PlateNumberSpecifiedIfHasCar());
        Include(new LasTNameIsNotBlacklisted());
        Include(new UserIsOver18());
    }
}

public class UserNameIsSpecified : AbstractValidator<UserModel>  
{
    public UserNameIsSpecified()
    {
        RuleFor(user => user.Name)
            .Cascade(CascadeMode.Stop)
            .NotEmpty()
            .WithMessage("No ha indicado el nombre de usuario.")
            .Length(2, 50)
            .WithMessage("{PropertyName} tiene {TotalLength} letras. Debe tener una longitud entre {MinLength} y {MaxLength} letras.");
    }
}

public class LastNameDistinctThanName : AbstractValidator<UserModel>  
{
    public LastNameDistinctThanName()
    {
        RuleFor(user => user.Name).NotEqual(user => user.LastName);  
    }
}

public class PlateNumberSpecifiedIfHasCar : AbstractValidator<UserModel>  
{
    public PlateNumberSpecifiedIfHasCar()
    {
        RuleFor(user => user.PlateNumber).Length(7, 12).When(user => user.HasCar); 
    }
}

public class LasTNameIsNotBlacklisted : AbstractValidator<UserModel>  
{
    public LasTNameIsNotBlacklisted()
    {
        List<string> blackListWords = new List<string>();
        RuleFor(user => user.LastName).Must(name => !blackListWords.Contains(name));
    }
}

public class UserIsOver18 : AbstractValidator<UserModel>  
{
    public UserIsOver18()
    {
        RuleFor(user => user.BirthDate)
            .Must(IsOver18)
            .WithMessage("Tiene que ser mayor de edad para pder registrarse.");
    }

    private bool IsOver18(DateTime birthDate)
    {
        return DateTime.Now.AddYears(-18) >= birthDate;  
    }    
}

Como puedes ver la clave es el método Include que te permite agrupar las reglas que necesitas y mantener tu código limpio y bien estructurado.

Pruebas unitarias de nuestro validador

Con la refactorización anterior, realizar las pruebas unitarias de tu código es realmente sencillo. Todas nuestras validaciones están en clase separadas y bien organizadas por lo que realizar las pruebas unitarias correspondientes no debería tener ninguna complejidad.

Un ejemplo de prueba unitaria a la clase UserNameIsSpecified sería algo así:

[Fact]
public void UserNameIsNull()  
{
    var user = new UserModel
    {
        Name = null
    };

    var validator = new UserNameIsSpecified();
    var validationResult = validator.Validate(user);

    Assert.False(validationResult.IsValid);
}

Conclusiones

Espero que esta introducción a Fluent Validation te sirva como base y para sopesar si usarlo en tus proyectos o no.

Como decía al principio del artículo, creo que el hecho de tener tus validaciones totalmente independientes del modelo de datos es un punto muy a tener en cuenta a la hora de apostar por él, así como la posibilidad de usar una sintaxis fluida para crear tus reglas de una forma sencilla y con mucha versatilidad.

Respecto a usar Data Annotations o Fluent Validation depende de tus necesidades y requerimientos. Ambos tienen sus pros y contras.

Con Data Annotations al acceder a tu modelo ya ves de forma sencilla las reglas que se aplican a esa propiedad mientras que en Fluent Validation necesitarías navegar por las distintas clases creadas.

Por otro lado, si tus validaciones son complejas o requieren cierta lógica, Fluent Validation se convierte en un gran aliado facilitándote la vida enormemente mientras que con Data Annotations se apilarían los atributos a aplicar a la propiedad ensuciando tu código e incluso así, probablemente, tendrías que codificar las validaciones que necesitases.

Me he dejado cosas en el tintero para no alargar en exceso este artículo, y también porque el objetivo principal era servir como introducción a Fluent Validation, pero te animo a consultar la página oficial para descubrir el resto de posibilidades como el uso de ficheros de recursos para los mensajes de error, validaciones con expresiones regulares o cómo invocar las validaciones usando inyección de dependencias.

Si he despertado tu interés visita Fluent Validation para saber más, y si quieres bajarte la aplicación sobre la que hemos trabajado la tienes disponible en github aquí.

Y si te ha gustado, ¡síguenos en Twitter para estar informado de próximas entregas!