Accueil Nos publications Blog Mapping d’entités en .net

Mapping d’entités en .net

Parmi les tâches les plus rébarbatives que nous effectuons en tant que développeur, il en existe certaines plus faciles à simplifier, c’est le cas du mapping entre plusieurs entités.

Imaginons le cas classique : dans votre application, vous utilisez des Models qui recueillent les informations de votre base de données et des ViewModels  permettant de faire le lien entre vos Models et vos Views . Bref, du MVVM classique.

Comment écrire simplement et surtout de façon robuste le mapping entre les Models et les ViewModels ? Voici un exemple de Mapper utilisant les Expressions Linq.

Pourquoi utiliser des Expressions Linq ?

En fait, ce n’est pas parce que ça fait “beau” ou que c’est dans “l’air du temps”, mais parce que les Expressions Linq sont validées à la compilation et ainsi en cas de changement sur l’une des entités, j’aurai une erreur du compilateur.

Je préfère donc écrire :


AddBinding( a => a.Property1, b => b.Property2 );

à la place de :


AddBinding( a, "Property1", b, "Property2" );

Cet article vous expliquera comment utiliser les Expressions Linq pour créer un mapper.

Stocker les mappings

Pour commencer, il faut décider comment nous allons enregistrer le mapping des propriétés.

L’idéal est de stocker un couple (a.Property1, b.Property2) voire (a => a.Property1, b => b.Property2 ). L’usage du Tuple<T1, T2> me semble approprié.
Un Tuple est une structure de données qui a un nombre spécifique et une séquence de valeurs, on les utilise communément pour représenter un jeu de données.

Enfin, le type de l’expression a => a.Property1 n’est pas forcément évident au premier abord, voyons comment le déterminer.

En fait, cette expression cache un délégué anonyme que l’on peut réécrire sous sa forme normale :


object AnonymousDelegate(TypeA a) {
    return a.Property1;
}

Ce délégué est de la forme Func<TypeA, object>.

Résumons : nous souhaitons stocker une collection de Tuple<T1, T2> dont les types T1 et T2 sont de la forme Func<Type, object>.
Et comme c’est l’expression qui nous intéresse et non le délégué, nous utiliserons le type Expression<> pour englober tout cela.

Voici donc notre collection :


private List<Tuple<Expression<Func<TypeA, object>>, Expression<Func<TypeB, object>>>> bindings;

Cependant, je trouve qu’ajouter des items dans ce genre de collection devient vite “laborieux” et si justement je me suis attelé à la création de ce mapper, c’était avant tout pour me simplifier la vie !

Ex :


bindings.Add(
   new Tuple<Expression<Func<TypeA, object>>, Expression<Func<TypeB, object>>>()
   {
      a => a.Property1,
      b => b.Property2
   }
);

Vous pouvez remarquez à quel point c’est verbeux !

Je simplifie tout ça rapidement avant d’en être malade 🙂
Je vais donc créer une méthode AddBinding dans laquelle je passerai en paramètre mes expressions. Avec en prime une gestion d’exception en cas de types différents.


public void AddBinding(
   Expression<Func<TypeA, object>> entity,
   Expression<Func<TypeB, object>> model, bool thrownOnTypeMismatch = true) {

if (thrownOnTypeMismatch &&
entity.ReturnType != model.ReturnType) {
throw new Exception("Types are differents !");
}

this.Bindings.Add(
           new Tuple<Expression<Func<TypeA, object>>, Expression<Func<TypeB, object>>>(
              entity,
              model
           )
         );
}

Et maintenant, pour ajouter un lien entre 2 propriétés il suffit d’écrire :


AddBinding( a => a.Property1, b => b.Property2 );

Et voilà !
Maintenant que nous avons la liste des liens entre nos entités, il va falloir copier les valeurs de l’une vers l’autre !

Copier les données de A vers B

Donc, ce que j’aimerais obtenir, c’est que la propriété Property2 de b soit égale à a.Property1 : b.Property2 = a.Property1 !
Mais ceci peut-être exprimé sous la forme d’une expression Linq :


(a, b) => b.Property2 = a.Property1;

C’est maintenant que ça va se corser car nous allons construire dynamiquement l’expression Linq correspondante :

Construction de l’expression Linq d’assignation

Pour construire cette expression, on va avoir besoin de tous les éléments qui la compose :

  • (a, b) : les paramètres
  • a.Property1 : a et sa propriété
  • b.Property2 : b et sa propriété
  • = : opérateur d’assignation

Commençons par récupérer les paramètres (a, b) :


var parama = Expression.Parameter(typeof(TypeA), "a");
var paramb = Expression.Parameter(typeof(TypeB), "b");

Puis récupérons les noms des propriétés (a.Property1 et b.Property2)


var propertyNamea = ((MemberExpression)binding.Item1.Body).Member.Name;
var propertyNameb = ((MemberExpression)binding.Item2.Body).Member.Name;

Continuons avec les expressions Object.Property


var propertyOfa = Expression.PropertyOrField(parama, propertyNamea); // a.Property1
var propertyOfb = Expression.PropertyOrField(paramb, propertyNameb); // b.Property2

Dernière étape, l’assignation :


var assignation = Expression.Assign(propertyOfb, propertyOfa); // b.Property2 = a.Property1

Maintenant, il va falloir transformer cela en quelque chose d’utilisable par C# et donc passer d’une Expression Linq à un délégué du type : (a, b) => b.Property2 = a.Property1;
==> donc un délégué du type Action<TypeA, TypeB>.

Pour cela, rien de plus simple ! Utilisons une nouvelle fois la classe Expression pour construire une Lambda expression :


var lambda = Expression.Lambda<Action<TypeA, TypeB>>(assignation, parama, paramb);
var action = lambda.Compile();

Remarque pour qu’une Lambda Expression soit utilisable, il faut qu’elle soit compilée, d’où la ligne contenant lambda.Compile().

Et voilà, pour assigner la valeur de a.Property1 vers b.Property2, il suffit donc d’appeler l’action comme ceci :


action(a, b);

Conclusion

Je ne vous ai pas décrit l’intégralité du Mapper car je pense qu’il n’est pas nécessaire de s’attarder sur les autres points.

Bien qu’elle ne soit pas parfaite (certaines optimisations restent à faire), cette classe permet de gagner pas mal de temps sur le mapping entres entités. Les Expressions Linq garantissent la levée d’erreur à la compilation en cas de changement…

Je vous invite à découvrir le code dans sa totalité sur mon partage SkyDrive