Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
miércoles, 1 de febrero de 2012
Microsoft .NET Habitualmente asociamos la validación de entidades basadas en anotaciones de datos, o data annotations, a tecnologías como dynamic data o ASP.NET MVC, y estamos acostumbrados a que la validación se realice de forma automática, pero nada más lejos de la realidad. Podemos utilizar data annotations desde cualquier tipo de aplicación .NET (Webforms, Winforms, WPF, Consola, o cualquier otra en la que tengamos disponible System.ComponentModel.DataAnnotations), puesto que existe la posibilidad de invocar manualmente los procedimientos de validación.

En este post vamos a ver cómo realizar validaciones basadas en anotaciones de forma manual, lo cual puede tener su utilidad en gran número de escenarios.

Resumidamente, esta técnica consiste en decorar cada una de las propiedades con una serie de atributos llamados anotaciones (definidos en System.ComponentModel.DataAnnotations)  que indican las comprobaciones que se aplicarán a la entidad para determinar su validez. La siguiente porción de código muestra una entidad en la que se están indicando estas restricciones en cada una de sus propiedades:
public class Friend
{
    [Required, StringLength(50)]
    public string Name { get; set; }
 
    [Range(0, 120)]
    public int Age { get; set; }
}
En el citado espacio de nombres encontramos atributos que cubren la mayoría de casos frecuentes: Required (propiedad obligatoria), RegularExpression (validar contra una expresión regular), StringLength (longitud máxima y mínima de un texto), Range (rangos de valores permitidos), y CustomValidation (validaciones personalizadas). Además, este conjunto de anotaciones puede ser extendido muy fácilmente creando atributos que hereden de ValidationAttribute, disponible también en System.ComponentModel.DataAnnotations.

Validación manual de objetos

De lo más sencillo: la clase estática Validator, disponible también en el namespace System.ComponentModel.DataAnnotations, ofrece métodos que permiten realizar las comprobaciones de forma directa sobre objetos o propiedades concretas.

En este caso, dado que lo que nos interesa es validar las entidades completas, utilizaremos el método Validator.TryValidateObject(), al que suministraremos:
  • el objeto a validar,
  • un contexto de validación (que debemos crear previamente),
  • una colección de ValidationResult en la que almacenaremos los errores,
  • y, por último, si deseamos validar todas las propiedades (indicando true), o por el contrario preferimos parar el proceso en cuanto se detecte el primer error (false).
La implementación de la validación podría ser como la que la sigue:
    private IEnumerable<ValidationResult> getValidationErrors(object obj)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(obj, null, null);
        Validator.TryValidateObject(obj, context, validationResults, true);
        return validationResults;
    }
El método retornará una lista de errores vacía cuando el objeto haya superado las restricciones impuestas, o llena con los objetos ValidationResult que describen los problemas encontrados.

Y podríamos utilizarlo desde una aplicación de consola de la siguiente forma:
    var friend = new Friend { Age = -1, Name = "" };
    var errors = getValidationErrors(friend);
    foreach (var error in errors)
    {
        Console.WriteLine(error.ErrorMessage);
    }
The Name field is required.

The field Age must be between 0 and 120.
Los mensajes de validación que aparecen pueden ser definidos en la misma anotación, por ejemplo así:
    [Required(ErrorMessage="Please, enter the name")]
    public string Name { get; set; }

¿Y si los metadatos están en otra clase?

Hay escenarios en los que no tenemos acceso a la clase en la que deseamos introducir las anotaciones. Un ejemplo claro lo encontramos cuando nos interesa especificar las restricciones en una clase generada por un proceso automático, como el diseñador de EDM de Entity framework; cualquier cambio realizado sobre el código generado será sobrescrito sin piedad al modificar el modelo.

En estos casos, es una práctica frecuente definir los metadatos en clases “buddy”, que son copias exactas de la entidad a anotar, pero que serán utilizadas únicamente como contenedores de anotaciones. Las clases buddy se vinculan con la entidad original utilizando el atributo MetadataType de la siguiente forma:
    // This class has been generated by a tool
    public partial class Friend
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
 
    // Let's associate the buddy class FriendMetadata
    [MetadataType(typeof(FriendMetadata))]
    public partial class Friend
    {
        
    }
 
    // Buddy class
    public class FriendMetadata
    {
        [Required]
        public string Name { get; set; }
 
        [Range(0, 120)]
        public int Age { get; set; }
    }
Observad que para poder utilizar esta técnica es necesario que la entidad a la que queremos añadir anotaciones sea creada como parcial. En caso contrario no podríamos indicarle con MetadataType dónde se encuentran definidos sus atributos de validación.

Pues bien, resulta que algunos marcos de trabajo (como ASP.NET MVC) están preparados para detectar este escenario y obtener de forma automática los metadatos desde la clase buddy, pero si estamos realizando la validación de forma manual el atributo [MetadataType] no será tenido en cuenta.

Por tanto, debemos ser nosotros los que indiquemos expresamente dónde se encuentran los metadatos, para lo que, afortunadamente, contamos con la ayuda de TypeDescriptor  (definida en System.ComponentModel), desde donde podemos indicar el origen de los metadatos de clases simplemente registrando el proveedor desde el cual pueden ser obtenidos.

El procedimiento para conseguirlo es bastante simple: creamos un proveedor de descripciones basado en metadatos utilizando la clase AssociatedMetadataTypeTypeDescriptionProvider (uuf con el nombrecito ;-)) en el que vinculamos la clase “original” con la que contiene los metadatos (la clase buddy), y a continuación añadimos dicho proveedor a la primera.

Por ejemplo, para hacer que las anotaciones de la clase Friend se obtengan desde el tipo FriendMetadata podríamos incluir el siguiente código de inicialización:
var descriptionProvider = new AssociatedMetadataTypeTypeDescriptionProvider(
     typeof(Friend), 
     typeof(FriendMetadata)
);
TypeDescriptor.AddProviderTransparent(descriptionProvider, typeof(Friend));
Otra posibilidad más genérica sería implementarlo como se muestra a continuación, donde buscamos en todo el ensamblado actual clases decoradas con el atributo MetadataType, registrando el proveedor de metadatos indicado en dicho atributo de forma automática:
private static void registerBuddyClasses()
{
    var buddyAssociations = 
        from t in Assembly.GetExecutingAssembly().GetTypes()
        let md = t.GetCustomAttributes(typeof(MetadataTypeAttribute), false)
                .FirstOrDefault() as MetadataTypeAttribute
        where md != null
        select new { Type = t, Buddy = md.MetadataClassType };
 
    foreach (var association in buddyAssociations)
    {
        var descriptionProvider = 
            new AssociatedMetadataTypeTypeDescriptionProvider(
                association.Type, association.Buddy
            );
        TypeDescriptor.AddProviderTransparent(descriptionProvider, association.Type);
    }
}
De esta forma, bastará con invocar el método registerBuddyClasses() durante la inicialización de la aplicación para que las clases buddy sean registradas de forma automática.

Pero más interesante es, sin duda, que podríamos implementar nuevas fórmulas para indicar dónde se encuentran los metadatos de una clase. Por ejemplo, sería realmente sencillo modificar el método anterior para sustituir el atributo MetadataType por una convención de nombrado del tipo “las clases llamadas FooMetadata contendrán los metadatos de las clases de llamadas Foo”:
private static void registerBuddyClassesUsingConventions()
{
    var allAssemblyTypes = Assembly.GetExecutingAssembly().GetTypes().ToList();
    var buddyAssociations =
        from t in allAssemblyTypes
        let buddy = allAssemblyTypes
                    .FirstOrDefault(other => other.Name == t.Name + "Metadata")
        where buddy != null
        select new { Type = t, Buddy = buddy };
 
    foreach (var association in buddyAssociations)
    {
        var descriptionProvider =
            new AssociatedMetadataTypeTypeDescriptionProvider(
                association.Type, association.Buddy
            );
        TypeDescriptor.AddProviderTransparent(descriptionProvider, association.Type);
    }
}

¿Y si quiero usar IValidatableObject?

El interfaz IValidatableObject (definido System.ComponentModel.DataAnnotations) obliga a implementar un único método, llamado Validate(), que retornará una lista de objetos ValidationResult con los resultados de las comprobaciones.

A continuación se muestra un ejemplo de implementación de este interfaz sobre una entidad:
public class Friend : IValidatableObject
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Name.Equals("albert", StringComparison.CurrentCultureIgnoreCase))
        {
            yield return new ValidationResult("I don't like Alberts!");
        }
    }
}
El método Validate() impuesto por el interfaz será invocado automáticamente por el framework desde el mismo TryValidateObject() siempre que no encuentre errores al comprobar las restricciones especificadas mediante anotaciones. O sea, que sólo se invocará a Validate() cuando no se hayan detectado errores previos de validación (gracias, Arturo, por la aportación).

En cualquier caso, si nos interesa validar de forma manual también estos objetos, siempre podemos hacerlo como sigue:
    private static IEnumerable<ValidationResult> getIValidatableErrors(object obj)
    {
        var validationResults = new List<ValidationResult>();
        var context = new ValidationContext(obj, null, null);
        var validatable = obj as IValidatableObject;
        if(validatable!=null) 
            validationResults.AddRange(validatable.Validate(context));
 
        return validationResults;
    }
De esta forma, podríamos comprobar la ejecución como sigue:
    var friend = new Friend { Age = -1, Name = "albert" };
    var errors = getValidationErrors(friend);
    foreach (var error in errors)
    {
        Console.WriteLine(error.ErrorMessage);
    }
The field Age must be between 0 and 120.

I don’t like Alberts!
En resumen, en este post hemos visto cómo utilizar las herramientas que ofrece el framework .NET para trabajar con validaciones basadas en data annotations de forma manual, lo que abre su ámbito de utilización a prácticamente cualquier tipo de aplicación para este marco de trabajo. Por el camino hemos repasado los mecanismos de anotaciones, y diversos escenarios como la externalización de atributos en clases buddy o el uso de la interfaz IValidatableObject.

Descargar un proyecto VS2010 con el código y pruebas desde Skydrive.

Publicado en Variable not found.

6 Comentarios:

CADAVID dijo...

Muchas gracias! Excelente articulo!

josé M. Aguilar dijo...

Gracias a tí, Cadavid, por comentar!

Arturo Null dijo...

Hola José,

Si sigo al pie de la letra el código posteado en la parte IValidatableObject me devuelve 2 veces el mismo error:

I don’t like Alberts!
I don’t like Alberts!

En el proyecto de Skydrive no sucede porque la propiedad Age tiene una validación con Data Annotations.

¿Como puedo evitar ese comportamiento?

Saludos

josé M. Aguilar dijo...

Hola, Arturo!

Efectivamente, es una cuestión que pasé por alto al escribir el post.

Resulta que el mismo método Validator.TryValidateObject(), cuando no encuentra errores en las anotaciones de datos usa a continuación el interfaz IValidatableObject.

Es decir, que en principio no haría falta volver a llamar a Validate() de forma manual.

He actualizado el post para reflejarlo, muchas gracias por aportar :-)

Saludos.

Arturo Null dijo...

Hola José,

Muchas gracias por enseñarnos.

Saludos

Alvaro De León (orochies) dijo...

Excelente articulo, me ayudo bastante muchas gracias...