
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.
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 así:
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.