Entity Framework, template T4 et data annotations

[English version of this post can be found here]

Pour la nouvelle version d’un site personnel, je me suis décidé à implémenter le repository pattern avec Entity Framework 4.0 complété par l’utilisation des templates T4 pour me faciliter la tâche. Passer la tâche de déclarer mes interfaces de repository et mes services, je souhaite maintenant aller plus loin dans mon implémentation car je voudrais être en mesure d’utiliser les data annotations, ces data contracts qui permettent de définir au niveau de votre modèle vos règles de validation.

L’intérêt de l’utilisation des templates T4 est de générer ces attributs automatiquement, afin de pouvoir en tirer partie au sein de la validation automatique de mon projet Web, un portail ASP.Net MVC.

La tâche est un peu compliquée qu’elle n’en a l’air, il faut en effet :

  • analyser le SSDL (Storage Schema Definition Langage) pour lire les metadonnées de chaque entité et de ses champs
  • générer de façon dynamique ces attributs sans action de l’utilisateur
  • être en mesure de prendre en compte les mises à jour du modèle sans perdre les modifications existantes

La première problématique consiste à créer des attributs de façon automatique. Cela signifie que certains attributs comme les Range ou les expressions régulières ne seront pas disponibles car non décrits dans le SSDL.

Il nous faut tout d’abord télécharger le template ADO.NET C# POCO Entity Generator pour générer nos classes POCO afin d’implémenter ce pattern permettant d’être indépendant de la source de données. Cette solution permet en plus de pouvoir faire de l’IoC (Inversion of Control) grâce au pattern, de regénérer, grâce aux T4,les classes modèles autant de fois que nécessaire si jamais le CSDL (Conceptual Schema Definition Langage) venait à changer. Malheureusement, du fait du fonctionnement des templates T4, cela signifie qu’à chaque fois que l’on regénèrerait les classes POCO, les fichiers contenant celles-ci seraient détruits puis recréés, supprimant au passage toutes les modifications manuelles que le développeur aurait pu ajouter pour étendre celles-ci.

Il est donc nécessaire de placer ces attributs dans un fichier différent des classes POCO mais comment alors faire en sorte que ces attributs soient liés aux membres de nos classes POCO? Les classes partielles? Impossible, il faudrait pour cela redéfinir un membre et une erreur de compilation apparaitrait. La solution passe alors par l’utilisation de MetaData, un mécanisme permettant de définir une classe contenant les metadata d’une seconde classe.

En voici un exemple simple :

 [MetadataType(typeof(ArticleMetaData))]
public partial class Article
{
    string Auteur { get; set; }
    string Titre { get; set; }
}

public class ArticleMetaData
{
   [Required]
   [StringLength(255)]
   private string Auteur;
}

Nous avons notre classe POCO sur laquelle nous indiquons via l’attribut MetaDataType la classe qui contiendra les metadonnées. Celle-ci ne contient que les champs sur lesquels nous souhaitons ajouter des annotations et sur chaque membre peut se placer une ou plusieurs annotations.

Pour notre problématique de génération future des classes POCO, il est donc nécessaire de générer ces classes de metadata dans différents fichiers que ceux de la classe POCO et pour être plus précis, il sera même nécessaire de faire en sorte de posséder un template T4 pour générer les metadata et un autre pour générer les POCO, afin de pouvoir recréer les POCO autant que possible. Bien entendu, il n’existe pas de solution parfaite et il faudra créer les metadata futures à la main mais néanmoins, pour le début de projet c’est beaucoup de temps gagné.

Ainsi donc, la première modification à réaliser consiste à modifier le template T4 par défaut (celui qui génère les classes POCO) et d’y ajouter à la ligne 42 (juste avant la déclaration d’une classe), la ligne suivante

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

La seconde étape consiste à créer un second template T4, copie du premier (sans la ligne ajoutée juste avant), dans lequel vous enleverez tout le code servant à générer les propriétés de navigations et les fixup d’associations. Vous rajouterez ensuite dans la méthode WriteHeader la ligne suivante:

 using System.ComponentModel.DataAnnotations;
  

Enfin, ligne 52, juste avant la création des propriétés, ajoutez le bloc de code suivant qui permet d’ajouter les attributs Required et StringLength lorsque nécessaire.

 <# // begin max length attribute
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")]
<#
   }
}
    // begin required attribute
  if (edmProperty.TypeUsage.Facets["Nullable"].Value.ToString() =="False")
  {
#>
    [Required]
<#
    }
#>

et c’est tout. Pour chaque entité, une classe de metadonnées sera créée et chaque fois que la propriété ne doit pas être nulle ou que sa longueur est définie en base alors l’attribut correspondant sera ajouté. Il ne reste alors qu’à ajouter les attributs complémentaires correspondant à notre logique métier (Range, etc).

Ci-joint, le fichier T4 complet

GenMetadata.zip

Comments

  • Anonymous
    December 25, 2010
    Hi! Great article. I found a bug and correct it, check it out: if (edmProperty.TypeUsage.Facets["MaxLength"].Value != null && Int32.TryParse(edmProperty.TypeUsage.Facets["MaxLength"].Value.ToString(), out maxLength)) So, now we can use table properties without any information about MaxLength.

  • Anonymous
    December 25, 2010
    Indeed. I didn't meet this behavior with different databases but I forgot the 1# rule : always test for null value :) thanks Vladimir!