Partagez cette page :

 

Ajouter des attributs à des membres de classes issues d'une génération de code

 

Mitsuru Furuta

Un article rédigé le 3 juin 2010 par :

Mitsuru Furuta, Responsable Relation Technique avec les Développeurs

Retrouvez plus d'informations sur son blog.

 

*La génération de code représente un des leviers de productivité les plus évidents. Cependant, un problème infini intervient :*comment personnaliser le code généré sans être perturbé par les générations de code suivantes ?

Encore une fois, la création initiale est simple, la maintenance plus complexe…
Dans de nombreux cas, les solutions de personnalisation post-génération de code sont complexes et peu fiables. On préfèrera souvent intervenir en amont de la génération de code, au niveau des templates par exemple. Cela sous-entend qu’une partie de la logique doit être accessible au niveau du moteur de génération et on se retrouve parfois à coder du spécifique dans les templates, voire à tout y définir et tendre au final vers une architecture orientée modèle.

Quoi qu’il arrive l’exercice devient souvent complexe. Nous n’avons cessé depuis le .Net Framework 2.0 de faire évoluer nos langages pour faciliter cette tâche, principalement avec l’ajout des classes partielles puis des méthodes partielles.
Ces principes sont très simples et ont pourtant révolutionné les scénarii de génération de code. A se demander pourquoi personne ne l’avait fait avant… :-)

 

Rappel et limites de la "partialité"

L’article suivant, vous rappelle les bases des classes et méthodes partielles : https://msdn.microsoft.com/fr-fr/vcsharp/bb968892.aspx

Prenons un exemple simple : comment ajouter un attribut à une classe partielle provenant d’un générateur de code ?

//dans MyClass.generated.cs
[Serializable]
public partial class MyClass
{
}

//code manuel personnalisé
[Browsable(true)]
public partial class MyClass
{
}

Comme le montre le code-source ci-dessus, il suffit d’écrire sa propre version de la classe, en l’étendant par partialité, et d’y j’ajouter l’attribut voulu. A la compilation, les deux classes partielles n’en feront évidemment qu’une et les attributs de chacune des classes seront fusionnés.

Cependant, tout n’est pas résolu.

En effet, si la génération de code définit une propriété et que l’on veuille ajouter un attribut à cette propriété, aucun mécanisme de partialité n’offre de solution.

Il faudrait pouvoir symboliser la propriété existante dans notre classe partielle personnalisée afin d’ajouter les attributs voulus.

Un prototype avait été mis en place lors de la conception de C# 4.0, mais la fonctionnalité fut écartée, faute de temps…entre autre.

 

Alternative

Il y a bien évidemment dans les outils Microsoft de nombreux scenarii de génération de code qui rencontrent également ce problème de ne pas pouvoir ajouter d’attributs à des propriétés existantes.

Dans WCF Ria Services par exemple, des entités métier sont générées depuis un modèle. Il est pourtant possible d’ajouter des attributs post-génération sur les propriétés via une classe partielle (règles de validation par exemple).

Découvrons le système utilisé.

Dans ce premier exemple, nous montrons un code classique de découverte d’attribut sur la propriété « Id » de la classe MyClass.

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetDisplay(typeof(MyClass), "Id"));    
        }
        static string GetDisplay(Type type, string propertyName)
        {
            var prop = type.GetProperty(propertyName);
            var attrs = prop.GetCustomAttributes(typeof(DisplayAttribute),false).OfType<DisplayAttribute>();
            if (attrs.Count() > 0)
            {
                var attr = attrs.First();
                return attr.Name;
            }
            else
                return null;
        }
    }
    public class MyClass
    {
        [Display(Name = "Identifiant")]
        public int Id { get; set; }
    }

"Identifiant” a bien été trouvé :

Identifiant

 

Première étape, notre intérêt étant le binding d’éléments, nous allons éviter d’utiliser les APIs de Reflection pour préférer les TypeDescriptors. Si la Reflection renvoie les metas information de types, les TypeDescriptors renvoient des métas-informations dédiées au binding associées au même type. Contrairement à la Reflection, les TypeDescriptors sont des structures dynamiques qui peuvent donc varier.

Pour que le contrat soit respecté, nous allons donc réécrire notre logique en utilisant les TypeDescriptors.

class Program
{
    static void Main(string[] args)
    {
Console.WriteLine(
GetDisplay(
TypeDescriptor.GetProperties(typeof(MyClass)),
"Id"));
    }
    static string GetDisplay(
PropertyDescriptorCollection propertyDescriptors,
string propertyName)
    {
        var desc = propertyDescriptors.Find(propertyName, false);
        var attr =
desc.Attributes.OfType<DisplayAttribute>().FirstOrDefault();
        if (attr != null)
            return attr.Name;
        else
            return null;
    }
}
public class MyClass
{
    [Display(Name = "Identifiant")]
    public int Id { get; set; }
}

Le code est très similaire et renvoie le même résultat car par défaut, faute de surcharge, les TypeDescriptors renvoient les mêmes informations que la Reflection.

Désormais, reprenons la problématique de la génération de code. Dans notre exemple, imaginons donc qu’il ne nous est pas possible d’ajouter l’attribut « Display » sur la propriété « ID » de la classe « MyClass » car celle-ci serait issue d’une génération de code.

Imaginons un système externe d’enrichissement d’information de type.

//dans MyClass.generated.cs
[MetadataTypeAttribute(typeof(MyClassMetadata))]
public partial class MyClass
{
}

//code manuel personnalisé
public class MyClassMetadata
{
    [Display(Name="Identifiant")]
    public int Id { get; set; }
}

Dans le code ci-dessus, nous ne touchons plus à la classe « MyClass ». L’attribut « MetadataTypeAttribute » enrichit la classe entière et peut aisément être ajouté à la logique de génération (dans le template par exemple).

Cet attribut cible un type « factice » dont l’usage sera uniquement de symboliser la classe « MyClass » mais dans une version éditable hors du scénario de génération.
L’idée est d’utiliser ce type et d’y ajouter des propriétés qui existent (même nom) dans la classe d’origine « MyClass ». Il est évidemment désormais possible d’y ajouter autant d’attributs que voulus.

La classe « MyClassMetadata » ne sera jamais instanciée et sert uniquement de support de décoration pour la classe « MyClass ».

Ces mécanismes sont officiellement supportés par le .Net Framework 4.0 (voire le namespace System.ComponentModel.DataAnnotations). Il est bien sûr possible de coder soit-même la logique de récupération d’attributs dans la classe de meta-data, mais le framework fournit ce provider sous forme de provider de *TypeDescriptor *! Il suffit donc de substituer le TypeDescriptor naturel du framework par le « AssociatedMetadataTypeTypeDescriptionProvider » (merci le nom ! :-))

Dans notre exemple il suffit donc de remplacer :

Console.WriteLine(GetDisplay(TypeDescriptor.GetProperties(typeof(MyClass)), "Id"));
Par :
var provider = 
new AssociatedMetadataTypeTypeDescriptionProvider(typeof(MyClass));
var td = provider.GetTypeDescriptor(typeof(MyClass));
Console.WriteLine(GetDisplay(td.GetProperties(), "Id"));

L’indirection est relativement simple à mettre en œuvre et profite de l’infrastructure existante des TypeDescriptors.

Comme le montre la capture d’écran suivante, les MetadataPropertyDescriptorWrapper se substituent aux PropertyDescriptor de base afin d’exposer les méta-datas détournées.

metadata

 

Le cas WCF Ria Services

Le framework WCF Ria Services s’appuie sur un modèle de source de données existante (Entity Framework par exemple). Le modèle génère ses propres entités.

entités

WCF Ria Services propose d’enrichir ce modèle avec par exemple des règles de validation par champ. Ce système simple basé sur attribut rencontre exactement le problème que nous exposons dans cet article. Le fichier « NorthwindModel.Designer.cs » ne peut pas être édité car généré par Entity Framework. Un problème supplémentaire survient cependant. Le template d’Entity Framework n’a pas connaissance de WCF Ria Services et ne génère donc pas l’attribut « MetadataTypeAttribute » et encore moins le type cible bien évidemment. Heureusement, l’entité générée est « partielle ». Nous pourrons donc définir une indirection en deux temps.

A noter : dans le doute, si vous générez des classes quel que soit votre système, définissez les comme partielles !

Imaginons l’entité « Customer » du modèle. La modèle génère la classe Customer que l’on ne peut pas modifier.

Dans un fichier de personnalisation séparé, nous pouvons définir la même classe Customer grâce à la partialité. Sur cette classe, nous pouvons enfin ajouter l’attribut « MetadataTypeAttribute » et cibler une classe de méta-data.

1

L’assistant de création de DomainService de WCF Ria Services permet d’automatiser exactement ce fonctionnement en cochant « Generate associated classes for metadata ».

2

 

Conclusion

WCF Ria Services masque cette complexité mais si vous avez vos propres scénarii de génération de code et que vous voulez permettre aux utilisateurs de pouvoir personnaliser leurs classes générées, il est intéressant de voir que le .Net Framework offre cette possibilité.

On peut également noter que les bétas de WCF RIA Services fournissent les attributs et provider qui sont actuellement dans le namespace DataAnnotations et et permettent ainsi d’avoir un système équivalent qui fonctionne sous .Net 3.5/VS2008 …

Télécharger un exemple de code