cogimator.net

Une ligne à la fois...

Implémentation naive d'un Serializer en C# - Partie 6

Une des dernières parties de cette série d’article concerne la sérialisation correcte des graphes d’objets, et surtout de leurs références. En effet, le tableau déclaré dé la manière suivante :

var instance = new TestReference() { Str = "3" };
var array = new TestReference[] { instance, instance, instance, instance };

Ne doit pas sérialiser l’objet “Instance” quatre fois, mais une seule. La résolution de ce problème permettra également d’aborder les références cycliques :

var t1 = new TestReference() { Str = "1" };
var t2 = new TestReference() { Str = "2" };

t1.Reference = t2;
t2.Reference = t1;

Dans un premier temps, il faut pouvoir identifier les objets de manière unique. Dans un premier temps, j’ai régardé du coté de la classe GCHandlepour obtenir l’adresse mémoire des objets. Cette méthode ne s’est pas avérée adaptée : le Garbage Collector pouvant déplacer les objets et donc changer leur adresse mémoire.

En fait, le framework offre une classe toute faite pour identifier les objets : ObjectIDGenerator, qui permet d’obtenir un identifiant unique pour chaque objet passé en paramètre à la méthode GetId. Et en bonus, cette méthode indique même si il s’agit d’une instance déjà identifiée.

La modification de la classe DefaultObjectSerializer s’avère donc simple :

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    bool firstTime;

    // generate unique id for object, in order not to save same object multiple times
    var key = idGenerator.GetId(source, out firstTime);

    writer.Write(firstTime);
    writer.Write(key);

    if (firstTime)
    {
        // inspect object
        foreach (var prop in sourceType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).OrderBy(x => x.Name))
        {
            this.SerializeBase(prop.FieldType, prop.GetValue(source), writer);
        }
    }
}

public override object Deserialize(ExtendedBinaryReader source, object target, Type type)
{
    var firstTime = source.ReadBoolean();
    var key = source.ReadInt64();

    if (!firstTime)
    {
        return cache[key];
    }
    else
    {
        var destination = Activator.CreateInstance(type);

        // add instance to cache before deserializing properties, to untangle eventual cyclic dependencies
        cache.Add(key, destination);

        // inspect object
        foreach (var prop in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).OrderBy(x => x.Name))
        {
            var v = this.DeserializeBase(prop.FieldType, destination, source);
            prop.SetValue(destination, v);
        }

        return destination;
    }
}

En résumé, pour la sérialisation, on identifie chaque objet sérialisé, et si il est connu, on ne sérialise que son identifiant. Pour la désérialisation, on place les objets et leur identifiant dans un dictionnaire faisant office de cache.

La subtilité concerne les références cycliques : il faut placer l’objet dans le cache juste après sa création, car lorsque l’on va désérialiser les propriétés de l’objet, on pourrait rencontrer une référence vers un objet que l’on n’aurait pas encore désérialisé.

Comme toujours, les tests unitaires permettent de valider que les modifications n’entrainent pas de régressions.

Implémentation naive d'un Serializer en C# - Partie 5

Après avoir refactorisé le code du Serializer, j'ai choisi d'implémenter la sérialisation des implémentations d'interfaces. En effet, si un objet possède des propriétés de type interface (IList<T> par exemple), ce n'est pas pour autant qu'il ne doit pas pouvoir être sérialisable.

L'implémentation de la classe InterfaceSerializer s'est avérée plus simple que prévue : dans le principe, si un type de propriété est une interface, et que la valeur de cette propriété est non nulle, le type de la valeur (via GetType()) est sérialisé.

Ensuite, on appelle simplement le sérializer racine, en lui substituant le type de l'interface par le type de l'instance qui l'implémente.

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    var st = source.GetType();

    // write implemented type
    writer.Write(st);

    // continue with implemented type and not interface
    base.Serialize(writer, source, st);
}

Implémentation naive d'un Serializer en C# - Partie 4

Après avoir implémenté le support de la sérialisation des tableaux,  le code est devenu moins lisible. En effet, toute la logique pour le choix du type d'objet à sérialiser se faisait dans une seule méthode,  bardée de ifs. Le but de cette implémentation étant de m'amuser, je ne pouvais laisser cette situation perdurer.

Pour simplifier le code, j'ai choisi d'implémenter une interface ISubSerializer plus spécialisée. Le principe est simple, chaque implémentation de ISubSerializer est applicable pour un type d'objet donné, et sait comment le sérialiser/déserialiser. Ces instances sont organisées de manière hiérarchique, chacune pouvant avoir des ISubSerializer enfant.

public interface ISubSerializer
{
    bool CanApply(Type type);
    void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType);
    object Deserialize(ExtendedBinaryReader source, object target, Type type);
}

Le serializer racine, quant à lui, se charge d'appeler le ISubSerializer adéquat, en fonction du type d'objet à sérialiser.

public void SerializeBase(Type sourceType, object source, ExtendedBinaryWriter writer)
{
    var serializers = this.SubSerializers ?? this.Root.SubSerializers;

    foreach (var s in serializers)
    {
        if (s.CanApply(sourceType))
        {
            s.Serialize(writer, source, sourceType);
            return;
        }
    }
}

Par exemple, dans le cas des types nullables, la class ObjectSerializer se charge uniquement de sérialiser un boolean indiquant si l'objet est null ou non.

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    // always write a boolean indicating if object is null or not
    var hasValue = source != null;
    writer.Write(hasValue);

    if (hasValue)
    {
        this.SerializeBase(sourceType, source, writer);
    }
}

Ensuite, la classe NullableSerializer va appeler le serializer racine pour sérialiser le type valeur sous-jacent.

public override void Serialize(ExtendedBinaryWriter writer, object source, Type sourceType)
{
    this.SerializeBase(sourceType.GetGenericArguments().First(), source, writer);
}

De cette manière,  ajouter le support pour de nouveaux types devient l'affaire d'implémenter un nouveau ISubSerializer et de le déclarer dans le serializer racine.

Le code source est disponible sur GitHub : https://github.com/mathieubrun/Cogimator.Serialization/blob/master/Cogimator.Serialization/

Implémentation naive d'un Serializer en C# - Partie 3

Cette semaine, l'itération suivante de notre serializer supporte les tableaux d'objets.

Avec la propriété Array.Rank ainsi que la méthode Array.GetLength(int), je peux déterminer le nombre de dimensions du tableau, ainsi que la taille de celles-ci. Ces valeurs seront sérialisées dans le flux binaire pour reconstruire le tableau lors de la désérialisation.

La méthode Array.GetValue(int[]) permet de récupérer la valeur dans le tableau en fonction des indices passés en paramètre. Le calcul de tous les indices se fait en deux étapes. Dans un premier temps, il faut déterminer les indices possibles pour chaque dimension du tableau, en fonction de sa longueur. Ensuite, il reste à calculer le produit cartésien de tous ces indices. Cet ensemble servira à sérialiser l'ensemble des valeurs du tableau.

La désérialisation est simplement le procédé inverse.

Le code source est disponible sur GitHub : https://github.com/mathieubrun/Cogimator.Serialization/blob/master/Cogimator.Serialization/ReflectionSerializer.cs

Implémentation naive d'un Serializer en C# - Partie 2

Pour la suite de cette série, je vais vous présenter le fonctionnement de la 1ere itération du serializer. Cette version supporte les types de base, nullables ou non, ainsi que les graphes d'objets simples (pas de cycles, pas de tableaux).

Dans un premier temps, le serializer va détecter le type de l'objet passé en paramètre. Si il s'agit d'un type valeur standard, celui ci est écrit dans le flux.

Si il s'agit d'un objet, celui ci sera inspecté. Cette inspection servira a déterminer si il s'agit d'un Nullable, ou d'un autre type d'objet. Dans les deux cas, on écrira dans le flux un booléen indiquant si l'objet est null.

L'écriture dans le flux se fait au travers de la classe BinaryWriter, étendue pour gérer les cas spécifiques. La classe BinaryWriter de base ne supportant les chaines nulles, la méthode Write(string) est surchargée pour gérer ce cas.

La désérialisation suit le même principe, le flux d'octets est lu à l'aide d'une implémentation spécifique de la classe BinaryReader, et l'objet est reconstitué.

Le code source est disponible sur GitHub : https://github.com/mathieubrun/Cogimator.Serialization/blob/master/Cogimator.Serialization/ReflectionSerializer.cs

Implémentation naive d'un Serializer en C# - Partie 1

Cet article sera le premier d'une série qui me démangeait depuis quelque temps, sur l'implémentation d'un serializer en C#. Le but de cette implémentation n'est pas de rivaliser avec un protobuf-net, mais plutôt l'occasion d'écrire un peu de code plus bas niveau que d'habitude.

Pour commencer, le but est d'obtenir le même niveau de fonctionnalité que le BinaryFormatter du Framework. Voilà pour nos spécifications fonctionnelles détaillées !

Au niveau de l'implémentation, j'ai choisi de commencer par une approche naïve basée sur la reflection. Cette première étape permettra la mise en place des tests unitaires automatisés servant à vérifier le bon fonctionnement du Serializer.

Ce bon fonctionnement s'appuie sur deux phases : durant la sérialisation, notre objet est transformé en tableau d'octets. Durant la désérialisation, ce même tableau d'octets sert à recomposer notre objet. Il faut dont choisir un format de stockage pour les différentes données présentes. J'ai choisi de rester le plus simple possible : les membres des notre objet sont sérialisés dans l'ordre alphabétique. 

Ceci a pour implication, tout comme le BinaryFormatter, de ne permettre de désérialiser uniquement dans le même objet: si des définitions de champs sont modifiées, le processus ne fonctionnera plus.

Les champs de notre objet étant sérialisés dans l'ordre alphabétiques, dans un premier temps les champs de type valeur sont convertis en tableau d'octets et placés les uns a la suite des autres. Pour les string, les octets représentant la chaîne seront précédés de la longueur de celle ci.

Pour les autres types de champs (object, nullables, ...) ce sera l'objet de l'article suivant.

Implémenter simplement un formulaire dynamique avec WPF

En s'appuyant sur les DataTemplates et les capacités de binding de WPF, on peut très facilement mettre en place des formulaires dynamiques. La première étape est de définir les classes qui serviront à définir les éléments de notre formulaire :

Ensuite, on va définir les DataTemplates qui vont permettre leur affichage :

Et pour finir, il reste juste a ajouter les éléments dans un conteneur :

Tout le reste est géré par le WPF, qui, au travers des DataTemplates et du Binding affichera les contrôles nécessaires pour remplir nos éléments.

Le code source complèt est disponible sur : https://github.com/mathieubrun/Cogimator.Samples

Implémentation d'un service WCF générique

Parfois on peut être améné a déclarer un service de manière générique :

Toutefois, le service ainsi déclaré ne pourra pas être utilisé dans IIS sans modifier la déclaration dans le fichier .svc :

Exemple complet sur : https://github.com/mathieubrun/Cogimator.Samples

Hériter un style WPF par défaut

Parfois il peut arriver d’avoir besoin de baser un style sur un autre déclaré sans x:Key (s'applicant donc a tous les éléments correspondant à l'attribut TargetType. Ici; la clé est de déclarer l'attribut BasedOn avec non pas une ressource, mais le type de l'élément sur lequel appliquer le style.

Cela a le mérite d’être plus concis et plus explicite que le pattern suivant :

Comment utiliser protobuf-net avec WCF

Protobuf-net est l'implémentation .NET de protocol buffers mis au point par Google, qui l'utilise comme protocole de communication pour ses échanges de données. L'objectif est d'obtenir une sérialisation binaire de faible taille (largement moindre qu'une sérialisation XML par exemple), et peu coûteuse a sérialiser et désérialiser, autant coté serveur que client.

L'intérêt de protobuf-net est le gain en performances offert, grâce a une sérialisation plus efficace que le DataContractSerializer utilisé par défaut dans WCF.En utilisant des entités Linq-to-Sql générées à partir de la base Northwind :

  • Sérialisation 3x plus rapide
  • Désérialisation 4x plus rapide
  • Taille 3.5x moindre

Pour l'implémenter rapidement dans un projet existant (afin de valider un gain en performances substantiel), 3 étapes principales a suivre :

Placer un attribut sur les classes devant être sérialisées :

[DataContract]
[ProtoBuf.ProtoContract(ImplicitFields = ProtoBuf.ImplicitFields.AllFields)]
public class CompositeType
{
	public CompositeType()
	{
		this.Children = new List<CompositeTypeChild>
		{
			new CompositeTypeChild { IntValue = 1234, StringValue = "1234" },
			new CompositeTypeChild { IntValue = 5678, StringValue = "5678" }
		};
	}

	[DataMember]
	public int IntValue { get; set; }

	[DataMember]
	public string StringValue { get; set; }
	
	[DataMember]
	public List<CompositeTypeChild> Children { get; set; }
}

[DataContract]
[ProtoBuf.ProtoContract(ImplicitFields = ProtoBuf.ImplicitFields.AllFields)]
public class CompositeTypeChild
{
	[DataMember]
	public int IntValue { get; set; }

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

Modifier la configuration du serveur, en rajoutant un endpointBehavior

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="WcfService.Service" behaviorConfiguration="WcfService.ServiceBehavior">
        <endpoint address="" binding="basicHttpBinding" contract="WcfService.IService" behaviorConfiguration="protoEndpointBehavior" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WcfService.ServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="protoEndpointBehavior">
          <protobuf/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <extensions>
      <behaviorExtensions>
        <add name="protobuf" type="ProtoBuf.ServiceModel.ProtoBehaviorExtension, protobuf-net"/>
      </behaviorExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

Modifier la configuration du client

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address="http://localhost:1085/Service.svc" binding="basicHttpBinding" contract="ServiceReference.IService"
          behaviorConfiguration="protoEndpointBehavior"
          name="BasicHttpBinding_IService" />
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name="protoEndpointBehavior">
          <protobuf/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <extensions>
      <behaviorExtensions>
        <add name="protobuf" type="ProtoBuf.ServiceModel.ProtoBehaviorExtension, protobuf-net"/>
      </behaviorExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

Et le résultat dans Fiddler :

Avant :

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <GetDataUsingDataContractResponse xmlns="http://tempuri.org/">
      <GetDataUsingDataContractResult xmlns:a="http://schemas.datacontract.org/2004/07/WcfService" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <a:Children>
          <a:CompositeTypeChild>
            <a:IntValue>1234</a:IntValue>
            <a:StringValue>1234</a:StringValue>
          </a:CompositeTypeChild>
          <a:CompositeTypeChild>
            <a:IntValue>5678</a:IntValue>
            <a:StringValue>5678</a:StringValue>
          </a:CompositeTypeChild>
        </a:Children>
        <a:IntValue>1234</a:IntValue>
        <a:StringValue>1234</a:StringValue>
      </GetDataUsingDataContractResult>
    </GetDataUsingDataContractResponse>
  </s:Body>
</s:Envelope>

Apres :

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <GetDataUsingDataContractResponse xmlns="http://tempuri.org/">
      <proto>CgkI0gkSBDEyMzQKCQiuLBIENTY3OAoJCNIJEgQxMjM0CgkIriwSBDU2NzgQ0gkaBDEyMzQ=</proto>
    </GetDataUsingDataContractResponse>
  </s:Body>
</s:Envelope>

.