Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Mapping d’objets en C#

Poster un commentaire

Le mapping des propriétés entre deux objets est une opération très courante et surtout très fastidieuse. Se pose ensuite le problème de la maintenance d’un tel code.

C’est pourquoi il est assez rare qu’on fasse le travail soit même à la main, on essaye de l’outiller.

 

Je pense que l’on peut identifier au moins 3 cas d’usage:

  1. cloner une instance d’objet vers une instance de même type
  2. cloner une instance d’objet vers une instance d’un type différent
  3. Micro ORM en sortie d’une requête ADO.NET

Je vais examiner ces 3 cas à la lumière de l’outillage possible.

Il existe plusieurs façon de faire une copie d’un objet.

  1. La copie profonde (deep copy)
    On essaye de tout copier, y compris les propriétés qui ne sont pas des types primitifs.
    Une copie profonde d’une collection va par exemple copier la structure, mais aussi les éléments eux-même
  2. la copie superficielle (shallow copy)
    On ne copie que le moins possible. Une copie d’une collection par exemple, ne va copier que la structure de la collection, pas ses éléments
  3. Copie mixte (mixed copy) ou encore copie paresseuse (lazy copy)
    Un mélange des deux

 

Il est important d’avoir cela en tête.

Cloner une instance d’objet vers une instance de même type

FluentMapper qui sera présenté plus loin répond au besoin, mais C# propose également des outils natifs qui souvent suffisent. Je vais donc plutôt me concentrer dessus dans ce chapitre.

 

Le clonage est pris en charge dans C# grâce à l’interface ICloneable qui expose une unique méthode:


Object Clone()

 

Cette interface cumule de nombreux problèmes. Tout d’abord il n’existe pas de version générique.

Le plus grave vient ensuite.

Il vous appartient donc d’implémenter vous même la méthode et c’est tout le problème car pour celui qui consomme la méthode, il n’est pas très clair de savoir ce qui est fait dans les coulisses et notamment quel type de copie.

 

Faute de définition claire proposée par la méthode Clone on s’expose à pas mal de bugs.

Voici un exemple d’utilisation de ICloneable:

class Personne: ICloneable
{
   public string Nom { get; set; }
   public string Prenom { get; set; }
 
   public object Clone()
   {
      return new Personne() { Nom = this.Nom, Prenom = this.Prenom };
   }
}

 

La classe Object expose également une méthode MemberwiseClone qui effectue une copie superficielle. Le code qui précède pourrait aussi s’écrire:

class Personne: ICloneable
{
   public string Nom { get; set; }
   public string Prenom { get; set; }
 
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

Qui présente l’avantage de ne pas avoir à gérer la liste des propriétés.

 

Vous l’avez compris, ces méthodes existent et peuvent se rencontrer dans du code un peu ancien, mais il est préférable d’utiliser les outils que nous allons décrire maintenant.

Cloner une instance d’objet vers une instance d’un type différent

Nous allons faire la connaissance de FluentMapper:

https://github.com/messagexpress/fluent-mapper

 

C’est un outil qui bénéficie d’une API Fluent ce qui en général donne des interfaces assez lisibles.

Attaquons toute de suite une ou deux démo dans un projet complété avec le package Nuget FluentMapper.

 

On va tout d’abord définir ces deux classes:

public class PersonneEntity
{
   public string Nom { get; set; }
   public string Prenom { get; set; }
   public DateTime DateNaissance { get; set; }
   public string Sexe { get; set; }
}
 
public class Personne
{
   public string Nom { get; set; }
   public DateTime DateNaissance { get; set; }
   public string Sexe { get; set; }
   public int Taille { get; set; }
}

Remarquons que Personne et PersonneEntity n’ont pas exactement les même membres. Ce sera important pour la suite.

 

Disons que l’on s’intéresse à la conversion d’une instance de PersonneEntity vers une instance de Personne.

FluentMapper demande tout d’abord de définir un IMapper de la façon suivante:


var mapper = FluentMapper
   .ThatMaps<Personne>().From<PersonneEntity>()
   .Create();

Armé de notre mapper on peut procéder à des conversions:


PersonneEntity source = new PersonneEntity() { Nom = "Chaplin", Prenom = "Charlie", Sexe = "M", DateNaissance = new DateTime(1889, 4, 16) };
Personne personne = mapper.Map(source);

A l’exécution les choses se passent mal:

2016-03-03_22-46-41

FluentMapper détecte que Personne expose la propriété Taille que n’a pas PersonneEntity. On verrait de la même façon un problème similaire avec PersonneEntity.Prenom.

Lever une exception est le choix technique fait par l’outil et les auteurs s’en explique dans le wiki du projet.

 

 

Dans notre cas la seule chose à faire est de lui dire de ne pas en tenir compte:


var mapper = FluentMapper
   .ThatMaps<Personne>().From<PersonneEntity>()
   .IgnoringSourceProperty(src => src.Prenom)
   .IgnoringTargetProperty(src => src.Taille)
   .Create();

Cette fois les choses se passent mieux et on obtient une instance de Personne.

 

On pourrait faire les choses autrement. Par exemple Personne n’a pas de propriété Prenom et le code qui précède perd cette information. Pourquoi ne pas concaténer Nom + Prenom dans Personne.Nom?

C’est bien entendu possible:


var mapper = FluentMapper
   .ThatMaps<Personne>().From<PersonneEntity>()
   .IgnoringSourceProperty(src => src.Prenom)
   .IgnoringTargetProperty(src => src.Taille)
   .ThatSets(trg=>trg.Nom).From(src=>src.Nom + " " + src.Prenom)
.Create();

Et vous constaterez que ça fonctionne.

Il peut arriver que la source soit Null. Dans ces condition NullReferenceException est levé ce qui n’est pas du meilleur effet.

Il est peut être mieux de renvoyer Null également:


var mapper = FluentMapper
   .ThatMaps<Personne>().From<PersonneEntity>()
   .IgnoringSourceProperty(src => src.Prenom)
   .IgnoringTargetProperty(src => src.Taille)
   .ThatSets(trg=>trg.Nom).From(src=>src.Nom + " " + src.Prenom)
   .WithNullSource().ReturnNull()
   .Create();

 

FluentMapper est comme on le voit un outil plutôt simple à utiliser. Si je devais faire une critique c’est que la doc est à peut près inexistante. De mon point de vue ce n’est pas sérieux de dire qu’il suffit de regarder les tests unitaires dans la mesure où ils ne sont pas commentés, pas plus que le code!

 

Quelle est la valeur ajoutée par rapport à ICloneable?

Tout d’abord la nature du clonage effectué n’est plus une boîte noire puisque c’est votre code qui en apporte la maîtrise. Vous pouvez de ce fait envisager plusieurs scénarios de copie alors que vous n’avez qu’une seule méthode Clone.

Un autre apport est cette fois de gérer des classes typées.

Mais le point intéressant est surtout que FluentMapper est capable de détecter automatiquement lorsque la structure des classes sources ou cibles a changé. Une exception est levée si une nouvelle propriété apparaît quelque part.

Micro ORM en sortie d’une requête ADO.NET

Le principal outil dans ce domaine est je pense Dapper:

https://github.com/StackExchange/dapper-dot-net

 

Dapper est propose sous la forme de méthodes d’extension de l’interface System.Data.IDbConnection. C’est un micro ORM spécialisé dans le mapping de DTO ADO.NET.

L’outil est extrêmement léger qui convient bien si on a également des besoins de performances. Le site GitHub propose d’ailleurs quelques métriques. Une des clefs des performances de Dapper est la mise en cache de certaines informations comme expliqué dans sa documentation.

 

Comme toujours allons voir tout de suite les démos. Je commence par créer une table qui hébergera la classe PersonneEntity présentée au chapitre précédent. On va d’ailleurs reprendre le même modèle objet.

Je laisse de côté les détails spécifiques à ADO.NET qui ne sont pas le sujet de cet article et la lecture de la table pourrait ressembler à ceci:

List<Personne> personnes = new List<Personne>();
 
using (var cnx = new SqlConnection(connexionString))
{
   cnx.Open();
   using (var cmd = new SqlCommand("select * from personne", cnx))
   {
      var reader = cmd.ExecuteReader();
      while (reader.Read())
      {
         Personne personne = new Personne();
 
         int pointer = reader.GetOrdinal("DateNaissance");
         personne.DateNaissance = reader.GetDateTime(pointer);
         pointer = reader.GetOrdinal("Nom");
         personne.Nom = reader.GetString(pointer);
         pointer = reader.GetOrdinal("Sexe");
         personne.Sexe = reader.GetString(pointer);
 
         personnes.Add(personne);
     }
   }
}

La partie pénible sont les lignes comme 13, 15, 17. De plus le nom des propriétés est codé dans une String.

On Dapper améliore le score?

Contrairement à FluentMapper, Dapper est optimisé pour ADO.NET. Il va donc intervenir au niveau du code lui-même et en particulier on n’aura plus besoin de gérer soi-même le DataReader.

On oublie pas également que Dapper ce sont des méthodes d’extension, on ajoute donc:


using Dapper;

Voici le code:

using (var cnx = new SqlConnection(connexionString))
{
   cnx.Open();
 
   var personnes = cnx.Query<Personne>("select * from personne");
}

Comme on le constate, le code est nettement plus agréable!

 

La requête peut très bien être paramétrée:


Personne personne = cnx.Query<Personne>("select * from personne where id=@id", new { Id = 2 }).Single();

Et bien sur il est possible d’appeler une procédure stockée:


Personne personne = cnx.Query<Personne>("GetPersonneById", new { Id = 2 }
    ,commandType: CommandType.StoredProcedure)
   .Single();

 

Puisque Dapper court-circuite DataReader, en tout cas se charge de le gérer, il peut également exécuter n’importe quelle fonction CRUD ou DDL à l’aide des méthodes Execute et ExecuteScalar qui fonctionnent de la même façon que celles du même nom dans les DataReader.

 

Je conseille d’utiliser un package Nuget qui fournit des extensions avec une syntaxe un peut plus naturelle:

Install-Package DapperExtensions

 

Son site GitHub avec sa doc est ici:

https://github.com/tmsmith/Dapper-Extensions

 

On complète le code avec cette déclaration:


using DapperExtensions;

 

Ce qui permet de réécrire les exemples qui précèdent:


// liste des personnes de la base
var personnes = cnx.GetList<Personne>();
// la personne ayant un id=2
Personne personne = cnx.Get<Personne>(2);

 

Les autres fonctions CRUD sont cette fois plus intuitives:


Personne personne = new Personne() { Nom = "Durand", Prenom = "Annabelle", Sexe = "F", DateNaissance = new DateTime(1950, 1, 1) };
cnx.Insert(personne);

La propriété Id (clef primaire) de l’objet Personne (si elle existe) est automatiquement mise à jour si c’est la base qui génère la valeur de la clef primaire.

 

On peut bien entendu ajouter des filtres, un prédicat dans le vocabulaire de l’outil:


var predicat = Predicates.Field<Personne>(f => f.Nom, Operator.Eq, "Chaplin");
var personnes = cnx.GetList<Personne>(predicat);

 

Qui retourne tous les enregistrements dont le nom est ‘Chaplin’.

 

J’ai du un peu triché pour faire fonctionner l’exemple qui précède en supprimant la propriété Taille de Personne qui n’existe pas dans la base.

C’est le comportement par défaut. Peut t’on le modifier?

 

En interne l’outil génère une instance de ClassMapper réglée sur un certain nombre de conventions, par exemple chaque POCO a au moins une propriété Id. Nous pouvons proposer notre propre instance pour adapter la mapping à nos besoins.

On commence par construire une classe personnalisée CustomMapper:

public class CustomMapper: ClassMapper<Personne>
{
   public CustomMapper()
   {
      Map(p => p.Taille).Ignore();
 
      //optionnel, mappe toutes les autres colonnes
      AutoMap();
   }
}

On demande d’ignorer la propriété Taille qui ne sera pas mappée. On consomme CustomMapper ainsi:

DapperExtensions.DapperExtensions.DefaultMapper = typeof(CustomMapper);
var personnes = cnx.GetList<Personne>();

 

D’autres opérations sont bien entendu possibles. On peut par exemple implémenter une logique qui décide dynamiquement si un mapping est possible en l’injectant en paramètre de AutoMap().

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s