RSS

UniversalSerializer

13 juil

[ Le code source se trouve dans l'article original en anglais et mis à jour, voir ici ]

Un sérialiseur universel pour .Net .

C’est quoi ?

UniversalSerializer est un sérialiseur pour .Net.
En d’autres termes, il transcode un arbre d’instances d’objets en un tableau d’octets, et vice versa.

En résumé

L’objectif de UniversalSerializer est d’être capable de sérialiser tout type sans effort.

  • Pas besoin d’ajouter des attributs ni des interfaces aux types (classes & structures).

  • Lorsqu’une instance de classe est référencée plusieurs fois, elle n’est sérialisée qu’une seule fois.

  • Les références circulaires sont autorisées.

  • Les mécanismes de sérialisation et de transcodage existant sont réutilisés.
    Actuellement: [Serializable], ISerializable, [ValueSerializer] et [TypeConverter].

  • Les types sans constructeur par défaut (sans paramètres) sont autorisés, si un constructeur paramétrique peut être exploité (c’est automatique).

  • Les classes ICollection non génériques peuvent être sérialisées si une méthode Add ou Insert peut être exploitée (c’est automatique).

Bien entendu, toutes les constructions ordinaires telles que les classes, structures, propriétés publiques, champs publics, énumérations, collections, dictionnaires, etc.. sont sérialisées par UniversalSerializer.

Lorsqu’un type n’est pas directement sérialisable, UniversalSerializer propose deux mécanismes:

  • ITypeContainers
    Nous pouvons englober le type (ou l’ensemble de types) qui pose problème dans une classe personnalisée qui gérera sa sérialisation et sa désérialisation.

  • Un ensemble de filtres
    Nous pouvons bloquer certains types, et demander au sérialiseur de stocker certains champs privés de la source.

Mon plus grand souhait est que les gens ajoutent de nouveaux Conteneurs et filtres dans le futur, et qu’ainsi peut-être un jour tous les types pourront être sérialisés.

Exemples d’utilisation

var data = new Window();
byte[] DataBytes = UniversalSerializer.Serialize(data);
var data2 = UniversalSerializer.Deserialize<Window>(DataBytes);

C’est aussi simple que ça !

Exemple pour WPF

Il existe une DLL spécialisée pour WPF, qui gère plus de types de WPF:

var data = new Window();
byte[] bytesData = UniversalSerializerWPF.SerializeWPF(data);
var data2 = UniversalSerializerWPF.DeserializeWPF<Window>(bytesData);

Exemple pour WinForms

Il existe une DLL spécialisée pour WinForms, qui gère plus de types de WinForms:

var data = new Form();
byte[] DataBytes = UniversalSerializerWinForm.SerializeWinForm(data);
var data2 = UniversalSerializerWinForm.DeserializeWinForm<Form>(DataBytes);

Pourquoi et comment est-ce fait ?

J’avais besoin d’un sérialiseur universel, capable de sérialiser n’importe quoi sans modification.

Mais les sérialiseurs existant posent des problèmes:

  • Certains nécessitent des attributs ou des interfaces spéciaux.
  • D’autres ne prennent pas en compte les champs.
  • Les références sont un vrai problème. En général, les instances sont dupliquées, et les références des objets désérialisés pointent vers différentes instances.
    Et les références circulaires ne sont jamais gérées.
  • Aucun ne peut gérer des types sans constructeur par défaut (sans paramètres), pour autant que je sache.
  • Certaines classes .Net particulières nécessitent plus d’attention mais sont scellées et donc ne peuvent pas être héritées avec des attributs de sérialisation.

Donc la solution nécessitait un ensemble de techniques et de mécanismes.
Nous allons passer en revue ces mécanismes dans les prochains chapitres.

Le code original

UniversalSerializer est construit à partir de fastBinaryJSON 1.3.7, de Mehdi Gholam, qui sérialise vers un format binaire inspiré par JSON.
UniversalSerializer y ajoute une certaine universalité en permettant de sérialiser n’importe quel type.
Notez que j’ai dû modifier fastBinaryJSON en profondeur, donc je ne peux plus me synchroniser avec le code source original de fastBinaryJSON. Par conséquent, UniversalSerializer n’est une extension de fastBinaryJSON et je ne maintiendrai pas de compatibilité dans le futur.

Sérialiser des classes sans constructeur par défaut

Dans .Net, les constructeurs de classes par défaut (sans paramètres) ne sont pas imposés. Mais presque tous les sérialiseurs en ont besoin.
Et des classes sans constructeur par défaut sont fréquentes dans la plateforme. Exemple: System.Windows.Controls.UIElementCollection.

La solution dans UniversalSerializer est de rechercher les autres constructeurs, et de trouver une correspondance entre leurs paramètres et des champs de la classe, même s’ils sont privés.

Par exemple, dans UIElementCollection, nous trouvons ce constructeur:

public UIElementCollection( UIElement visualParent, FrameworkElement logicalParent )

Et ces champs sont disponibles dans la même classe:

  • private readonly UIElement _visualParent;
  • private readonly FrameworkElement _logicalParent;

Les types sont les mêmes et leur nom très proches. Assez pour que UniversalSerializer essaie de créer une instance avec ces valeurs.
Et ça marche !

Sérialiser des classes ICollection non génériques

Pour d’obscures raisons, l’interface ICollection ne fournit pas de méthodes Add et Insert.
En d’autres termes, elle définit une collection en lecture-seule, au contraire de l’interface générique ICollection<T>.
Normalement, nous ne pourrions pas désérialiser une classe qui implémente ICollection mais pas ICollection<T>.

Heureusement, dans le monde réel, la quasi-totalité de ces classes possèdent une méthode Add ou Insert, au moins pour un usage interne.
UniversalSerializer détecte ces méthodes et les utilise pour désérialiser une instance de classe collection.

Réutiliser les mécanismes existant et les gérer à l’aide de ITypeContainers

Dans certains cas, il est plus efficace ou seulement possible d’utiliser les mécanismes de transcodage existant.

Lorsque j’ai essayé de sérialiser des contrôles WPF, j’ai découvert que:

  • [TypeConverter] permet de transcoder un objet vers des types facilement sérialisables (string, byte[ ], etc.).
    Exemple: System.Windows.Input.Cursor
  • [ValueSerializer] permet de transcoder à partir et vers un string.
    Exemple: System.Windows.Media.FontFamily
  • [Serializable] (et ISerializable) permet d’utiliser BinaryFormatter.
    Exemple: System.Uri

Si vous examinez FontFamily, vous vous apercevrez que la transcoder vers un string est bien plus facile que d’essayer de sauvegarder ses propriétés.
Et plus sûr, car modifier une propriété peut entraîner des conséquences imprévisibles sur des classes inconnues ou complexes.

Pour ces attributs, j’ai créé le mécanisme de ITypeContainer. Un conteneur remplace l’instance source par sa valeur transcodée, en général un string ou un byte[ ].
Un conteneur peut s’appliquer à un ensemble de types, par exemple tous les types affublés d’un certain attribut.

Des exemples et des détails suivent.

Les Filtres

Certains types nécessitent une gestion particulière, qui peut être faite par un ensemble de filtres.

Le filtre de validation de type

Ce filtre permet d’éviter à UniversalSerializer de sérialiser des types problématiques.

Par exemple, j’ai rencontré certaines classes qui utilisent System.IntPtr .
Sérialiser ce type mène droit à des problèmes vu que ses instances sont uniquement utilisées en interne dans les classes, même lorsqu’elles sont stockées dans des propriétés publiques.

Les filtres d’ajout de champs privés

Ce filtre ordonne au sérialiseur d’ajouter certains champs privés aux informations de sérialisation.

Par exemple, System.Windows.Controls.Panel a besoin de _uiElementCollection pour remplir sa propriété Children, car Children est en lecture-seule.
Grâce à ce filtre, la solution est simple. Et tous les types héritant de Panel, comme StackPanel, bénéficient du même filtre.

ForcedParametricConstructorTypes

Ce n’est pas un filtre mais une liste de types.
Lorsqu’un type est dans cette liste, UniversalSerializer ignore son constructeur par défaut (sans paramètres) et recherche un constructeur paramétrique.
Exemple: System.Windows.Forms.PropertyManager . Il est ici bien plus simple d’utiliser son constructeur paramétrique que d’écrire un ITypeContainer pour ce type.

Les références

Examinons ce code:

{ // Two references to the same object.
  var data = new TextBox[2];
  data[0] = new TextBox() { Text = "TextBox1" };
  data[1] = data[0]; // Same reference
  byte[] bytesData = UniversalSerializer.Serialize(data);
  var data2 = UniversalSerializer.Deserialize<TextBox[]>(bytesData);
  data2[0].Text = "New text"; // Affects the two references.
  bool sameReference = object.ReferenceEquals(data2[0], data2[1]);
}
  1. UniversalSerializer sérialise une seule instance du TextBox.
  2. Il désérialise ensuite seulement une seule instance de TextBox, et deux références pointant vers elle.
    Les preuves: sameReference est vrai, et Text est identique dans les deux références.

Des filtres et des ITypeContainer personnalisés

Étant donné que l’objectif majeur de UniversalSerializer est de permettre de sérialiser n’importe quel type, il est essentiel que nous partagions notre expérience.

Par conséquent, j’ai essayé de rendre la création de conteneurs et de filtres aussi facile que possible, pour vous permettre de les essayer dans tous les sens et de partager ensuite vos solutions.

Créer un ITypeContainer

Le but est de remplacer une instance problématique par une instance facile à sérialiser. Le plus souvent, le conteneur contiendra des types très simples (string, int, byte[ ], etc..).

Prenons un exemple:

/// <summary>
/// . No default (no-param) constructor.
/// . The only constructor has a parameter with no corresponding field.
/// . The field ATextBox has no public 'set' and is different type from constructor's parameter.
/// </summary>
public class MyStrangeClassNeedsACustomerContainer
{
    /// <summary>
    /// It is built from the constructor's parameter.
    /// Since its 'set' method is not public, it will not be serialized directly.
    /// </summary>
    public TextBox ATextBox { get; private set; }
    public MyStrangeClassNeedsACustomerContainer(int NumberAsTitle)
    {
        this.ATextBox = new TextBox() { Text = NumberAsTitle.ToString() };
    }
}

Comme noté dans son résumé, cette classe pose quelques problèmes au(x) sérialiseur(s).

Pour régler ce problème, nous créons un conteneur:

class ContainerForMyStrangeClass : UniversalSerializerLib.ITypeContainer
{
#region Here you add data to be serialized in place of the class instance
public int AnInteger; // We store the smallest, sufficient and necessary data.
#endregion Here you add data to be serialized in place of the class instance

public UniversalSerializerLib.ITypeContainer CreateNewContainer(object ContainedObject)
{
MyStrangeClassNeedsACustomerContainer sourceInstance = ContainedObject as MyStrangeClassNeedsACustomerContainer;
return new ContainerForMyStrangeClass() { AnInteger = int.Parse(sourceInstance.ATextBox.Text) };
}
public object Deserialize()
{
return new MyStrangeClassNeedsACustomerContainer(this.AnInteger);
}
public bool IsValidType(Type type)
{
return Tools.TypeIs(type, typeof(MyStrangeClassNeedsACustomerContainer));
}
public bool ApplyEvenIfThereIsANoParamConstructor
{
get { return false; }
}
public bool ApplyToStructures
{
get { return false; }
}
}

Un détail: toutes les méthodes se comportent comme des méthodes statiques (mais sans l’être officiellement), à l’exception de Deserialize().

Voyons-les plus en détail:

  • public int AnInteger

    Ça ne fait pas partie de l’interface ITypeContainer.
    C’est ici que nous stockerons les informations qui serviront plus tard à la désérialisation.

  • ITypeContainer CreateNewContainer(object ContainedObject)

    Utilisé durant la sérialisation.
    C’est un genre de constructeur pour une instance de ce conteneur. Le paramètre sera l’instance de la classe source à sérialiser.

  • object Deserialize()

    Utilisé durant la désérialisation.
    L’instance du conteneur produira une nouvelle instance, une copie de l’instance de la classe source, en utilisant notre champ AnInteger.

  • bool IsValidType(Type type)

    Utilisé durant la sérialisation.
    Renvoie vrai si le type hérite, ou est, du type source.
    C’est un filtre.
    Nous pouvons choisir d’accepter les types hérités ou non, d’accepter des types compatibles, etc..

  • bool ApplyEvenIfThereIsANoParamConstructor

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types de classes possédant un constructeur par défaut (sans paramètres).
    Peut être utile avec des conteneurs très généraux.

  • bool ApplyToStructures

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types structure, et pas seulement aux types classe.
    Peut être utile avec des conteneurs très généraux.

Les étapes sont:

  1. Le sérialiseur teste si le type source (MyStrangeClassNeedsACustomerContainer) est géré par ce conteneur.
    Notre classe de conteneur (ContainerForMyStrangeClass) réponds que oui, via IsValidType(), ApplyEvenIfThereIsANoParamConstructor et ApplyToStructures.
  2. Le sérialiseur construit une instance de notre conteneur, via CreateNewContainer().
    CreateNewContainer construits une instance et modifie son champ AnInteger.
  3. Le sérialiseur stocke (sérialise) ce conteneur à la place de l’instance source.
  4. Le sérialiseur retrouve (désérialise) l’instance du conteneur.
  5. Le désérialiseur appelle Deserialize() et obtient une copie de l’instance de la classe source.
    Deserialize() crée cette copie en utilisant son champ AnInteger.

Plus qu’a sérialiser:

/* This example needs a custom ITypeContainer.
Normally, this class can not be serialized (see details in its source).
But thanks to this container, we can serialize the class as a small data (an integer).
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.Containers = new UniversalSerializerLib.ITypeContainer[] {
new ContainerForMyStrangeClass()
};
var data = new MyStrangeClassNeedsACustomerContainer(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<MyStrangeClassNeedsACustomerContainer>(bytesData, Params);
bool ok = data2.ATextBox.Text == "123";

Comme vous pouvez le voir, cette implémentation est très facile.

La classe statique Tools nous offre de l’aide:

  • Type Tools.TypeIs(Type ObjectType, Type SearchedType)

    C’est l’équivalent du ‘is’ en C#, mais pour les Types.
    Par exemple, TypeIs((typeof(List<int>), typeof(List<>)) renvoie true.

  • Type DerivedType(Type ObjectType, Type SearchedType)

    Renvoie le type correspondant à SearchedType qui est hérité par OjectType.
    Par exemple, DerivedType(typeof(MyList), typeof(List<>)) renvoie typeof(List<int>) lorsque MyList est un

    MyList: List<int> { }.

  • FieldInfo FieldInfoFromName(Type t, string name)

    Renvoie le FiedInfo du champ nommé de ce type.
    Nous l’utiliserons dans le prochain chapitre.

Créer un ensemble de filtres

Notez que le mécanisme des filtres est complètement indépendant des ITypeContainers.
On peut les utiliser ensemble, ou séparément.

Prenons un exemple:

public class ThisClassNeedsFilters
{
public ShouldNotBeSerialized Useless;
private int Integer;
public string Value { get { return this.Integer.ToString(); } }
public ThisClassNeedsFilters()
{
}
public ThisClassNeedsFilters(int a)
{
this.Integer = a;
this.Useless = new ShouldNotBeSerialized();
}
}
public class ShouldNotBeSerialized
{
}

Cette classe (ThisClassNeedsFilters) pose quelques problèmes:

  • Elle contient un ShouldNotBeSerialized.
    Nous supposerons que la classe ShouldNotBeSerialized doit être évitée pour certaines raisons, je ne sais pas pourquoi, peut-être qu’elle est empoisonnée !
  • Le champ Integer n’est pas public et donc est ignoré par le(s) sérialiseur(s).
  • Même le nom du paramètre du constructeur est différent d’aucun champ ou propriété.
    De toute façon, le sérialiseur n’a pas besoin du constructeur, puisqu’il y a un constructeur par défaut.

Pour dépasser ces problèmes, nous écrivons un ensemble personnalisé de filtres:

/// <summary>
/// Tells the serializer to add some certain private fields to store the type.
/// </summary>
FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
{
if (Tools.TypeIs(t, typeof(ThisClassNeedsFilters)))
return new FieldInfo[] { Tools.FieldInfoFromName(t, "Integer") };
return null;
}
/// <summary>
/// Returns 'false' if this type should not be serialized at all.
/// That will let the default value created by the constructor of its container class/structure.
/// </summary>
bool MyTypeSerializationValidator(Type t)
{
return ! Tools.TypeIs(t, typeof(ShouldNotBeSerialized));
}

Ils parlent d’eux-même:

  • FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)

    dit au sérialiseur d’ajouter un champ privé (Integer) à chaque instance source de ce type (ThisClassNeedsFilters).

  • bool MyTypeSerializationValidator(Type t)

    empêche le sérialiseur de stocker toute instance de ce type (ShouldNotBeSerialized).
    En conséquence, tout instance de ThisClassNeedsFilters ne fixera pas son champ Useless (il sera null après la désérialisation).

Maintenant on sérialise la classe:

/* This example needs custom filters.
Normally, this class can be serialized but with wrong fields.
Thanks to these filters, we can serialize the class appropriately.
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.FilterSets = new FilterSet[] {
new FilterSet() {
AdditionalPrivateFieldsAdder=MyAdditionalPrivateFieldsAdder,
TypeSerializationValidator=MyTypeSerializationValidator } };
var data = new ThisClassNeedsFilters(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize<ThisClassNeedsFilters>(bytesData, Params);
bool ok = data2.Value == "123" && data2.Useless == null;

L’implémentation est même plus facile qu’avec ITypeContainer.

Le code source de UniversalSerializer

Toutes les sources sont en C#.

La solution a été créée avec Visual Studio 2012, mais elle doit être compatible avec VS 2010.
La compilation nécessite seulement .Net 4, aucune DLL tierce, le code source est complet.

Il y a 3 DLLs:

  • UniversalSerializer1.dll
    La DLL principale générale, avec peu de dépendances.
  • UniversalSerializerWPF1.dll
    Une DLL spécialisée pour WPF, qui gère plus de types de WPF.
  • UniversalSerializerWinForm1.dll
    Une DLL spécialisée pour WinForms, qui gère plus de types de WinForms.

De cette façon, votre application n’aura pas de dépendances inutiles.

Dans l’archive, vous trouverez ces solutions pour VS:

  • \UniversalSerializer Tester\UniversalSerializer Tester.sln
    Une application WPF avec de nombreux exemples.
  • \UniversalSerializerWinForm\UniversalSerializerWinForm.sln
    Une application WinForm avec quelques exemples.
  • \UniversalSerializerReleaseLibs\UniversalSerializerReleaseLibs.sln
    Cette solution génère la version Release des trois DLLs.

Quelques points important

  • Le processus de sérialisation/désérialisation  identifie les types par leur nom complet (grâce à type.AssemblyQualifiedName).
    Ce genre de nom dépend de la version de l’assemblage (assembly), alors faites attention aux problèmes de versions pour la plateforme et pour les DLLs !

  • Le format produit actuel est sujet à changements dans le futur.
    C’est pour cela que le nom de la DLL porte un numéro de version.
    Si vous sauvegardez le tableau d’octets dans un fichier, je vous suggère d’ajouter une information de version à votre arbre d’objets.

Le futur

Je voudrais améliorer certains aspects:

  • Ajouter de nouveaux ITypeContainers et filtres.
    Vous pouvez m’aider. Prévenez-moi lorsque des Types ne sont pas sérialisés correctement. Et merci de partager votre solution, telle que des conteneurs et des filtres.
  • Réduire la taille des données produites.
    J’ai besoin de créer un nouveau format binaire.
  • Accélérer le traitement.

Faciliter le travail des sérialiseurs

Cette expérience m’a enseigné quelles difficultés rencontrent les sérialiseurs avec certains types et comment on peut leur faciliter le travail lorsque nous créons une classe.

  1. Écrivez un constructeur par défaut (sans paramètres) lorsque c’est possible.
    Le sérialiseur reconstruira l’instance à partir des champs et des propriétés.
  2. Si vous ne pouvez pas écrire un constructeur par défaut (sans paramètres), écrivez un constructeur paramétrique dont les paramètres correspondent à des champs privés.
    Chaque paramètre doit être du même type que le champ, et leurs noms doivent être semblables. En fait les noms peuvent différer légèrement: Param -> param ou _param ou encore _Param.
  3. Implémentez un ValueSerializerAttribute ou un TypeConverterAttribute lorsqu’une instance peut être construite à partir de quelque chose d’aussi simple qu’un string.
    En particulier lorsque votre classe contient de nombreux champs et propriétés d’optimisation publics.
    Par exemple, FontFamily peut être construit à partir d’un simple string; peu importe s’il contient bien d’autres informations, elles peuvent toutes être retrouvées à partir de ce simple string.
    Je ne sais pas si beaucoup de sérialiseurs prennent ces attributs en compte, mais au moins UniversalSerializer le fait.
  4. Tous les champs et les propriétés d’optimisation devraient être privés, lorsque c’est possible. Voir plus haut.
  5. Lorsque vous créez une collection d’objets, implémentez IList.
      Parce que ICollection ne permet pas d’ajouter des éléments à une collection.
    Lorsque vous créez une collection générique, implémentez ICollection<>.
      Parce que IEnumerable<> ne permet pas d’ajouter des éléments à une collection.

Remerciements

Je remercie Mehdi Gholam pour son fastBinaryJSON.
J’ai appris des choses intéressantes en lisant son code.
Merci pour avoir partagé cela.

About these ads
 
Poster un commentaire

Publié par le 2013-07-13 dans .Net, Programmation

 

Tags: , , ,

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

 
Suivre

Recevez les nouvelles publications par mail.

%d bloggers like this: