This page looks best with JavaScript enabled

Validando con FluentValidation en Blazor

 ·  ☕ 6 min  ·  ✍️ eiximenis

Blazor tiene un soporte para formularios bastante bien montado. Por un lado, existe el componente “padre”, que representa al propio formulario. Este componente padre (solemos utilizar EditForm) es el encargado de crear “el contexto de edición” y proveerlo a todos los componentes hijos que están dentro de él. Este contexto consta básicamente de un modelo (el objeto que se está editando) y el estado de la validación de sus propiedades.

Dentro de un EditForm usamos los componentes de formulario (tales como InputText). Esos controles consultan en dicho contexto de edición si el valor de la propiedad es válido o inválido y se renderizan en consecuencia. Pero esos componentes no modifican nunca el estado de una propiedad. Esto se reserva a otros componentes (que también deben ser hijos del EditForm), que son los validadores. Así pues la responsabilidad está claramente separada:

  • El formulario padre (EditForm) provee del contexto de edición
  • Los componentes de edición usan dicho contexto para consultar si el valor de la propiedad a la que están enlazados es correcto o no (y renderizarse en consecuencia)
  • Los validadores validan los valores de las propiedades, modificando el contexto de edición

Blazor viene de serie con un componente de validación que es el DataAnnotationsValidator que examina el modelo asociado al contexto y busca atributos de Data Annotations. Así si tenemos un modelo tal que:

1
2
3
4
5
public class UserData 
{
  [Required]
  public string Name {get; set;}
}

Podemos crear un formulario para editarlo y validarlo de forma sencilla:

1
2
3
4
<EditForm Model="@User">
  <DataAnnotationsValidator />
  <InputText @bind-Value="Name" >
</EditForm>

Ahora bien, si en lugar de Data Annotations prefieres usar algun otro mecanismo de validación, ¿como lo puedes implementar? Pues la solución pasa por crearte un componente validador. En la documentación está muy bien explicado aunque el ejemplo es un poco rebuscado. En este post os voy a mostrar como crear un validador que valide en base al código de FluentValidation que tengáis.

Creando el validador de FluentValidation

Primero, el código entero y luego lo comentamos por partes :)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  public class FluentValidationValidator : ComponentBase
  {
      [CascadingParameter] EditContext CurrentEditContext { get; set; }
      protected override void OnInitialized()
      {
          if (CurrentEditContext == null)
          {
              throw new InvalidOperationException("You must use FluentValidationValidator inside an EditForm or any other EditContext provider");
          }
          CurrentEditContext.AddFluentValidation();
      }
  }

Este es el componente, que lo único que hace es recojer el contexto de edición que provee el EditForm. Para ello declara una propiedad de tipo EditContext decorada con [CascadingParameter], ya que EditForm provee del contexto de edición usando una cascading value.

Hay trampa claro, ese código depende del método AddFluentValidation que es un método de extensión sobre EditContext, y que es el que realmente hace el trabajo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
    static class EditContextExtensions
    {
        public static EditContext AddFluentValidation(this EditContext editContext)
        {
            if (editContext == null)
            {
                throw new ArgumentNullException(nameof(editContext));
            }

            var messages = new ValidationMessageStore(editContext);

            editContext.OnValidationRequested +=
                (sender, eventArgs) => ValidateModel((EditContext)sender, messages);

            editContext.OnFieldChanged +=
                (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);

            return editContext;
        }

        private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
        {
            var validator = GetValidatorForModel(editContext.Model);
            var context = CreateValidationContextForModel(editContext.Model);
            var validationResults = validator.Validate(context);
            messages.Clear();
            foreach (var validationResult in validationResults.Errors)
            {
                messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
            }
            editContext.NotifyValidationStateChanged();
        }

        private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
        {

            var properties = new[] { fieldIdentifier.FieldName };

            var context = CreateValidationContextForProperties(fieldIdentifier.Model, properties);
            var validator = GetValidatorForModel(fieldIdentifier.Model);
            var validationResults = validator.Validate(context);

            messages.Clear(fieldIdentifier);
            messages.Add(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));
            editContext.NotifyValidationStateChanged();
        }

        private static IValidationContext CreateValidationContextForProperties(object model, string[] properties)
        {
            var valType = typeof(ValidationContext<>).MakeGenericType(model.GetType());
            return (IValidationContext)Activator.CreateInstance(valType, new[] { model, new PropertyChain(), new MemberNameValidatorSelector(properties) })!;
        }

        private static IValidationContext CreateValidationContextForModel(object model)
        {
            var valType = typeof(ValidationContext<>).MakeGenericType(model.GetType());
            return (IValidationContext)Activator.CreateInstance(valType, new[] { model });
        }

        private static IValidator GetValidatorForModel(object model)
        {
            var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType());
            var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType));
            var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType)!;
            return modelValidatorInstance;
        }
    }

Ahora sí, podemos ir método a método.

  1. El método AddFluentValidation es el método inicial que lo que hace es suscribirse a los eventos que lanza el EditContext. Hay dos eventos a los que debemos suscribirnos: OnFieldChanged que se lanza cuando un componente de edición pide validar la propiedad a la que está enlazado y OnValidationRequested que se lanza cuando se debe validar todo el formulario. Así en el primero debemos validar solo una propiedad y en el segundo pues todo el modelo. Luego este método también crea la ValidationMessageStore que es, básicamente, un diccionario de propiedades y errores de validación asociados.
  2. El método ValidateModel es el que se ejecuta en respuesta al evento OnValidationRequested y a partir de ese punto ya todo es código de FluentValidation: Se obtiene el validador asociado al modelo (GetValidatorForModel) y se crea el contexto de validación de FluentValidation (CreateValidationContextForModel), luego se valida el modelo, se añaden los errores a la ValidationMessageStore y finalmente se notifica al EditContext que ha habido cambios en el estado de la validación.
  3. El método ValidateField es parecido, simplemente se cambia como se crea el contexto de validación de FluentValidation (ahora es un contexto para validar solo una propiedad no todo el modelo). Para ello se usa el parámetro FieldIdentifier que (básicamente) contiene el nombre de la propiedad que debemos validar.

Luego los siguientes métodos son métodos usados por esos dos, y son un poco “complejos” porque se debe usar reflection. La API de FluentValidation no está pensada para “obtener un validador de un tipo que solo conocemos en runtime”, pero vamos… tampoco son gran cosa. Por ejemplo, para validar nuestro tipo UserData se debe crear un ValidationContext<UserData>. Eso está perfecto si conoces en tiempo de compilación el tipo, pero no es nuestro caso, de ahí la necesidad de usar reflection. Lo mismo ocurre con el método GetValidatorForModel, que debe crear una instancia de cualquier clase que herede de AbstractValidator<UserData>.

Usando nuestro validador

Bueno, eso no tiene ningún secreto. Ahora, en lugar de decorar con [Required] la propiedad Name de UserData tendríamos que crear un validador de FluentValidation:

1
2
3
4
5
public class UserDataValidator : AbstractValidator<UserData> {
  public UserDataValidator() {
    RuleFor(u => u.Name).NotEmpty();
  }
}

Y en nuestro EditForm en lugar de usar el DataAnnotationsValidator, pues usamos nuestro nuevo validador:

1
2
3
4
<EditForm Model="@User">
  <FluentValidationValidator />
  <InputText @bind-Value="Name" >
</EditForm>

¡Y listos! Ya hemos integrado FluentValidation en nuestro proyecto de Blazor.

Combinando los validadores

Un formulario no debe por que estar validado por un único validador. Este modelo de validación en el que los validadores son componentes adicionales que comparten el EditContext provisto por el EditForm permite que en un mismo formulario haya dos, tres o los validadores que sea:

1
2
3
4
5
<EditForm Model="@User">
  <DataAnnotationsValidator />
  <FluentValidationValidator />
  <InputText @bind-Value="Name" >
</EditForm>

Ahora se aplicarán tanto las validaciones de Data Annotations como las de FluentValidation. Un validador no puede interferir con las validaciones que aplique otro validador (para ello deberían compartir el ValidationMessageStore y en este caso cada uno está creando el suyo propio). Así si UserData tuviera tanto el [Required] aplicado y también existiese el validador de FluentValidation, si dejas el nombre en blanco recibirias dos errores en la propiedad Name. Si p. ej. usaras un <ValidationSummary> para mostrar el resúmen de errores, te aparecerían dos mensjaes de error asociados al nombre del usuario.

Y… ¡nada más!

Si quieres, puedes invitarme a un café xD

eiximenis
ESCRITO POR
eiximenis
Compulsive Developer