Validator providers en ASP.NET MVC

ASP.NET MVCEn la pasada charla sobre el sistema de validaciones de MVC 3 vimos un ejemplo, creo que bastante ilustrativo, de los proveedores de validación del framework. Concretamente, implementamos un proveedor capaz de obtener las anotaciones partiendo de las restricciones definidas en el web.config. Es decir, las reglas de comprobación como Required o StringLength no las definíamos a nivel de código mediante atributos, sino en el archivo de configuración, lo que podía aportar interesantes ventajas vistas a flexibilizar nuestras soluciones.

En este post vamos a ver un nuevo ejemplo de cómo utilizar el mismo mecanismo de proveedores, pero esta vez para conseguir de forma muy sencilla simplificar el código de clases del modelo en las que todas sus propiedades sean por defecto obligatorias, salvo aquellas en las que indiquemos expresamente lo contrario.

1. Empezando por el final…

Lo que pretendemos hacer es crear dos nuevos atributos, AllRequired y NotRequired. El primero, aplicado a una clase indicará la obligatoriedad de todas sus propiedades, mientras que el segundo, aplicado a una propiedad, indicará que ésta es opcional.

Observad en la siguiente tabla el efecto que podríamos conseguir sobre el código:

Enfoque tradicional Usando AllRequired y NotRequired
public class Friend
{
    [Required] 
    public string Name { get; set; }
    [Required]
    public string Surname { get; set; }
    [Required]
    public string Nickname { get; set; }
    [Required]
    public string Address { get; set; }
    [Required]
    public string City { get; set; }
    [Required]
    public string Country { get; set; }
    public string Phone { get; set; }
    [Required]
    public string MobilePhone { get; set; }
    [Required]
    public string Email { get; set; }
}
[AllRequired]
public class Friend
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string Nickname { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    [NotRequired]
    public string Phone { get; set; }
    public string MobilePhone { get; set; }
    public string Email { get; set; }
}

2. Model validator providers

Cuando el framework necesita obtener los validadores asociados a una clase y sus propiedades, no lo hace de forma directa sino a través de unos componentes llamados model validator providers. Éstos son los encargados de acceder a las distintas fuentes en las que puede existir información sobre las reglas de validación para cada clase.

Así, existe “de serie” un model validator provider específico para obtener los validadores partiendo de las anotaciones realizadas sobre la clase (DataAnnotationsModelValidatorProvider ), que es el método utilizado normalmente; existe otro para obtener las implementaciones del interfaz IDataErrorInfo (DataErrorInfoModelValidatorProvider), y otro que se encarga de validaciones implícitas del tipo (ClientDataTypeModelValidatorProvider).

Todos ellos se encuentran registrados en la colección pública ModelValidatorProviders.Providers, de forma que cuando el framework necesita obtener los validadores asociados a una entidad, lo que hace es recorrer dicha colección e ir llamando uno por uno a los proveedores solicitándoles información de validación para cada una sus propiedades.

¡Todo encaja!Este proceso se realiza, cuando se tiene activa la validación en cliente, para obtener las reglas y parámetros que hay que llevar a la vista, o también durante el binding, para obtener los validadores, ejecutarlos y actualizar el ModelState con el resultado.

Pero sin duda lo mejor que tiene este sistema es su facilidad de extensión. Podemos añadir con suma facilidad nuevos proveedores a la colección ModelValidatorProviders a la que hacía referencia anteriormente, de forma que podemos introducir nuestra propia lógica de generación de validadores en el flujo de ejecución de peticiones del framework.

Y es lo que vamos a hacer a continuación: implementaremos nuestro proveedor para generar automáticamente validaciones de tipo Required para todas las propiedades de las clases decoradas con el atributo [AllRequired], exceptuando aquellas propiedades en las que se haya indicado lo contrario con [NotRequired].

3. Los atributos AllRequired y NotRequired

Antes que nada, necesitamos definir estos dos atributos para poder continuar. El código es tan simple como el que veis a continuación:

[AttributeUsage(AttributeTargets.Class)]
public class AllRequiredAttribute : Attribute
{
        
}
 
[AttributeUsage(AttributeTargets.Property)]
public class NotRequiredAttribute: Attribute
{
        
}

Observad que no es necesario que éstos hereden de ValidationAttribute; de hecho, no estamos creando anotaciones personalizadas, sólo atributos que nos permitan “marcar” en clases y propiedades para generar más adelante, desde el proveedor, las anotaciones necesarias.

Los [AttributeUsage] lo único que hacen es restringir su uso de forma que [AllRequired] se pueda utilizar únicamente sobre clases, mientras que [NotRequired] sea utilizable exclusivamente en propiedades, más que nada para que se comprueben en compilación estos aspectos.

4. Implementando nuestro model validator provider

La implementación de un model validator provider es bastante sencilla. Lo único que tenemos que hacer es heredar de ModelValidatorProvider e implementar su método GetValidators(), que es el que invocará el framework para obtener los validadores de cada una de las propiedades de la clase.

En nuestro caso, simplemente comprobaremos que la clase esté marcada como [AllRequired] , retornando un validador de tipo Required para todas aquellas propiedades que no se hayan marcado con [NotRequired]. Casi mejor lo vemos sobre el código:

public class AllRequiredValidatorProvider : ModelValidatorProvider
{
  public override IEnumerable<ModelValidator> GetValidators(
      ModelMetadata metadata, ControllerContext context)
  {
      var allRequiredAttribute =
              TypeDescriptor.GetAttributes(metadata.ContainerType)
              .OfType<AllRequiredAttribute>().FirstOrDefault();
 
      if (allRequiredAttribute != null)
      {
        var property = metadata.ContainerType.GetProperty(metadata.PropertyName);
        var isOptional = property.GetCustomAttributes(typeof(NotRequiredAttribute), false).Any();
        if (!isOptional)
        {
          var requiredValidator = new RequiredAttribute() { ErrorMessage = "Requerido" };
          yield return new RequiredAttributeAdapter(metadata, context, requiredValidator);
        }
      }
  }
}

Observad la firma del método: retorna una colección de objetos que heredan de ModelValidator, que son adaptadores de los distintos mecanismos de validación que podemos emplear en MVC. En la práctica permiten homogeneizar la diversidad de validadores vistas al framework, y a veces incluyen cierta lógica, como la generación de las reglas de validación en cliente.

El framework incluye ya adaptadores para cada una de las anotaciones habituales (RequiredAttributeAdapter, StringLengthAdapter, RegularExpressionAttributeAdapter …), que podemos instanciar directamente para ir construyendo la colección que retorna el método GetValidators(). En el código anterior, fijaos que en la última línea retornamos, usando yield, un nuevo adaptador de tipo RequiredAttributeAdapter, a cuyo constructor suministramos una instancia del atributo Required convenientemente inicializada.

Un último detalle: tened en cuenta que por simplificar el ejemplo no he prestado atención a aspectos como el rendimiento, que podría mejorarse introduciendo una caché, o la personalización de los mensajes de error, que sería fácilmente implementable permitiendo parámetros en el atributo [AllRequired] y suministrándolos más adelante en la instanciación de los RequiredAttribute que retornamos desde el proveedor, como hacemos ahora con la cadena de texto constante “Requerido”.

5. Registro del proveedor

Por último, ya sólo queda indicar al framework que debe utilizar nuestro proveedor para obtener los validadores, por lo que lo añadimos en el global.asax a la colección de providers:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
 
    ModelValidatorProviders.Providers.Add(new AllRequiredValidatorProvider());
 
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

Casi todo obligatorio¡Y ahí lo tenemos! A partir de este momento, ya podemos hacer uso de estos nuevos atributos en nuestras clases del modelo o view models, como se muestra a continuación:

[AllRequired]
public class Friend
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string Nickname { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    [NotRequired]
    public string Phone { get; set; }
    public string MobilePhone { get; set; }
    public string Email { get; set; }
}

En tiempo de ejecución tendremos, por supuesto, validación tanto en cliente como en servidor; de hecho, el resultado será idéntico al conseguido si hubiéramos decorado cada una de las propiedades anteriores con él atributo Required, puesto que en realidad estamos inyectándolo en todas las propiedades de forma automática, exceptuando en aquellas donde se ha indicado expresamente lo contrario.

Podéis descargar el proyecto de prueba desde Skydrive (VS2010+MVC3).

Publicación original (8-nov-2011): http://www.variablenotfound.com/2011/11/validator-providers-en-aspnet-mvc.html
Tip: ¿todavía no conoces la selección de enlaces interesantes de Variable not found?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *