TL;DR
- Fluent Validation é unha potente biblioteca para validar datos en aplicacións .NET, que ofrece máis flexibilidade e testabilidade que as Anotacións de Datos integradas.
- Beneficios clave de Fluent Validation:
- A lóxica de validación está separada do modelo, seguindo o principio de Separación de Concerns.
- Permite validacións máis complexas, incluíndo validacións dependentes e personalizadas.
- Facilita as probas unitarias da lóxica de validación.
- As regras de validación defínense cunha sintaxe fluída e lexible usando o método
RuleFor
.- As mensaxes de validación poden personalizarse e utilizar marcadores de posición para valores dinámicos.
- A execución da validación pódese controlar usando o método
Cascade
para deter no primeiro erro.- As regras de validación poden organizarse en clases separadas e incluírse no validador principal.
- Fluent Validation proporciona unha excelente alternativa ás Anotacións de Datos cando se necesita máis flexibilidade e mantemento na lóxica de validación.
- Visita Fluent Validation para saber máis.
Como programadores somos conscientes da importancia que ten validar os datos que os usuarios introducen na nosa aplicación, ben a través dun formulario nunha aplicación ou dunha petición á nosa API.
Se algo nos deu a experiencia é que non podes fiarte dos datos introducidos polos usuarios, eses seres malignos que tarde ou cedo atoparán ese caso de uso que non contemplaches; ou peor aínda, do QA, que sen lugar a dúbida usarán os valores límite e máis alá para comprobar a robustez do noso software.
Fluent Validation ou Data Annotations?
Seguramente lendo o título deste artigo o teu primeiro pensamento fose: "Os meus modelos xa se validan e limitan con Data Annotations. Que me aportaría usar Fluent Validation?".
Data Annotations é perfectamente válido e achégache sinxeleza á hora de facer a validación dos teus modelos pero, desde o meu punto de vista, ten certas carencias e inconvenientes que non atoparemos en Fluent Validation.
Un destes inconvenientes dos que falamos é que desde o momento en que usas Data Annotations co teu modelo estás creando unha dependencia entre as túas regras de validación e o teu modelo, incumprindo así un dos principios SOLID: Separation of Concerns.
Outro "pero" é que o atributo [Required]
, por exemplo, non che aporta moita flexibilidade sobre a validación. Como farías se unha propiedade é obrigatoria en función doutra?, Ou se necesitas aplicar certa lóxica para saber que o campo é correcto? Con Fluent Validation veremos como facelo dunha forma moi sinxela.
E, por último, as probas unitarias do teu modelo con Data Annotations son máis complexas que con Fluent Validation, onde a túa validación é unha clase separada e totalmente testeable.
A idea deste artigo é mostrache as funcionalidades máis destacadas e as vantaxes de usar Fluent Validation. Non veremos todo o seu potencial nin funcionalidades pero espero que sirva como unha base sólida coa que comezar a usalo. Así que basta de charla e ensinemos algo de código.
Creando o noso proxecto de proba
Para ilustrar as distintas posibilidades de Fluent Validation, imos crear unha sinxela aplicación de escritorio que nos permitirá dar de alta usuarios na nosa compañía de seguros de coche.
Unha vez creado o proxecto, o primeiro será importar o paquete nuget correspondente. Se o teu proxecto é un proxecto web de tipo ASP.Net Core, deberás seleccionar o paquete FluentValidation.AspNetCore, para o resto de proxectos FluentValidation axustarase ao que precises.
Comezaremos polo importante e crearemos o modelo de datos de usuarios a validar. Neste caso, teremos o nome do asegurado, o seu apelido, data de nacemento, se ten coche e, se é así, a matrícula do 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; }
}
E finalmente, un formulario desde o que dar de alta os clientes e que nos mostrará os erros de validación que se irán producindo.
Con isto xa temos a base para comezar a facer as nosas primeiras validacións e "trastear" un pouco con Fluent Validation.
Un exemplo sinxelo
As validacións con Fluent Validation créanse nunha clase que herde de AbstractValidator, onde T é o tipo da clase que queremos validar.
using FluentValidation;
public class UserValidator : AbstractValidator
{
}
Todas as regras que vaiamos creando declararanse dentro do construtor da nosa clase de validación.
Para crear unha regra de validación para unha propiedade usaremos o método RuleFor cunha expresión lambda onde se especifica a propiedade a validar e a validación que aplicaremos sobre esa propiedade, ben sexa unha validación predefinida ou unha personalizada como veremos máis adiante.
Comecemos cunha validación sinxela, que se podería corresponder ao [Required] de Data Annotations, que será comprobar que se indicou o nome do noso asegurado.
using FluentValidation;
public class UserValidator : AbstractValidator
{
public UserValidator ()
{
RuleFor(user => user.Name).NotEmpty();
}
}
Como podes ver, ao usar unha sintaxe fluída o método é moi sinxelo de ler. Dunha soa ollada e sen saber nada de Fluent Validation sabemos que propiedade estamos validando e que validación é. E mellor aínda! O noso modelo de datos non ten ningunha dependencia nin vínculo con esta validación.
NotEmpty é só unha das moitas validacións que Fluent Validation ten predefinidas para nós. Seguiremos vendo algunhas máis neste artigo, pero se queres ver a lista completa de validacións podes consultalas aquí.
Ampliemos un pouco máis a nosa validación. Só vendo o código, que cres que estamos validando agora mesmo?
using FluentValidation;
public class UserValidator : AbstractValidator
{
public UserValidator ()
{
RuleFor(user => user.Name).NotEmpty().Length(2,50);
}
}
Seino, foi insultantemente sinxelo. O nome do noso usuario non poderá ser baleiro e deberá ter entre 2 e 50 caracteres.
Con isto quería mostrache como Fluent Validation nos permite encadear tantas regras de validación para unha mesma propiedade como queiramos.
Chamando á nosa validación
Para poder executar a validación, invocaremos o método Validate que herdamos na nosa clase UserValidator. Para iso, no noso exemplo engadiremos ao botón "Validar" do noso formulario o evento Click co seguinte código:
private void ValidateButton_Click(object sender, EventArgs e)
{
//Limpamos as mensaxes de erro de anteriores validacións
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
{
//Incluímos todos os erros de validación na nosa caixa de texto de erros
foreach (var error in result.Errors)
{
ErrorTextBox.AppendText(error.ErrorMessage);
ErrorTextBox.AppendText(Environment.NewLine);
}
}
}
O realmente importante deste código, e co que debes quedarte principalmente, é coa chamada ao método Validate e a resposta do método, que é un obxecto de tipo ValidationResult.
O ValidationResult indícanos se a instancia do noso modelo que lle pasamos cumpre todas as nosas regras (IsValid) ou, de non cumprilas, unha lista cos erros de validación.
E cal é o resultado de executar o noso pequeno programa sen indicarlle ningún valor en ningún dos campos do formulario?
O primeiro que nos chama a atención é que, aínda que non especificamos ningunha mensaxe de erro, xa se nos devolven 2 mensaxes predefinidas en función das regras de validación que usamos (NotEmpty e Length) usando como idioma o que teñamos especificado en CultureInfo.CurrentUICulture do noso framework.
Personalizando as mensaxes de resposta
Se non queres usar as mensaxes de resposta predefinidas de Fluent Validation podes personalizar a mensaxe de resposta co método WithMessage co texto desexado da seguinte forma:
RuleFor(user => user.Name)
.NotEmpty().WithMessage("Non indicou o nome de usuario.")
.Length(2,50).WithMessage("O nome debe ter unha lonxitude entre 2 e 50 caracteres");
Fíxate que repetimos a chamada ao método .WithMessage() por cada unha das validacións que facemos. Se só quixésemos usar unha única mensaxe de erro bastaría con colocala ao final de todas as validacións.
Volvamos facer a mesma proba de antes e vexamos como cambiou.
Podemos ir un pouco máis alá e personalizar as nosas mensaxes co uso de placeholders dentro do texto.
Algúns destes placeholders son:
{PropertyName}
que contén o nome da propiedade a validar.{PropertyValue}
que contén o valor da propiedade a validar{MinLength}
que nas validacións de tipo Length indica o nº de caracteres mínimo a introducir.{MaxLength}
que nas validacións de tipo Length indica o nº de caracteres máximo a introducir.{TotalLength}
que nas validacións de tipo Length indica o nº de caracteres introducido
Se queres consultar a lista completa podes facelo aquí
Fagamos uso destes placeholders modificando a nosa mensaxe de erro para a validación da lonxitude do nome:
RuleFor(user => user.Name)
.NotEmpty().WithMessage("Non indicou o nome de usuario.")
.Length(2,50).WithMessage("{PropertyName} ten {TotalLength} letras. Debe ter unha lonxitude entre {MinLength} e {MaxLength} letras.");
Se probamos a romper esta validación introducindo un só carácter no nome, o resultado será algo como isto:
A vantaxe de usar estes placeholders é que nos evitan cometer erros por omisión, é dicir, se cambiásemos o rango permitido para o nome a 5 e 100 non teriamos que tocar o texto de erro xa que colle eses valores directamente da regra.
Mensaxes en cascada
Co noso código actual, se non introducimos o nome do usuario, devólvenos todas as mensaxes de erro das regras para esa propiedade. Pero poderiamos facer que só nos mostrase a primeira mensaxe? Podemos, usando o método Cascade
.
Cascade
permítenos indicarlle o comportamento que queremos para as nosas regras pasándolle como parámetro un dos dous valores permitidos:
CascadeMode.Continue
=> Valídanse todas as regras e devólvense todos os erros (valor por defecto).CascadeMode.Stop
=> A validación detense no primeiro erro producido.
Así para conseguir que a nosa aplicación só mostre a primeira mensaxe necesitariamos cambiar a nosa regra a isto:
RuleFor(user => user.Name)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage("Non indicou o nome de usuario.")
.Length(2,50)
.WithMessage("{PropertyName} ten {TotalLength} letras. Debe ter unha lonxitude entre {MinLength} e {MaxLength} letras.");
Desta forma, en canto se incumpra a primeira regra de validación das que teñamos definidas, detense a validación e devólvese a mensaxe de erro.
Validacións dependentes doutros campos
Unha das cousas que considero máis interesantes de Fluent Validation é a posibilidade de poder aplicar regras en función doutras propiedades.
Por exemplo, podemos comparar dúas propiedades para evitar que sexan iguais ou, ao contrario, que sexan iguais como cando confirmamos un contrasinal ao darnos de alta nunha web. Para iso faremos uso doutra regra predefinida: NotEqual.
Vexamos un exemplo desta regra na nosa aplicación validando que o nome e apelido do noso usuario non sexan iguais.
RuleFor(user => user.Name).NotEqual(user => user.LastName);
Desta forma mostraremos unha mensaxe cada vez que nome e apelido sexan iguais evitando que Jar Jar poida darse de alta na nosa aplicación.
Síntoo, Jar Jar. Non no noso sistema.
Outro uso moi interesante de validacións dependentes entre propiedades é o feito de poder aplicar ou non unha regra en función do valor doutra propiedade.
Para iso dispoñemos de dous métodos: When e Unless.
Se recordas, un dos requisitos da nosa aplicación era que se marcabamos a opción de 'Ten coche', tiñamos que indicar a matrícula do coche do usuario.
Isto podemos conseguilo facendo uso do método When desta forma:
RuleFor(user => user.PlateNumber).Length(7,12).When(user => user.HasCar);
Dunha forma moi sinxela creamos unha regra para validar que a matrícula teña entre 7 e 12 caracteres só se o usuario marcou a opción de que ten coche (HasCar).
Estendendo as nosas validacións
Ata o momento puidemos ver parte das validacións predefinidas de Fluent Validation e como enlazalas entre propiedades. Pero que pasa se ningunha das validacións predefinidas cumpre coas túas necesidades?
Para estes casos podemos crear validacións personalizadas. Hai varias formas de crear validacións personalizadas pero a máis sinxela é usando o método Must. Vexamos un exemplo:
List blackListWords = new List() {"caca", "cu","pedo", "pis"};
RuleFor(user => user.LastName).Must(lastname => !blackListWords.Contains(lastname));
Con este exemplo implementamos unha regra para evitar que algún usuario troll intente introducir algún texto para o apelido que consideremos inapropiado e teñamos na nosa lista negra.
Outro uso interesante do método Must é a posibilidade de pasarlle como parámetro unha función coa nosa validación para que o noso código sexa máis limpo e poder reaproveitar unha validación noutra propiedade. Como exemplo, vexamos como indicarlle ao noso sistema que só os maiores de 18 anos poden darse de alta.
public class UserValidator : AbstractValidator
{
public UserValidator()
{
RuleFor(user => user.BirthDate)
.Must(IsOver18)
.WithMessage("Ten que ser maior de idade para poder rexistrarse.");
}
private bool IsOver18(DateTime birthDate)
{
return DateTime.Now.AddYears(-18) >= birthDate;
}
}
Agrupando validacións
Espero que chegados a este punto saibas como validar o teu modelo coas regras predefinidas, crear as túas propias validacións se as necesitases e mostrar textos de erro acordes, tanto xenéricos como personalizados. Se facemos un repaso do noso código debería ser algo así:
public class UserValidator : AbstractValidator
{
public UserValidator()
{
RuleFor(user => user.Name)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage("Non indicou o nome de usuario.")
.Length(2,50)
.WithMessage("{PropertyName} ten {TotalLength} letras. Debe ter unha lonxitude entre {MinLength} e {MaxLength} letras.");
RuleFor(user => user.Name).NotEqual(user => user.LastName);
RuleFor(user => user.PlateNumber).Length(7,12).When(user => user.HasCar);
List blackListWords = new List();
RuleFor(user => user.LastName).Must(name => !blackListWords.Contains(name));
RuleFor(user => user.BirthDate)
.Must(IsOver18)
.WithMessage("Ten que ser maior de idade para poder rexistrarse.");
}
private bool IsOver18(DateTime birthDate)
{
return DateTime.Now.AddYears(-18) >= birthDate;
}
}
}
Máis alá de ser un exemplo básico para iniciarte en FluentValidation non hai nada malo co noso código pero si que hai un punto de mellora.
Como firmes defensores do Clean Code que somos imos refactorizar o noso exemplo para evitar, por un lado que o construtor teña un número de liñas excesivo, e para seguir o Principio de responsabilidade única. Para iso separaremos as nosas regras en distintas clases que logo agruparemos e incluiremos na nosa clase de validación.
A maneira de facer isto é a través do uso do método Include que nos permite realizar esta agrupación de regras dentro da nosa clase.
public class UserValidator : AbstractValidator
{
public UserValidator()
{
Include(new UserNameIsSpecified());
Include(new LastNameDistinctThanName());
Include(new PlateNumberSpecifiedIfHasCar());
Include(new LasTNameIsNotBlacklisted());
Include(new UserIsOver18());
}
}
public class UserNameIsSpecified : AbstractValidator
{
public UserNameIsSpecified()
{
RuleFor(user => user.Name)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage("Non indicou o nome de usuario.")
.Length(2, 50)
.WithMessage("{PropertyName} ten {TotalLength} letras. Debe ter unha lonxitude entre {MinLength} e {MaxLength} letras.");
}
}
public class LastNameDistinctThanName : AbstractValidator
{
public LastNameDistinctThanName()
{
RuleFor(user => user.Name).NotEqual(user => user.LastName);
}
}
public class PlateNumberSpecifiedIfHasCar : AbstractValidator
{
public PlateNumberSpecifiedIfHasCar()
{
RuleFor(user => user.PlateNumber).Length(7, 12).When(user => user.HasCar);
}
}
public class LasTNameIsNotBlacklisted : AbstractValidator
{
public LasTNameIsNotBlacklisted()
{
List blackListWords = new List();
RuleFor(user => user.LastName).Must(name => !blackListWords.Contains(name));
}
}
public class UserIsOver18 : AbstractValidator
{
public UserIsOver18()
{
RuleFor(user => user.BirthDate)
.Must(IsOver18)
.WithMessage("Ten que ser maior de idade para poder rexistrarse.");
}
private bool IsOver18(DateTime birthDate)
{
return DateTime.Now.AddYears(-18) >= birthDate;
}
}
Como podes ver a clave é o método Include que che permite agrupar as regras que necesitas e manter o teu código limpo e ben estruturado.
Probas unitarias do noso validador
Coa refactorización anterior, realizar as probas unitarias do teu código é realmente sinxelo. Todas as nosas validacións están en clases separadas e ben organizadas polo que realizar as probas unitarias correspondentes non debería ter ningunha complexidade.
Un exemplo de proba unitaria á 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);
}
Conclusións
Espero que esta introdución a Fluent Validation che sirva como base e para sopesar se usalo nos teus proxectos ou non.
Como dicía ao principio do artigo, creo que o feito de ter as túas validacións totalmente independentes do modelo de datos é un punto moi a ter en conta á hora de apostar por el, así como a posibilidade de usar unha sintaxe fluída para crear as túas regras dunha forma sinxela e con moita versatilidade.
Respecto a usar Data Annotations ou Fluent Validation depende das túas necesidades e requirimentos. Ambos teñen os seus pros e contras.
Con Data Annotations ao acceder ao teu modelo xa ves de forma sinxela as regras que se aplican a esa propiedade mentres que en Fluent Validation necesitarías navegar polas distintas clases creadas.
Por outro lado, se as túas validacións son complexas ou requiren certa lóxica, Fluent Validation convértese nun gran aliado facilitándoche a vida enormemente mentres que con Data Annotations apilaríanse os atributos a aplicar á propiedade ensuciando o teu código e incluso así, probablemente, terías que codificar as validacións que necesitases.
Deixei cousas no tinteiro para non alongar en exceso este artigo, e tamén porque o obxectivo principal era servir como introdución a Fluent Validation, pero anímoche a consultar a páxina oficial para descubrir o resto de posibilidades como o uso de ficheiros de recursos para as mensaxes de erro, validacións con expresións regulares ou como invocar as validacións usando inxección de dependencias.
Se espertei o teu interese visita Fluent Validation para saber máis e estar o día das últimas versións.E se queres baixarte a aplicación sobre a que traballamos tela dispoñible no meu repositorio de github aquí.