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]  | 

 

Webcast des Techdays 2010 en ligne#

ça y est!!! tous ceux qui n’ont pas eu la chance d’être au Palais des congrès du 8 au 10 février vont pouvoir prendre leur revanche puisque les webcast de toutes les sessions des Techdays 2010 sont en ligne depuis ce matin à cette adresse. Je vous conseille bien évidemment celle que j’ai animé avec Arnaud AUROUX intitulée Silverlight 4 : Cas pratique du mode déconnecté. Vous trouverez attaché à cet article la solution que nous avons développé pendant la session.

Friday, March 12, 2010 12:11:06 AM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

Debugger un service WCF#

Nous pouvons debugger notre code au sein d’un service WCF de la même manière que tout autre code .NET, en définissant des points d’arrêts au niveau du code que  nous voulons débugger et si nous utilisons le serveur web IIS nous devons alors nous attacher au processus de ce dernier. Mais il arrive que notre code s’exécute bien et ne lève aucune exception alors que côté client nous récupérons une erreur qui ressemble à ça :

image

“The remote server returned an error: NotFound” pas très explicite comme erreur surtout lorsque vous savez que le code du service WCF a bien été atteint et qu’il s’est bien exécuté sans problème. Le problème s’est donc passé au sein de WCF après l’exécution de votre code du service. Pour avoir une erreur plus explicite et comprendre ce qui se passe réellement il suffit d’activer le tracing côté WCF en ajoutant cette entrée dans le web.config

  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel"
              switchValue="Information, ActivityTracing"
              propagateActivity="true">
        <listeners>
          <add name="WCFListener"
              type="System.Diagnostics.XmlWriterTraceListener"
              initializeData= "c:\wcflog\WCFTraces.svclog" />
        </listeners>
      </source>
    </sources>
  </system.diagnostics>

Après un nouvel appel de votre service, rendez vous au répertoire c:\wcflog et double cliquez sur le fichier WCFTraces.svclog, vous allez pouvoir découvrir la vraie exception

image

D’un coup, tout devient plus clair, dans mon cas j’ai un cycle dans mes classes.

Sunday, March 07, 2010 11:02:06 PM (Romance Standard Time, UTC+01:00) #    Comments [0]  | 

 

All content © 2012, Zied Nemili