domingo, 29 de mayo de 2011

(Entity Framework Metadata) Usando metadata y LINQ para validación de campos

Hoy quería compartir con ustedes una solución practica que encontré para la valuación de campos server side. Se me presento una situación en la cual tenia que validar múltiples campos de un formulario server side, tanto en su cualidad de nullables, como en su longitud.
Como realizar esta tarea es una de las cosas mas tediosas por las que uno como desarrollador, tiene que atravesar, busque la forma de hacerlo lo mas automatizado posible. Es así que me pregunte "Tendré alguna manera de queryar la metadata del modelo, y usar eso para validar los campos??". Resulta de que es posible, usando el "MetadataWorkspace" de EF (Hago la referencia obligada a la fuente principal de donde pude sacar la info que necesitaba aqui).

Primero les voy a mostrar los métodos que uso para verificar si el campo es nullable, y cual es su maxlength, y luego el método de valuación en si mismo (pueden cuestionar el uso de Reflection como NO óptimo, pero la verdad que el degradado de performance no es perceptible en mi situación, ya que el lugar donde lo uso, no es un formulario de uso continuo e intensivo).


public static int GetMaxLength(EntityObject entity, string nomProperty  )
{
int result = 0;

var context = GetNewContext();

var queryResult = from meta in context.MetadataWorkspace.GetItems(DataSpace.CSpace)
.Where(m => m.BuiltInTypeKind == BuiltInTypeKind.EntityType)
from p in (meta as EntityType).Properties
.Where(p => p.DeclaringType.Name == entite.GetType().Name
&& p.Name == nomProperty
&& p.TypeUsage.EdmType.Name == "String")
select p.TypeUsage.Facets["MaxLength"].Value;
if (queryResult.Count() > 0)
{
result = Convert.ToInt32(queryResult.First());
}

return result;
}


El método anterior me permite obtener la longitud máxima de un campo usando la metadata del modelo conceptual, paso a diseccionarlo para ustedes:

Primero, le pido al modelo conceptual del "MetadataWorkspace" que me devuelva todas las entidades de tipo "EntityType" (puesto que podria pedir las AssociationsSet/Type, las RelationshipSet/Type, etc.):

from meta in context.MetadataWorkspace.GetItems(DataSpace.CSpace)
.Where(m => m.BuiltInTypeKind == BuiltInTypeKind.EntityType)

Sobre este subset de metadata, me intereso en conseguir, ÚNICAMENTE la metadata de la property de la entidad en la cual estoy particularmente interesado, filtrando por:
- El tipo de la entidad
- El nombre de la property
- El tipo de la property que deseo validar (esto es un filtro extra, ya que solo me interesaba validar properties del tipo "String"):

from p in (meta as EntityType).Properties
.Where(p => p.DeclaringType.Name == entity.GetType().Name
&& p.Name == nomProperty
&& p.TypeUsage.EdmType.Name == "String")




Una vez aislada la metadata de la property que solicite, solo me queda consultar la informacion en particular que necesito, en este caso "MaxLength":


select p.TypeUsage.Facets["MaxLength"].Value;


El resto es anecdotico, solo devolver el valor necesario casteado correctamente.

Para comprobar si la property es nullable o no, el procedimiento es el mismo, unicamente varia la forma en la que se devuelve la data:


public static bool IsNullable(EntityObject entity, string nomProperty)
{
var context = GetNewContext();
var queryResult = from meta in contexte.MetadataWorkspace.GetItems(DataSpace.CSpace)
.Where(m => m.BuiltInTypeKind == BuiltInTypeKind.EntityType)
from p in (meta as EntityType).Properties
.Where(p => p.DeclaringType.Name == entite.GetType().Name
&& p.Name == nomProperty
&& p.TypeUsage.EdmType.Name == "String")
select p;
if (queryResult.Count() > 0)
{
return queryResult.First().Nullable;
}

return false;
}

Como ven, el procedimiento para seleccionar la metadata de la property que necesito es el mismo, y unicamente cambia la forma en la que devuelvo el resultado, devolviendo el valor de la property "Nullable" de la metadata.

Con los metodos anteriores, facilmente pueden armar un método génerico de validación, que itere por cada una de las properties de un objeto dado, verifique cada una de las propiedades
y devuelva los nombres de las propiedades que no cumplen con los requerimientos del modelo (y por ende no permitirán que se guarde dicha entidad):



public static string ValidateStringProperties(EntityObject entity, string emptyMessage, string overflowMessage, List<string> exclusions)
{
var sb = new StringBuilder();

foreach (PropertyInfo info in entity.GetType().GetProperties())
{
if (info.PropertyType == typeof(String) && (exclusions == null || exclusions.Where(a => a == info.Name).Count()==0))
{

if (!IsNullable(entity, info.Name) && (entity.GetType().GetProperty(info.Name).GetValue(entity, null)==null || string.IsNullOrEmpty(entity.GetType().GetProperty(info.Name).GetValue(entity, null).ToString())))
{
sb.Append(string.Format(emptyMessage, info.Name));
}
else
{
if (GetMaxLength(entity, info.Name) < entity.GetType().GetProperty(info.Name).GetValue(entity, null).ToString().Length)
{
sb.Append(string.Format(overflowMessage, info.Name));
}
}
}
}

return sb.ToString();
}



Como agregado, adicione una lista de nombre de propiedades que uno desea excluir de la verificación.

Como dije antes, la performance de esta solución puede ser cuestionada, ya que uso reflection en múltiples lugares, es por eso que cada solución depende de la situación en la que se aplique.

Happy Programming.