Blog ACENSI
Protobuf et PostSharp

AOP – Génération dynamique d’attributs : simplification de l’utilisation de Protobuf-net grâce à PostSharp

1. Introduction

Je profite que l’AOP (Programmation orientée aspect) ait été introduite dans un précédent article pour vous présenter un cas plus poussé d’utilisation combiné à PostSharp dans un contexte d’optimisation à la fois pour la machine et pour le développeur.
Le but de cet article sera d’utiliser PostSharp pour faciliter l’intégration de Google Protocol Buffer dans une application existante, avec une hiérarchie de classe conséquente.
Cet article fait suite à un retour d’expérience sur un projet d’assez grande taille où l’on a introduit Protobuf-net afin d’optimiser les échanges réseaux.
Ce projet utilisait WCF avec un binding Http, et faisait appel à un Serializer / Deserializer XML. Par conséquent, beaucoup de bande passante était utilisée juste pour encapsuler les messages. Notre ratio payload / message était beaucoup trop faible et ce même combiné à un binding NetTCP.

2. Google protocol buffer

Pour ceux qui ne sont pas familiers avec Google protocol buffer, il s’agit d’un protocole que Google a développé pour les communications entre ses logiciels internes. C’est un format extrêmement efficace, binaire, et élégant.
Il consiste à spécifier un protocol dans un fichier texte, qui est ensuite compilé pour générer des wrappers pour un langage (comme le C++ par exemple). Je vous réfère à la documentation de Google pour en savoir plus mais sachez qu’il s’agit d’un format bien plus optimisé que l’XML pour l’échange de données.
L’implémentation C# de la librairie peut se passer du fichier .proto et utiliser des attributs pour générer un protocole à partir d’une hiérarchie de classes. Cela a pour avantage de nous donner un peu plus de contrôle sur le protocole (nos objets ne changent pas en fonction du protocole, le protocole change en fonction de nos objets) et nous évite une étape de compilation supplémentaire.
Dans les tests préliminaires, l’utilisation de Protobuf-net s’est avérée très satisfaisante ; certains tests accusaient d’un gain de 45% par rapport à notre implémentation de base…
Cependant, repasser sur chaque classe pour ajouter les attributs nécessaires était une tâche fastidieuse où il était facile de se tromper et qui nous rajoutait une tâche de maintenance supplémentaire.
La problématique était donc de générer ces attributs ainsi que leurs tags uniques sur une hiérarchie d’objets sans aucune intervention manuelle. On parle ici d’un peu plus d’une centaine de classes sur 3 classes de base dont tous les autres objets découlent.
Le contenu de ces classes n’est pas important, et nous allons créer une hiérarchie d’objet quelconque pour notre exemple.

3. Introduction de l’AOP

Je vais vous présenter une méthode pour introduire dynamiquement ces attributs sur une hiérarchie de classes présentes dans la même assembly. Il est important de noter que même si notre implémentation ne cherche les types que dans la même assembly, cette technique n’est pas intrinsèquement limitée à une assembly.
Cette méthode se base sur l’utilisation de l’interface IAspectProvider de la librairie PostSharp. L’interface IAspectProvider permet d’augmenter les capacités de PostSharp en créant des aspects qui n’existent pas par défaut.
Les aspects custom sont générés en même temps que le reste du projet, et dans la phase de post compilation, PostSharp parcourra les assembly sélectionnées et instanciera ces aspects custom qui seront exécutés en même temps que les aspects natifs.
L’algorithme est le suivant :

  • Commencer à la racine d’une hiérarchie de types ; T = 1
  • Ajouter un [ProtoContract] à ce type
  • Pour toutes les propriétés publiques, non statique, disposant d’un setter
    • Introduire un attribut [ProtoMember(T++)]
  • Pour tous les types dérivés de cette classe
    • Introduire un attribut [ProtoInclude(T++)]
    • Rappeler l’algorithme en partant du type dérivé

L’algorithme fait essentiellement un parcours en profondeur de la hiérarchie des types (Depth-First).

1. Création de l’aspect

Comme indiqué précédemment, l’aspect utilisé dérive d’Attribute et d’IAspectProvider, afin de pouvoir être placé comme attribut sur les classes racines et pouvoir fournir les aspects nécessaires.
On s’assurera de créer un attribut qui n’est pas automatiquement propagé aux classes dérivées, et qui est unique. On utilise pour cela l’attribut :

[MulticastAttributeUsage(Inheritance = MulticastInheritance.None,
    AllowMultiple = false)]

On n’oubliera pas bien sûr de marquer notre attribut comme Serializable.
IAspectProvider quant à lui ne déclare qu’une seule méthode :

IEnumerable<AspectInstance> ProvideAspects(object targetElement);

Le paramètre reçu en entrée correspond à une instance du System.Type  de l’élément décoré avec l’attribut, et en retour on doit fournir une liste d’instance d’aspects.
En suivant notre algorithme on va récupérer le type de destination, et lui ajouter un attribut ProtoContract :

public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
{
	var targetType = (Type)targetElement;
	int tag = 1;
	var aspects = ProvideAspects(targetType, tag, out tag);
	return aspects;
}
private static List<AspectInstance> ProvideAspects(Type targetType, int startTag,
    out int endTag)
{
	var aspects = new List<AspectInstance>
	{
		GetProtoContract(targetType)
	};
	endTag = startTag;
	return aspects;
}
private static AspectInstance GetProtoContract(Type targetType)
{
	var ctor = new ObjectConstruction(typeof(ProtoContractAttribute)
		.GetConstructor(Type.EmptyTypes));
	Return new AspectInstance(targetType,
            new CustomAttributeIntroductionAspect(ctor));
}

GetProtoContract est une sorte de factory pour attributs, dans notre cas elle fabrique des instances d’un aspect qui ira injecter l’attribut sur le type de destination lors de la compilation. Pour cela, on indique le constructeur que l’on veut, grâce à la classe ObjectConstruction. ProtoContract ne prend aucun paramètre dans son constructeur, on récupère donc le constructeur par défaut.
Testons notre aspect sur une classe simple :

[AutoProtoContract]
public class BaseA
{
	public int Test
	{
		get;
		set;
	}
}

A l’aide de l’outil Telerik JustDecompile nous obtenons :

[AutoProtoContract]
[ProtoContract]
public class BaseA
{
	public int Test
	{
		get;
		set;
	}
	public BaseA()
	{
	}
}

On voit bien ici que PostSharp a introduit lors de la compilation l’attribut [ProtoContract]. Passons maintenant aux membres de la classe.

5. Introduction des ProtoMembers pour la classe de base

Maintenant que la classe de base a son attribut ProtoContract, on va ajouter à chacun de ses membres publics un ProtoMember avec un Tag unique. Pour cela, on va simplement modifier la méthode ProvideAspects :

private static IEnumerable<AspectInstance> ProvideAspects(Type targetType,
    int startTag, out int endTag)
{
	var aspects = new List<AspectInstance>
	{
		GetProtoContract(targetType)
	};
	aspects.AddRange(targetType.GetProperties(
		BindingFlags.Public |
		BindingFlags.DeclaredOnly |
		BindingFlags.Instance)
		.Where(property => property.CanWrite)
		.Select(property => GetProtoMember(property, startTag++)));
	endTag = startTag;
	return aspects;
}
private static AspectInstance GetProtoMember(PropertyInfo targetType, int tag)
{
	var ctor = new ObjectConstruction(
            typeof(ProtoMemberAttribute).GetConstructor(new Type[] { typeof(int) }),
            new object[] { tag });
	return new AspectInstance(
            targetType,
            new CustomAttributeIntroductionAspect(ctor));
}

Comme précédemment nous retrouvons une fonction « factory » GetProtoMember qui va se charger d’instancier l’aspect qui introduira l’attribut [ProtoMember]. Comme ProtoMember prends un « Tag » en paramètre de son constructeur, on spécifie à ObjectConstruction la signature du constructeur ainsi que l’argument à lui passer (le tag).
ProvideAspects(Type, int, out int) est simplement modifiée pour itérer sur toutes les propriétés publiques non-statiques et accessibles en écriture, et leur injecter l’aspect concerné. On notera que l’on incrémente le startTag dans notre requête LinQ et celle-ci est sauvée en fin de méthode dans endTag.
En rechargeant l’assembly dans JustDecompile, l’on voit bien que le membre public Test a bien reçu un attribut ProtoMember(1). Ajoutons quelques membres supplémentaire pour observer que le comportement est bien celui souhaité :

[AutoProtoContract]
public class BaseA
{
	public int Test
	{
		get;
		set;
	}
	private int _untouched;
	public string ReadOnly
	{
		get
		{
			return "Read-Only string";
		}
	}
	public float OtherProperty
	{
		get;
		private set;
	}
}
[AutoProtoContract]
[ProtoContract]
public class BaseA
{
	private int _untouched;
	[ProtoMember(2)]
	public float OtherProperty
	{
		get;
		private set;
	}
	public string ReadOnly
	{
		get
		{
			return "Read-Only string";
		}
	}
	[ProtoMember(1)]
	public int Test
	{
		get;
		set;
	}
}

6. Introduction des ProtoInclude pour les classes dérivées

 
En suivant le même schéma que précédemment on introduit une nouvelle « Factory » qui va instancier des aspects qui produiront des ProtoIncludes, et on va modifier ProvideAspects(object) afin de l’appeler récursivement pour toutes les classes dérivées :

private static AspectInstance GetProtoInclude(Type baseType,
    Type targetType, int tag)
{
	var ctor = new ObjectConstruction(
            typeof(ProtoIncludeAttribute).GetConstructor(
                new Type[] { typeof(int),
                typeof(Type) }),
            new object[] { tag, targetType });
	return new AspectInstance(
            baseType,
            new CustomAttributeIntroductionAspect(ctor));
}
private IEnumerable<Type> GetFirstLevelDerivedType(Type targetType)
{
    var derivedTypes = targetType.Assembly.GetTypes()
                                          .Where(t =>
                                                 t != targetType &&
                                                 t.BaseType == targetType);
    return derivedTypes;
}
public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
{
    var targetType = (Type)targetElement;
    int tag = 1;
    var aspects = ProvideAspects(targetType, tag, out tag);
    foreach (var dt in GetFirstLevelDerivedType(targetType))
    {
        var protoInclude = GetProtoInclude(targetType, dt, tag++);
        aspects.Add(protoInclude);
        aspects.AddRange(ProvideAspects(dt));
    }
    return aspects;
}

Le constructeur de ProtoInclude prend un int (le tag) et un System.Type (le type de base dont on hérite). Le reste ne change pas vraiment. On retrouve une méthode qui va parcourir l’assembly pour trouver toutes les classes dérivant directement de notre type de base (descendants de premier niveau).
Attention : l’attribut ProtoInclude est ajouté sur le type de *base* et non sur le type dérivé.
Si on crée une classe dérivée de notre classe de test :

public class DerivedFromA : BaseA
{
	public string DerivedString
	{
		get;
		set;
	}
}

On voit ainsi que PostSharp a correctement généré les attributs sur notre classe de base ainsi que notre classe dérivée :

[AutoProtoContract]
[ProtoContract]
[ProtoInclude(3, typeof(DerivedFromA))]
public class BaseA
{
	private int _untouched;
	[ProtoMember(2)]
	public float OtherProperty
	{
		get;
		private set;
	}
	public string ReadOnly
	{
		get
		{
			return "Read-Only string";
		}
	}
	[ProtoMember(1)]
	public int Test
	{
		get;
		set;
	}
	public BaseA()
	{
	}
}
[ProtoContract]
public class DerivedFromA : BaseA
{
	[ProtoMember(1)]
	public string DerivedString
	{
		get;
		set;
	}
	public DerivedFromA()
	{
	}
}

En conclusion, nous venons de voir comment simplifier l’utilisation de la librairie protobuf.net grâce à la programmation orientée aspect. Bien évidemment l’AOP ne se limite pas à PostSharp même si c’est une des librairies les plus connues. Une librairie Open Source qui a l’air très prometteuse est Fody.
Bien d’autres aspects de la programmation peuvent bénéficier de l’AOP, et j’espère que cet aperçu vous a donné envie d’en découvrir plus.
 
Références :
https://www.postsharp.net/
https://developers.google.com/protocol-buffers/
https://code.google.com/p/protobuf-net/
http://www.telerik.com/products/decompiler.aspx
https://github.com/Fody/Fody
 

Pourquoi ce blog ?

Pour permettre à nos consultants et experts techniques de partager leurs connaissances et retours d’expérience autour des sujets qui les passionnent. Ce blog, intégralement écrit par eux, a pour vocation d’être un véritable lieu d’échanges et d’apprentissage.

Alors n’hésitez pas à commenter nos articles pour rejoindre la conversation !

Une suggestion ?

Si vous avez des idées pour améliorer ce blog, nous sommes à l’écoute de vos remarques. Vous pouvez nous écrire via le formulaire de contact qui se trouve en bas de page.

Bonne visite !