samedi 9 octobre 2010

Entity Framework Code Only configuration from attributes

N'aimant pas particulièrement ni les designer, ni les fichier xml, je m'intéresse de plus en plus à la solution "Code Only" proposée bientôt par Entity Framework. Voyant ce post je me suis un peu trop empressé et j’ai implémenté une petite solution permettant de déduire le mapping depuis les DataAnnotations sur les objets du modèle.  

Ce n’est que quelques heures plus tard, super fier de moi (c’était bien plus difficile que prévu), que je m’apprête à publier ma solution et que je tombe sur cet autre post rendant ma solution complètement inutile et obsolète. C’est la vie. Je publie quand même car cet exemple montre une multitude d’utilisations de la Reflection sur des types et des méthodes génériques ainsi que ma première utilisation du mot clé dynamic.

Si vous souhaitez vous servir de Entity Framework Code Only c’est par ici : http://blogs.msdn.com/b/adonet/archive/2010/07/14/ctp4codefirstwalkthrough.aspx

Quelle aventure ! :-s



/// <summary>
/// Classe d'aide à la configuration Entity Framwork
/// permettant de déduire la configuration des attributs Key, Required et StringLength
/// de System.ComponentModel.DataAnnotations présent sur les POCO du model.
/// </summary>
/// <typeparam name="TContext">Type du context (ObjectContext)</typeparam>
class EntitiesConfigurator<TContext>
where TContext : ObjectContext
{
private readonly ContextBuilder<TContext> builder;

public EntitiesConfigurator(ContextBuilder<TContext> builder)
{
this.builder = builder;
}

public EntityConfiguration<T> TypeConfig<T>()
{
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var typeConfig = builder.Entity<T>();
foreach (var prop in properties)
{
// instancie un PropertyConfigurator<T, TProperty> avec les bons paramètres de type
// et le place dans une variable dynamique
// pour ne pas avoir à appeller ses méthodes par réflexion
dynamic prop_configurator =
typeof(PropertyConfigurator<,>).MakeGenericType(typeof(T), prop.PropertyType)
.GetConstructor(new Type[] { })
.Invoke(new object[] { });

var propertyAccessExpression = prop_configurator.GetPropertyAccessExpression(prop);
prop_configurator.TypeConfig_Type(typeConfig, prop, propertyAccessExpression);
prop_configurator.TypeConfig_Property(typeConfig, prop, propertyAccessExpression);
}

return typeConfig;
}
}

class PropertyConfigurator<T, TProperty>
{
public Expression GetPropertyAccessExpression(PropertyInfo prop)
{
// d'abord l'expression du paramètres
var paramExpression = Expression.Parameter(typeof(T), "obj");

// puis l'expression entière
// Expression<Func<T, TProperty>> expression = obj => obj.Property
var expression = Expression.Lambda<Func<T, TProperty>>(
Expression.Property(paramExpression, prop.Name),
paramExpression
);

return expression;
}

public void TypeConfig_Type(EntityConfiguration<T> typeConfig, PropertyInfo prop, object propertyAccessExpression)
{
// ici le paramètre propertyAccessExpression à été passé en objet
// pour permettre à la compilation dynamique de trouver la bonne méthode
var propertyAccess = propertyAccessExpression as Expression<Func<T, TProperty>>;

// Si la propriété porte l'attribut [Key]
var isKey = prop.GetCustomAttributes(typeof(KeyAttribute), true).Any();
// On déclare la clé
if (isKey) typeConfig.HasKey<TProperty>(propertyAccess);
}

public void TypeConfig_Property(EntityConfiguration<T> typeConfig, PropertyInfo prop, object propertyAccessExpression)
{
// ici le paramètre propertyAccessExpression à été passé en objet
// pour permettre à la compilation dynamique de trouver la bonne méthode
var propertyAccess = propertyAccessExpression as Expression<Func<T, TProperty>>;

// Valeur (ou pas) de MaximumLength de l'attribut [StringLength(int)]
// s'il est présent sur la propriété
int? stringLenght =
prop.GetCustomAttributes(typeof(StringLengthAttribute), true)
.Select(att => (int?)((StringLengthAttribute)att).MaximumLength)
.FirstOrDefault();

// Si la propriété porte l'attribut [Required]
bool isRequired = prop.GetCustomAttributes(typeof(RequiredAttribute), true).Any();

if (!isRequired && !stringLenght.HasValue) return; // pas la peine d'aller plus loin

// on appel en dynamique la méthode Property() de typeConfig
// (qui possède 26 surcharge définition différentes typées pour des types tels que int, int?, string, bool, bool? ...)
// l'appel dynamique permet d'éviter de faire de la réflexion mais il peut ne pas trouver la bonne définition
// donc l'appel est protégé.
dynamic dyna_typeConfig = typeConfig;
dynamic propConfig;
try { propConfig = dyna_typeConfig.Property(propertyAccess); }
catch (RuntimeBinderException) { return; } // pas de configuration possible pour le type de cette propriété

// Les appels dynamiques suivants sont protégés

if (stringLenght.HasValue) // Configuration de la taille de chaine dans la base de donnée
try { PropertyConfigurationExtensions.HasMaxLength(propConfig, stringLenght.Value); }
catch (RuntimeBinderException) { }

if (isRequired) // Configuration du null dans la base de donnée
try { PropertyConfigurationExtensions.IsRequired(propConfig); }
catch (RuntimeBinderException) { }
}
}

Aucun commentaire: