Exposer les POCO Entity Framework 4 via un service WCF, résolution du problème de référence circulaire : 3ème partie#

Le problème de référence circulaire n’est pas propre à Entity Framework mais plutôt un problème de sérialisation WCF, dans cet article nous allons expliquer le problème et proposer une solution.

Soit le modèle edmx suivant :

image

Ce modèle a été très bien conçu et reflète bien la réalité du métier, par contre pour le développeur ce n’est pas vraiment la joie parce que s’il a envie de savoir à quel StudentGroup appartient un étudiant (Person_Student) il devra passer par 4 tables alors que c’est censé être l’opération métier la plus élémentaire. Passer par ces 4 tables est non seulement pénible pour le développeur mais en plus catastrophique au niveau perf pour au final une opération très simple. Après une discussion houleuse entre le Développeur et le DBA, le DBA accepte (une fois n’est pas coutume:)) d’ajouter un raccourci matérialisé en une table de lien entre Student et StudentGroup permettant de déterminer directement l’appartenance d’un étudiant à un groupe d’étudiants. Voici ce qu’est devenu le modèle :

image

Si le DBA était retissant au rajout de ce raccourci c’est à cause de ce cycle (entouré en rouge) introduit au sein du modèle. Ce cycle est sans conteste une mauvaise pratique en design mais dans quelques cas ça s’avère bien pratique.

Tout content (juste parce qu’il a eu gain de cause:)), le développeur se met à développer son service WCF en créant une méthode qui renvoie un étudiant en embarquant quelques données liées. Voici le corps de la méthode :

public Person_Student GetById(int id) 
{
       SuiviScolaireEntities context = new SuiviScolaireEntities();
       return context.Person_Student.Include("Pass")
                                  .Include("Pass.Exam")
                                  .Include("Pass.Exam.Teach")
                                  .Where(s => s.PersonId == id)
                                  .FirstOrDefault<Person_Student>();               
}

Il crée ensuite la partie client pour appeler le service et en exécutant il obtient l’erreur suivante :

image

Bon je vous l’accorde ce n’est pas très explicite comme erreur, pour y voir plus clair nous allons activer la trace WCF, voici la vrai erreur qui se cache derrière ce message :

There was an error while trying to serialize parameter http://tempuri.org/:GetStudentByIdResult. The InnerException message was 'Object graph for type 'xxxxxxx' contains cycles and cannot be serialized if reference tracking is disabled.'.  Please see InnerException for more details.

L’erreur dit que le modèle comporte un cycle et qu’il ne peut être sérialisé via WCF. Le développeur la ramène moins d’un coup mais se dit qu’il faut absolument résoudre ce problème pour ne pas avoir l’air con et que le DBA lui dise : je t’avais dit que ce n’est pas bien!

Analyse du problème

Le problème est que le DataContractSerializer va vouloir sérialiser explicitement les objets à renvoyer au client au lieu de travailler avec des références, supposons que l’on ai ce modèle :

public class Person
{
    public int Id { get; set; }

    public int FirstName { get; set; }

    public int LastName { get; set; }

    public List<Person> Enfants { get; set; }
}

Et que j’ai une List<Person> dans laquelle j’ai inséré des données de la manière suivante :

Person pEnfant1 = new Person { Id = 1, FirstName = "Thierry", LastName = "BOB" }; //Enfant
Person pEnfant2 = new Person { Id = 2, FirstName = "Marcus", LastName = "TIBH" }; //Enfant
Person pEnfant3 = new Person { Id = 3, FirstName = "Adrien", LastName = "MOLBE" }; //Enfant
Person pEnfant4 = new Person { Id = 4, FirstName = "Yam", LastName = "BOB" }; //Enfant
Person pParent1 = new Person
{
    Id = 5,
    FirstName = "Julien",
    LastName = "BOB",
    Enfants = new List<Person> { pEnfant1, pEnfant4 }
}; //Parent

Person pParent2 = new Person
{
    Id = 6,
    FirstName = "Gilian",
    LastName = "TIBH",
    Enfants = new List<Person> { pEnfant2 }
}; //Parent

Par défaut, le DataContractSerializer instancié par WCF sérialise les données de la manière suivante :

<ArrayOfPerson xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Person>
    <Enfants i:nil="true"/>
    <FirstName>Thierry</FirstName>
    <Id>1</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person>
    <Enfants i:nil="true"/>
    <FirstName>Marcus</FirstName>
    <Id>2</Id>
    <LastName>TIBH</LastName>
  </Person>
  <Person>
    <Enfants i:nil="true"/>
    <FirstName>Adrien</FirstName>
    <Id>3</Id>
    <LastName>MOLBE</LastName>
  </Person>
  <Person>
    <Enfants i:nil="true"/>
    <FirstName>Yam</FirstName>
    <Id>4</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person>
    <Enfants>
      <Person>
        <Enfants i:nil="true"/>
        <FirstName>Thierry</FirstName>
        <Id>1</Id>
        <LastName>BOB</LastName>
      </Person>
      <Person>
        <Enfants i:nil="true"/>
        <FirstName>Yam</FirstName>
        <Id>4</Id>
        <LastName>BOB</LastName>
      </Person>
    </Enfants>
    <FirstName>Julien</FirstName>
    <Id>5</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person>
    <Enfants>
      <Person>
        <Enfants i:nil="true"/>
        <FirstName>Marcus</FirstName>
        <Id>2</Id>
        <LastName>TIBH</LastName>
      </Person>
    </Enfants>
    <FirstName>Gilian</FirstName>
    <Id>6</Id>
    <LastName>TIBH</LastName>
  </Person>
</ArrayOfPerson>

Remarquez dans ce flux XML généré par le DataContractSerializer que les personnes peuvent se répétées plusieurs fois dans le flux et que leur description entière est reprise à chaque fois qu’elles apparaissent. Marcus TIBH apparaît dans le flux 2 fois : Une fois comme “Person” sans enfants et ses informations sont reprises complètement en tant que "Enfant” de Gilian TIBH.

En effet, par défaut, WCF instancie le DataContractSerializer avec la propriété preserveObjectReferences = false. Cette propriété permet de spécifier de ne pas préserver les références d’objets lors de la sérialisation et du coup répéter à chaque fois entièrement les données.

WCF n’autorise pas les références circulaires avec ce mode de sérialisation et l’attribut preserveObjectReferences = false.

1ère Solution

La première solution est décrite dans le blog de sowmy et qui consiste à modifier le fonctionnement par défaut de WCF en instanciant le DataContractSerializer avec le preserveObjectReferences = true. Ceci est possible en héritant de DataContractSerializerOperationBehavior et en surchargeant sa méthode CreateSerializer qui renvoie le Serializer au moteur WCF. Nous pourrons par la suite faire de notre classe un attribut avec lequel nous allons décorer toutes les méthodes qui vont avoir besoin de ce mode de fonctionnement.

2ème solution

La deuxième méthode consiste à définir la propriété IsReference à True sur les classes qui vont être sérialisés de cette manière :

[DataContract(IsReference=true)]
public class Person
{
    [DataMember]
    public int Id { get; set; }

    [DataMember]
    public string FirstName { get; set; }

    [DataMember]
    public string LastName { get; set; }

    [DataMember]
    public List<Person> Enfants { get; set; }
}

De cette manière le DataContractSerializer va travailler avec les références à chaque fois qu’il s’agit de sérialiser un objet du type de cette classe.

Pour ces 2 méthodes, voici le XML résultant :

<ArrayOfPerson xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Person z:Id="i1" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants i:nil="true"/>
    <FirstName>Thierry</FirstName>
    <Id>1</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person z:Id="i2" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants i:nil="true"/>
    <FirstName>Marcus</FirstName>
    <Id>2</Id>
    <LastName>TIBH</LastName>
  </Person>
  <Person z:Id="i3" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants i:nil="true"/>
    <FirstName>Adrien</FirstName>
    <Id>3</Id>
    <LastName>MOLBE</LastName>
  </Person>
  <Person z:Id="i4" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants i:nil="true"/>
    <FirstName>Yam</FirstName>
    <Id>4</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person z:Id="i5" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants>
      <Person z:Ref="i1"/>
      <Person z:Ref="i4"/>
    </Enfants>
    <FirstName>Julien</FirstName>
    <Id>5</Id>
    <LastName>BOB</LastName>
  </Person>
  <Person z:Id="i6" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Enfants>
      <Person z:Ref="i2"/>
    </Enfants>
    <FirstName>Gilian</FirstName>
    <Id>6</Id>
    <LastName>TIBH</LastName>
  </Person>
</ArrayOfPerson>

Vous avez certainement remarqué les attributs z:Id et z:ref qui permettent de ne plus reprendre toutes les données d’une instance et de la référencer via son Id.

Enjoy ;)

Monday, March 15, 2010 8:13:00 PM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Comments are closed.
All content © 2012, Zied Nemili