1. Introduction

Les attributs sont des métadonnées liées à une classe ou à une propriété. Elles permettent de spécifier un comportement. On peut distinguer un type spécifique d'attributs : les data annotations. Ceux-ci permettent de mettre en place de la validation, en imposant par exemple un format, une taille minimale…

Un attribut s'écrit entre crochets, au-dessus de la classe ou de la propriété liée. Voici un exemple avec une data annotation :

 
Sélectionnez

public class AlbumMetaData
{
    [Required]
    public int IdAlbum;
}

Important : pour avoir accès aux Data Annotations, il faut ajouter la référence suivante : System.ComponentModel.DataAnnotations et importer le namespace.

Les Data Annotations sont très utiles pour de la validation. Par exemple, dans une application MVC, on pourra se servir de ces attributs pour faire de la validation automatique.

2. Classe de métadonnées (Buddy Classes)

Le principe des classes de métadonnées consiste à déporter la déclaration des attributs d'une classe au sein d'une autre classe. Prenons l'exemple d'une classe Student :

 
Sélectionnez

public class Student
{
    [Required]
    public int IdStudent{ get; set; }

    [Email]
    public string Email { get; set; }
}

Nous allons créer une classe StudentMetaData qui contiendra nos attributs :

 
Sélectionnez

public class StudentMetaData
{
    [Required]
    public int IdStudent;

    [Email]
    public string Email;
}

Maintenant, nous allons enlever les attributs de la classe Student, et lui indiquer d'aller les chercher dans la classe StudenMetaData :

 
Sélectionnez

[MetadataType(typeof(StudentMetaData))]
public class Student
{
    public int IdStudent{ get; set; }

    public string Email { get; set; }
}

3. Annotations / EF4 / templates T4

3.1. Problématique

Dans notre application type (voir Templates T4 et Entity Framework par Kevin PerriatTemplates T4 et Entity Framework par Kevin Perriat), nos objets POCO sont générés automatiquement grâce à un template T4. Le problème est qu'à chaque fois que le template T4 est exécuté, les fichiers précédemment créés sont écrasés.

Dans le cadre d'une application nécessitant des attributs, nous allons donc devoir insérer dans nos POCO des attributs. Or, ceux-ci seront perdus dès lors que nous exécuterons le template T4. Nous allons donc devoir utiliser les classes de métadonnées.

3.2. Solutions

Dans cet article, nous allons étudier deux solutions possibles. Il est possible que d'autres méthodes existent, néanmoins, les deux présentées sont, à mon sens, les plus pertinentes.

3.2.1. Utilisation d'un template T4 spécifique

Cette solution va s'appuyer sur l'utilisation d'un template T4 spécifique à la génération des classes de métadonnées. L'avantage de cette solution est que nous allons pouvoir générer automatiquement des attributs du type Required et MaxLength. Sur une base de données volumineuse, cette possibilité est non négligeable.

Afin d'éviter de partir de zéro, nous allons dupliquer le template T4 qui nous permet de générer nos POCO. Vous pouvez télécharger le template ici. Il nous servira de base pour la génération des classes de métadonnées.

La première étape va consister à changer le nom des classes générées. Nous allons suivre une convention simple : "NomEntité"MetaData. Pour cela, modifions la ligne 37 de notre template T4 pour changer le nom des fichiers générés :

 
Sélectionnez

fileManager.StartNewFile(entity.Name + "MetaData.cs");

Enfin, ligne 43, changeons le nom des classes générées :

 
Sélectionnez

<#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>class <#=code.Escape(entity)#>MetaData<#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>

Comme nous l'avons vu lors de l'introduction, afin d'utiliser les data annotations, nous avons besoin de référencer System.ComponentModel.DataAnnotations.. Nous allons donc modifier la méthode WriteHeader pour y intégrer le using correspondant :

 
Sélectionnez

using System.ComponentModel.DataAnnotations;

Grâce à ce code, toutes nos classes de métadonnées feront un using sur System.ComponentModel.DataAnnotations :

 
Sélectionnez

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;

namespace MetaDonnees.POCO
{
    
    
    public class ClassMetaData
    {
    }
}

Maintenant cette étape finie, continuons la modification de notre template T4. Nous allons supprimer du template tout le code relatif à la génération des propriétés de navigation et des fixup d'association (collections d'objets liés). Ce code se situe entre les régions suivantes :

 
Sélectionnez

region.Begin("Navigation Properties");
//[...]
region.End();

region.Begin("Association Fixup");
//[...]
region.End();

Si vous exécutez à nouveau votre template T4, vous pouvez voir que toutes les propriétés de navigation ont disparu.

Afin de nous faciliter la tâche, nous allons déléguer la mise en place des attributs Required et StringLength à notre template T4. Pour cela, ajoutez le code suivant au niveau de la ligne 52 du template (juste avant l'écriture de la propriété) :

 
Sélectionnez

<# // attribut maxlength
if (code.Escape(edmProperty.TypeUsage) == "string")
{
  int maxLength = 0;
  if (edmProperty.TypeUsage.Facets["MaxLength"].Value != null && Int32.TryParse(edmProperty.TypeUsage.Facets["MaxLength"].Value.ToString(), out maxLength))
  {
#>	
    [StringLength(<#=code.CreateLiteral(maxLength)#>, ErrorMessage="Ce champ ne peut depasser <#=code.CreateLiteral(maxLength)#> caracteres")]
<#
   }
}
	// attribut required
  if ( edmProperty.TypeUsage.Facets["Nullable"].Value.ToString() =="False")
  {
#>
    [Required]
<#
    }
// end attribut
#>

Maintenant, si nous exécutons notre template T4, nous obtenons nos classes de métadonnées :

 
Sélectionnez

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;

namespace MetaDonnees.POCO
{
    
    
    public class ClassMetaData
    {
        [Required]
        public virtual int IdClass
        {
            get;
            set;
        }
    	
        [StringLength(100, ErrorMessage="Ce champ ne peut depasser 100 caracteres")]
        [Required]
        public virtual string Name
        {
            get;
            set;
        }
    }
}

À cette étape, vous seriez en droit de vous demander si nous en avons fini avec cette solution. Il n'en est rien. En effet, si vous rajoutez à la main un attribut, nous allons nous heurter à un problème induit par l'utilisation des templates T4 : notre fichier sera régénéré, et nos modifications perdues. Nous allons donc de nouveau modifier notre template T4 pour résoudre ce problème.

L'idée que nous allons mettre en place est la suivante : si notre classe de métadonnées a déjà été générée, on ne la régénère pas. Ainsi, nous gardons un contrôle sur les classes générées. Néanmoins, notre template T4 utilisant un filemanager spécifique, nous ne pouvons pas lui indiquer de ne pas supprimer tel fichier. Nous allons donc devoir contourner le problème.

La solution proposée ici n'est pas forcément la meilleure, mais une des plus rapides à mettre en place. Elle consiste à copier les classes déjà générées dans un dossier temporaire, laisser le template T4 s'exécuter, et déplacer les fichiers précédemment copiés.

Pour commencer, nous allons ajouter cette méthode à notre template T4 :

 
Sélectionnez

void Copy(DirectoryInfo DirectoryFrom, DirectoryInfo DirectoryTo, bool Delete)
{
	foreach(FileInfo file in DirectoryFrom.GetFiles().Where(f => f.Name.Contains("MetaData")))
		file.CopyTo(DirectoryTo.FullName + @"\" + file.Name, true);	

	if(Delete)
		Directory.Delete(DirectoryFrom.FullName, true);
}

Le fonctionnement de cette méthode est simple : elle va copier tous les fichiers contenant "MetaData" d'un répertoire dans un autre, et au cas par cas, supprimer le dossier de départ.

Maintenant, ajoutons les appels à notre méthode. Copiez ce code juste avant le commencement du foreach sur les entités de l'edmx :

 
Sélectionnez

//Sauvegarde des fichiers précédemment créés
DirectoryInfo diTemplate = new FileInfo(Host.TemplateFile).Directory;
DirectoryInfo diTemp = new DirectoryInfo(diTemplate.FullName + @"\Temp");
diTemp.Create();

Copy(diTemplate, diTemp, false);

Enfin, copiez ce code juste après le fileManager.Process() :

 
Sélectionnez

//restauration des fichiers sauvegardés
Copy(diTemp, diTemplate, true);

Grâce à ce nouveau template T4, nous pouvons donc générer nos classes de métadonnées, tout en conservant une possibilité de les personnaliser. Il ne nous reste plus qu'à modifier le template T4 générant nos POCO pour indiquer à chaque POCO la classe de métadonnées correspondante.

Tout comme pour notre nouveau template T4, nous allons devoir ajouter la référence à System.ComponentModel.DataAnnotations. Rajoutez donc cette ligne au niveau des usings :

 
Sélectionnez

using System.ComponentModel.DataAnnotations;

Pour finir, nous allons ajouter cette ligne avant la création de la classe pour les entités. Elle permet de mettre en place notre attribut MetadataType :

 
Sélectionnez

[MetadataType(typeof(<#=code.Escape(entity)#>MetaData))]

Cette méthode, un peu longue à mettre en place je vous l'accorde, a de nombreux avantages :

  • génération automatique des data annotations de base. Imaginez une grosse base de données avec plus de 200 entités, ça peut être agréable ;
  • possibilité de personnalisation des classes de métadonnées.

3.2.2. Edition manuelle des classes de métadonnées

Une autre solution, plus simple à mettre en place existe. On peut partir du principe que les classes de métadonnées seront générées manuellement. Le template T4 servira uniquement à indiquer à nos POCO s'ils doivent utiliser une classe de métadonnées.

La logique est simple : lors de la génération de nos POCO, on vérifie si une classe de métadonnées existe pour la classe en cours. Si oui, on rajoute l'attribut MetadataType.

Pour commencer, ajoutez ces lignes avant le premier foreach du template T4 :

 
Sélectionnez

//configuration du chemin pour les classes de métadonnées
String dossierMetadonnees = "/";
DirectoryInfo directoryMetadonnees = new DirectoryInfo(new FileInfo(Host.TemplateFile).Directory.FullName + dossierMetadonnees);

Enfin, juste avant la déclaration de la classe, nous allons ajouter ce code :

 
Sélectionnez

if(new FileInfo(directoryMetadonnees + entity.Name + ".cs").Exists)
	{
#>
[MetadataType(typeof(<#=code.Escape(entity)#>MetaData))]
<#
	}
		#>

Le fonctionnement du code est simple : il va vérifier si la classe de métadonnées pour l'entité existe, et si oui, il ajoute l'attribut MetadataType. Cette solution permet de mettre en place rapidement une gestion des classes de métadonnées. Celles-ci n'étant pas générées par un template T4, nous ne risquons pas de voir supprimer nos modifications.
Néanmoins, dans le cadre d'une base de données volumineuse, la charge de travail pour créer les métadonnées risque d'être importante.

4. Conclusion

Nous avons donc vu que l'utilisation des attributs avec les templates T4 nécessite une vraie réflexion. Les deux solutions proposées dans cet article ont le mérite de répondre à deux problématiques différentes.

Les deux solutions ne sont pas non plus incompatibles. Les combiner permet ainsi d'éviter des erreurs potentielles. En effet, dans le cadre de la solution 1, si on exécute le template de génération des POCO sans exécuter le template de génération des classes de métadonnées, des erreurs peuvent se produire.

Notez que vous pouvez aussi générer votre propre template T4, qui utilisera System.IO pour générer des fichiers, et non le templateFileManager.

Vous pourrez trouver les sources de ce tutoriel ici : sources Templates T4 et métadonnées, par Kevin Perriat.

5. Remerciements

Je tiens à remercier Nathanael Marchand et tomlev pour leur aide à la rédaction de cet article. Je remercie aussi ClaudeLELOUP pour sa relecture.