En toda aplicación, o bueno en practicamente todas es necesario validar las entidades y modelos que tenemos, para asegurar consistencia en la información y que el flujo que sigue cada proceso se ejecuta de forma correcta; por lo anterior, hoy quiero mostrar tres diferentes formas en las cuales es posible conseguir validar las entidades/modelos de la aplicación.

Para los ejemplos, tengo como base la plantilla de ASPNET Core Web Application, aunque en realidad no haremos nada raro.


Tenemos un controlador UsersController con un solo método GetUsers el cual tiene como finalidad retornar un listado de usuarios, dicho método recibe un objeto de tipo UserFilters que tiene los posibles filtros que se pueden aplicar para buscar los usuarios.

El siguiente es el controlador:


[Route("api/users")]
public class UsersController : Controller
{
    [HttpGet("getusers")]
    public IActionResult GetUser([FromQuery] UserFilters query)
    {
        // TODO: Validate UserFilters
        return Content("Searching users...");
    }
}

En la acción no voy a implementar la búsqueda de usuarios, ya que no es importante en el contexto del post que es solo validar los modelos, en este caso UserFilters, y bueno, la siguiente en la clase que tiene los posibles filtros:


public class UserFilters
{
    public string Name { get; set; }

    public string Email { get; set; }

    public int Age { get; set; }
}

Listo, ya tenemos la base que necesitamos para iniciar a validar los filtros, y la primera forma de validación disponible es la validación manual.

1. Validación manual

En la validación manual, en el código de la acción adicionamos todas las condiciones que se deben cumplir en el objeto de tipo UserFilters, por ejemplo, validar que el nombre no es nulo, que el email tiene el formato correcto, que la edad no es negativa, y todas las demás combinaciones disponibles, por lo tanto, adicionando estás validaciones en el método de la acción tenemos:


[HttpGet("getusers")]
public IActionResult GetUser([FromQuery] UserFilters query)
{
    // Validación Manual
    if (string.IsNullOrEmpty(query.Name))
        return BadRequest("El nombre es requerido");

    var isEmailValid = Regex.IsMatch(query.Email, @"\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\Z", RegexOptions.IgnoreCase);
    if (!isEmailValid)
        return BadRequest("El email no tiene un formato correcto");

    if (query.Age < 14 || query.Age > 120)
        return BadRequest("La edad debe estar en un rango entre 14 y 120");

    // more validation rules

    return Content("Searching users...");
}


Este tipo de validación manual tiene los siguientes problemas:

  • El código de la acción se vuelve extenso, y entre más reglas de validación menos legible se vuelve.
  • Duplicidad de código, ya que si se requiere validar en otra parte dicho modelo, se debe repetir las validaciones.
  • Agrega mayor dificultad para la creación de pruebas unitarias, ya que no tenemos las validaciones aisladas.

Ahora, vamos a pasar al segundo tipo de validación, en este caso utilizando DataAnnotatios.

2. DataAnnotations

EL segundo método de validación es utilizado DataAnnotations, que basicamente consiste en agregar atributos en las propiedades con las reglas necesarias, exiten un conjunto de atributos pre-definidos que son:

Ahora, si aplicamos los atributos a la clase UserFilters, tenemos lo siguiente:


public class UserFilters
{
    [Required(ErrorMessage = "El nombre es requerido")]
    public string Name { get; set; }

    [Required(ErrorMessage = "El email es requerido")]
    [EmailAddress(ErrorMessage = "El email no tiene un formato correcto")]
    public string Email { get; set; }

    [Required(ErrorMessage = "La edad es requerida")]
    [Range(14, 120, ErrorMessage = "La edad debe estar en un rango entre 14 y 120")]
    public int Age { get; set; }
}

Luego de definir las reglas en nuestro modelo, en la acción del controlador ya no es necesario validar cada unos de los campos, en este caso es suficiente con llamar a ModelState.IsValid que retonar un boolean indicando si el modelo es válido o no, reescribiendo el código de la acción tenemos:


[HttpGet("getusers")]
public IActionResult GetUser([FromQuery] UserFilters query)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    return Content("Searching users...");
}

AL igual que en la validación manual se retorna un código HTTP 400 (BadRequest) y le pasamos ModelState en lugar del texto asociado a cada error, validando el request se puede observar que efectivamente la respuesta tiene el texto definido en el atributo:

DataAnnotatios

Una mejor aproximación definitivamente, ahora tenemos las validaciones en un único sitio, por lo tanto no será necesario re-escribir código, sin embargo los DataAnnotations disponibles son limitados, y si se requiere algo más específico que no este cubierto por los disponibles out-of-box es necesario crear los propios, aunque no es díficil y puedes ver como hacerlo en el siguiente link Creando atributos de validación

La siguiente forma de validación que vamos a ver es utilizando una excelente librería llamada FluentValidation.


3. FluentValidation

Fluent Validation es una librería que permite definir reglas de validación utilizando el concepto de interfaz/clase fluida y aprovechando todo el poder de las expresiones lambda, además que es realmente bastante robusta y a su vez sencilla de usar, puede ser usada con ASPNET Core y en versiones anteriores.

Lo primero es añadir la referencia al paquete:

FluentValidation.AspNetCore

El siguiente paso, configurarlo, para ello en Startup.cs en el método ConfigureServices junto con la adición de MVC:


services
    .AddMvc()
    .AddFluentValidation(c => c.RegisterValidatorsFromAssemblyContaining<Startup>());

Ahora, voy a crear la clase UserFiltersValidator en la cual se van a definir las reglas que hemos definido anteriormente, dicha clase debe heredar de AbstractValidator<T> dónde T es la clase sobre la cual se va a crear el validador.

En este caso la clase validator quedaría:


public class UserFiltersValidator : AbstractValidator
{
    public UserFiltersValidator()
    {
        RuleFor(c => c.Name)
            .NotNull()
            .NotEmpty()
            .WithMessage("El nombre es requerido");

        RuleFor(c => c.Email)
            .NotNull()
            .NotEmpty()
            .EmailAddress()
            .WithMessage("El email no tiene un formato correcto");

        RuleFor(c => c.Age)
            .InclusiveBetween(14, 120)
            .WithMessage("La edad debe estar en un rango entre 14 y 120");
    }
}

En el la acción del controlador, es posible seguir utilizando ModelState.IsValid para verificar si el modelo es válido o no, sin embargo, otra forma de validar es implementando un filtro de acción, el cual se va a ejecutar antes del código de cada acción de cualquier controlador, con lo cuál nos ahorramos preguntar si el modelo es válido en cada acción, el filtro luce:


public class ValidatorModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Luego, solo hace falta "conectar" el filtro, esto lo hacemos luego de añadir el servicio de MVC, entonces de nuevo a la clase Startup.cs en el método ConfigureServices:


public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services
        .AddMvc()
        .AddFluentValidation(c => c.RegisterValidatorsFromAssemblyContaining());

    services.Configure(o => o.Filters.Add(new ValidatorModelAttribute()));
}

Y en la acción del controlador, ya no es necesario es uso de ModelState.IsValid.

Ahora, en el validador, es posible implementar validaciones complejas, sin necesidad de modificar la clase que se valida, como el controlador, ayudando a evitar repetir código.

Otro punto interesante de implementar FluentValidation es que al tener las validaciones aisladas del resto del código, la implemetación de pruebas unitarias se nos facilita, por ejemplo:


public class DemoValidatorTest
{
    private readonly UserFiltersValidator validator;

    public DemoValidatorTest()
    {
        validator = new UserFiltersValidator();
    }

    [Fact]
    public void DemoUnitTest_WhenEmailIsInvalid_ShouldReturnValidationError()
    {
        validator.ShouldHaveValidationErrorFor(c => c.Email, "invalidemail");
    }
}

En el siguiente link encuentran mayor información sobre pruebas unitarias y Fluent Validation: Testing Validators

Espero el post les sea interesante.

Saludos y no te olvides compartirlo.