I. Introduction

L'arrivée du framework .NET 4.0 a vu naître la version 4 d'Entity Framework. Cet ORM permet de s'interfacer avec une base de données SQL Server via trois solutions en fonction des besoins :

-          Database First où l'on part d'une base existante, ce qui permet de créer le modèle par simple drag and drop ;

-          Model First où comme son nom l'indique, un modèle est créé dans le designer, ce dernier assurant la génération de la base de données une fois une connexion spécifiée ;

-          et enfin,  Code First, dernier né de la version 4.1.  Une approche centrée autour du code que je vous propose d'étudier dans ce tutoriel.

II. But du tutoriel

Au travers des quelques exercices qui vont suivre, nous allons procéder à la création d'une base de données sur SQL Server à partir de code écrit dans Visual Studio, en utilisant l'approche Code First mise à disposition par Entity Framework.

Commençant par une conception objet très simple que nous étofferons par la suite, nous poursuivrons par une conception plus élaborée, une observation de la mécanique de génération de la base de données, avant de passer à des techniques un peu plus avancées, mais guère plus, promis.

L'ensemble de la solution est basée sur la version 4.2 d'EF (Actuellement en version 4.3).

III. Pré-requis

Ce tutoriel se base sur :

·         Visual Studio 2010 ;

·         Framework .NET 4.0 ;

·         Entity Framework 4.2 ;

·         SQL Server 2008 Express.

Dans cet article l'abréviation CF sera utilisée pour Code First.

IV. Architecture utilisée

Pour les besoins de ce cours, nous allons utiliser l'architecture suivante.

Image non disponible

IHM : Interface Homme Machine.

DAL : Data Access Layer (couche d'accès aux données).

DTO : Data Transfer Object.

Généralement, une BLL (Business Logic Layer) est intercalée entre l'IHM et la DAL, c'est d'ailleurs conseillé afin d'y développer toute la logique métier. Pour plus de détails à ce sujet, je vous recommande l'excellent tutoriel d'immobilis sur l'architecture multicouche : http://immobilis.developpez.com/articles/dotnet/architecture-multicouche-asp-net/.

Nous n'aurons pas de logique métier à implémenter ici, donc afin de simplifier le tutoriel, nous nous contenterons d'une DAL faisant l'interface avec notre BDD, puis d'une IHM. Gardez à l'esprit que pour un "vrai" projet, l'utilisation de la BLL est recommandée.

Concrètement, dans Visual studio, ça donne ceci :

Image non disponible

Comme vu sur le schéma, DTO est un projet transverse, donc référencez-le dès à présent dans tous les autres projets de la solution.

Maintenant que nous avons notre base de travail, mettons-nous en route.

V. Mise en situation !

- Mendez, mécanicien : « Salut mon ami ! Dis-moi, j'aimerais un programme pour gérer mon garage, connaître mon stock, ce genre de fonctionnalités, pour faire du grand Mendez quoi ! »

Voici notre besoin de départ (J), on va se débrouiller avec ça, on dira que cela sera suffisant dans un premier temps.

Pour faire simple, nous allons commencer en étant un peu feignant. Disons que ce brave mécano gérera dans son garage uniquement des Harleys.

On arrive donc à cette classe :

 
Sélectionnez
namespace CodeFirst.DTO
{
public class EntityHarley
    {
        public string Couleur { get; set; }
        public int Reservoir { get; set; }
        public int NbChevaux { get; set; }        
        public string Modele { get; set; }
    }
}

Effectivement, on a été feignant, la conception objet n'est pas bien maligne pour le coup, mais c'est pour la bonne cause !

Aucun doute sur la question, les classes qui vont enrichir notre projet DTO sont donc bien des POCO (Plain Old CLR Object, un POCO = un DTO).

Ces classes ont pour caractéristiques de ne pas hériter d'une classe en particulier ou de devoir implémenter une interface spécifique. Il n'y a aucune adhérence à un quelconque produit ou DLL permettant de s'interfacer avec un ORM.

De simples propriétés les composent afin de les décrire, il n'y pas de méthode.

Et ça tombe bien, après tout, nous sommes en train de concevoir de futures tables pour notre base de données.

V-A. Un databaseContext pour les unir tous...

Maintenant que nous savons avec quelle entité nous allons travailler, il nous faut encore de quoi la manipuler, une espèce de maître des entités qui les gouvernerait toutes et nous permettrait d'interagir avec elles.

Passons dans la DAL et utilisons l'excellent NuGet pour obtenir la version 4.2  d'EF. (Voir tuto sur NuGet ici : http://rdonfack.developpez.com/tutoriels/dotnet/presentation-gestionnaire-packages-net-nuget/).

Attention à bien préciser la version avec -version, sinon ce sera la dernière version qui sera téléchargée. De même mentionnez CodeFirst.DAL comme nom de projet, sinon la DLL sera installée dans le projet principal, la console.

NuGet va télécharger et référencer dans la DAL la DLL version 4.2 d'EF.

Créons une classe GarageContext, qui va hériter de DbContext. Le DbContext, dans les autres modèles d'EF, est plutôt connu sous le nom ObjectContext.

Pour faire simple, le DbContext est un ObjectContext simplifié. Il va notamment nous permettre de manipuler nos tables, donc nos DTO, avec des opérations type CRUD (Create Read Update Delete).

 
Sélectionnez
namespace CodeFirst.DAL
{
    class GarageContext : DbContext
    {
        public DbSet<EntityHarley> Harleys { get; set; }
    }
}

Avec ceci, nous informons notre contexte de données des entités qui devront être considérées comme des tables et avec lesquelles nous allons donc réaliser nos transactions et nos diverses manipulations, dans le cas présent, une collection d'entités Harley.

On note au passage l'utilisation de DbSet pour marquer l'existence d'une table.

Et maintenant, dernier point, la configuration de la chaîne de connexion à la base de données.

 
Sélectionnez
<?xmlversion="1.0"encoding="utf-8"?>
<configuration>
<configSections>
</configSections>
<connectionStrings>
<addname="GarageContext"
                    connectionString="DataSource=.\SQLEXPRESS;IntegratedSecurity=True"
        providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>

Détail qui a son importance, le nom de la chaîne de connexion est le même que le nom de notre classe de contexte.

Techniquement, on a (presque ;) fini l'implémentation de Code First dans sa version la plus sommaire. Encore un peu de code dans la DAL et nous allons pouvoir commencer à bidouiller.

Prenons la DAL et ajoutons cette classe.

 
Sélectionnez
namespace CodeFirst.DAL
{
    public class HarleyProvider
    {
        public HarleyProvider()
        {
 
        }
 
        public List<EntityHarley> GetAllHarleys()
        {
            using(GarageContext context = new GarageContext())
            {
                return context.Harleys.ToList();
            }
        }
    }
}

Notre solution ressemble maintenant à ça :

Image non disponible

Plus qu'à faire un bout de code dans la console :

 
Sélectionnez
static void Main(string[] args)
{            
          AfficheAllVehicule();
                    Console.WriteLine("Fin");
                    Console.ReadLine();
}
 
        static void AfficheAllVehicule()
        {
            HarleyProvider harleyProvider = new HarleyProvider();
  
            foreach(EntityHarley harley in harleyProvider.getAll())
            {
                Console.WriteLine(harley);
            }
        }

VI. Premiers essais

Pour l'instant, on va se contenter de faire un appel et d'observer ce qu'il se passe. Voici l'état de notre serveur SQL.

Image non disponible

On exécute… Paf !

Image non disponible

Notre entité ne possède pas de clé, donc erreur pour la génération du modèle. Eh oui, souvenez-vous, que sommes-nous en train de faire ? Nous avons conçu des classes qui vont être de futures tables pour notre base, il leur faut donc des clés primaires.

Rajoutons les propriétés qui vont bien dans chacune des entités.

 
Sélectionnez
public int HarleyPK { get; set; }

2e essai…

Et même erreur !

En fait, l'explication est simple. Comment la génération du modèle est capable de parser les classes pour trouver la clé primaire ? La réponse est dans la question, c'est bien une convention de nommage qui est utilisée et, pour réussir, la clé doit avoir le nom de la classe + ID ou tout simplement ID.

Retentons avec ceci :

 
Sélectionnez
public int ID{get;set;}

3e essai…

Image non disponible

A priori l'appel, s'est passé correctement. Jetons un coup d'œil dans SQL Server.

Succès : une base de données CodeFirst.DAL.GarageContext a bien été créée et si je regarde mes tables, elles correspondent bien à l'entité que j'ai créée dans mes DTO.

Image non disponible

Au passage, on remarque la présence d'une table créée automatiquement par CodeFirst : EdmMetaData. On verra son utilité par la suite.

Maintenant étoffons un peu ceci avec un peu de CRUD.

Tout d'abord dans le provider, créons ces méthodes (voir code en annexes), mettons à jour la BLL et enfin le Program.cs.

 
Sélectionnez
class Program
    {
        static void Main(string[] args)
        {
            AfficheAllVehicule();
 
            AjouterHarleys();
            AfficheAllVehicule();
 
            MAJ();
            AfficheAllVehicule();
 
            Console.WriteLine("Fin");
            Console.ReadLine();
        }
 
        static void AfficheAllVehicule()
        {
            HarleyProvider harleyProvider = new HarleyProvider();
 
            foreach (EntityHarley harley in harleyProvider.getAll())
            {
                Console.WriteLine(harley);
            }
        }
 
        static void AjouterHarleys()
        {            
            Console.WriteLine("===== Création d'harley dans le garage =====");
            HarleyProvider provider = new HarleyProvider();
   
            provider.Create(new EntityHarley { Couleur = "Noire",
     Modele = "883 Iron", Reservoir = 12 });
            provider.Create(new EntityHarley { Couleur = "Rouge",
     Modele = "1200 Nighster", Reservoir = 12 });
            provider.Create(new EntityHarley { Couleur = "Grise",
     Modele = "1200 Forty height", Reservoir = 12 });
            provider.Create(new EntityHarley { Couleur = "Noire",
     Modele = "Fat boy", Reservoir = 25 });
            Console.WriteLine("======================");
        }
 
        static void MAJ()
        {
            Console.WriteLine("===== Mise à jour de la couleur =====");
            HarleyProvider provider = new HarleyProvider();
 
            foreach (EntityHarley harley in provider.getAll())
            {
                harley.Couleur = "Violet";
                provider.Update(harley);
            }
            Console.WriteLine("======================");
        }
    }

Exécutons :

Image non disponible

Ce code n'a rien d'exceptionnel, mais il a le mérite de valider certaines choses :

  • les opérations de base type CRUD fonctionnent ;
  • par rapport à un mapping « classique », le fait que la base soit créée à partir des classes est transparent pour nous, il n'y rien à rajouter au niveau de la DAL pour opérer.

À ce stade, nous venons d'implémenter CF dans sa version la plus basique.

Maintenant que ces quelques bases sont validées, étoffons un peu tout ça, à commencer par le modèle de données.

Étant donné que ce sont des classes qui vont gérer la création de la base de données, on est en droit de se demander comment se comporte Code First avec l'héritage ? Et puisqu'on parle de base de données, on va forcément en venir aux relations, comment les gérer en CF ?

Je vous propose de répondre à ces questions avec la partie “Techniques avancées” !

VII. Techniques avancées

VII-A. Renommer la base

Avant de procéder à une évolution de la conception objet, nous allons modifier le nom de la base de données.

Pour ce faire, il suffit de modifier le constructeur du GarageContext :

 
Sélectionnez
public GarageContext() : base("GarageMendez")
{            
}

On ré-exécute… Et on observe dans SQL Management Studio.

Image non disponible

Voilà notre base renommée (enfin, une autre base est créée avec le bon nom).

Passons maintenant à l'évolution du modèle.

VII-B. Évolution de la conception objet

Reprenons donc notre conception objet, à la base très simple, pour la faire évoluer.

Au niveau des DTO, on étoffe pour obtenir ceci :

Image non disponible

On obtient donc la solution suivante :

Image non disponible

Le Main est quant à lui réduit à ceci :

 
Sélectionnez
 class Program
    {
        static GarageManager garage = new GarageManager();       
 
        static void Main(string[] args)
        {            
            AfficheAllVehicule();
            Console.WriteLine("Fin");
            Console.ReadLine();
        }
 
        static void AfficheAllVehicule()
        {
      HarleyProvider harleyProvider = new HarleyProvider();
 
            foreach (EntityHarley harley in harleyProvider.getAll())
            {
                Console.WriteLine(harley);
            }
        }
    }

On exécute, et là, on peut observer le message d'erreur suivant :

Image non disponible

EF a détecté que notre modèle de données, enfin, nos classes, ont été modifiées depuis la dernière création de la base et nous fait part de ce déphasage.

Avant d'aller plus loin, certains se sont peut-être posé la question. Mais comment EF a-t-il su que les classes ont été modifiées ?

Souvenez-vous de cette table, EdmMetadata, créée automatiquement.

Cette table permet de savoir si il y a eu un changement dans notre design, on peut d'ailleurs aller la consulter, on y trouve un enregistrement contenant un hachage, « ModelHash », on peut donc en déduire qu'un hash est obtenu à partir des DTO une première fois et que pour les accès suivant un nouveau hash est calculé, par comparaison EF en déduit si les classes ont été modifiées.

Pour résoudre ce problème, EF nous propose deux solutions, soit manuellement on delete et on met à jour la base, soit on utilise un IDatabaseInitializer…

Étant donné que nous sommes en phase de développement, nous allons prendre la 2e solution, bien plus souple et permettant plus de choses.

VII-C. Stratégie d'initialisation

Nous allons créer une nouvelle classe, GarageContextInitializer.

 
Sélectionnez
class GarageContextInitializer : DropCreateDatabaseIfModelChanges<GarageContext>
    {
        protected override void Seed(GarageContext context)
        {
            List<EntityHarley> listeHarley = new List<EntityHarley>();
            listeHarley.Add(new EntityHarley { Couleur = "Noire", Modele = "Fatbob", Reservoir = 15 });
            listeHarley.Add(new EntityHarley { Couleur = "Blanche", Modele = "Road King", Reservoir = 30 });
            listeHarley.Add(new EntityHarley { Couleur = "Noire", Modele = "883 Iron", Reservoir = 12 });
 
            List<EntityFerrari> listeFerrari = new List<EntityFerrari>();
            listeFerrari.Add(new EntityFerrari { Couleur = "Rouge", Modele = "Enzo" });
            listeFerrari.Add(new EntityFerrari { Couleur = "Bleue", Modele = "California" });
 
            listeHarley.ForEach(entity => context.Harleys.Add(entity));
            listeFerrari.ForEach(entity => context.Ferrari.Add(entity));
        }
    }

On peut voir qu'elle hérite d'une classe, très explicite, DropCreateDatabaseIfModelChanges.

Cette classe est une implémentation de IDatabaseInitializer, qui détruit la base et la recrée si le modèle change, et éventuellement, réinjecte un jeu de données.

C'est le rôle de la méthode « protected override void Seed(GarageContext context) ».

Et maintenant nous allons modifier le constructeur de GarageContext comme suit :

 
Sélectionnez
public GarageContext() : base("GarageMendez")

        {
            Database.SetInitializer<GarageContext>(new GarageContextInitializer());
        }       

Recompilons et relançons le programme, on obtient :

Image non disponible

Tout fonctionne, ma base a été recréée et dans la foulée a été alimentée. Bien sûr, nous n'oublierons pas d'enlever cette feature lors d'une mise en production, ça serait dommage de perdre des données… 

Sachez qu'il existe une dernière solution pour éviter d'avoir un message d'erreur suite à une modification des DTO. C'est Julie Lerman qui en parle sur son blog, il suffit de changer les stratégies d'initialisation, je vous laisse voir ça ici :

./fichiers/turning-off-code-first-database-initialization-completelyhttp://thedatafarm.com/blog/data-access/turning-off-code-first-database-initialization-completely/Maintenant que nous avons implémenté de l'héritage, il serait intéressant de voir comment cela s'est traduit en base.

VII-D. Code First et Héritage

Voyons ce que nous dis le Management Studio :

Image non disponible

On constate que chacune des propriétés propagées par l'héritage a été dupliquée dans les tables Harleys et Ferrari.

Maintenant que nous nous sommes assurés de ce point, établissons la relation entre le garage et les véhicules, en partant du principe qu'un garage peut posséder plusieurs véhicules et qu'un véhicule appartient à un seul garage.

Le moteur de génération de Code First utilise une convention d'écriture. Lors de la génération de la bdd, il va donc examiner vos classes.

La convention utilisée pour symboliser une relation du type « one to many » se fait via les mots clés virtual et ICollection.

Rajoutons les lignes suivantes dans l'entité garage :

 
Sélectionnez
public virtual ICollection<EntityVehicule> Vehicules { get; set; }

Et dans l'entité Vehicule nous rajoutons ceci :

 
Sélectionnez
public virtual EntityGarage Garage { get; set; }

Réexécutons le programme et observons les changements dans le Management Studio :

Image non disponible

Tout d'abord changement de taille, les tables pour les entités Harleys et Ferraris ont disparu pour laisser place à une nouvelle table pour les entités Vehicule. Bien évidemment, c'est l'héritage mis en place qui est responsable de ceci, nous allons voir pourquoi.

En ce qui concerne notre relation entre un garage et ses véhicules, on constate la présence d'un nouveau champ « Garage_ID », FK sur notre garage.

Si tous les types différents de véhicules sont stockés dans la même table, comment faire la différence ? Eh bien, grâce au nouveau champ créé automatiquement, le discriminant (colonne « Discriminator »). Regardons à quoi il ressemble.

Image non disponible

Le discriminant porte le nom de la classe, la FK sur le garage est null car nous ne l'avons pas renseigné, les enregistrements sont conformes à ce qui est dans le Seed.

Alors tout d'abord, pourquoi la création d'une seule supertable, correspondant en fait à la classe mère des entités Vehicules ?

Eh bien, par défaut, lorsque rien n'est précisé, Code First décide de créer la base de données selon le mode Table Per Hierarchy et encore par défaut, si rien n'est précisé c'est le nom de la classe qui fait office de discriminant.

Il est possible de customiser le discriminant avec plusieurs colonnes ou bien encore d'utiliser explicitement le mode Table Per Hierarchy.

Ensuite, sachez qu'il est possible de travailler selon un mode Table Per Type et Table Per Classe, mais cela dépasse un peu le cadre du tutoriel, je vous laisse chercher des infos là-dessus si cela vous intéresse.

Nous allons modifier la classe Harley pour y inclure un booléen indiquant l'existence d'une peinture personnalisée, ainsi que la méthode Seed pour prendre en compte la création d'un garage et relier les véhicules à ce garage.

Réexécutons et observons

Image non disponible

Nous avons bien récupéré l'id du garage créé, et on observe que le booléen rajouté est bien spécifique aux entités Harleys, il est initialisé à NULL pour les Ferrari n'ayant pas de sens pour ces entités. C'est peut-être dans ce genre de cas qu'un mode Table Per Type serait préférable).

Nous avons réalisé ici une relation très simple , du type « one to many », entre le garage et les véhicules . Mais on aurait pu faire beaucoup plus poussé grâce à l'API Fluent et à la méthode à surcharger « OnModelCreating » qui permet de modifier le schéma afin d'y apporter une plus grande précision sur les besoins requis.

Continuons ce tutoriel dans les techniques avancées en abordant les règles de validation.

VII-E. Décorer les propriétés avec des attributs

Pour l'instant, nous avons été relativement laxistes sur la valeur et le formalisme des données d'entrée. Nous allons mettre un peu plus de rigueur en décorant les propriétés souhaitées avec des attributs adéquats.

On se place dans la classe EntityVehicule, on observe ce que l'on a disposition dans le namespace prévu, à savoir System.ComponentModel.DataAnnotations.

Image non disponible

On peut par exemple spécifier qu'une propriété est une clé avec Key, fixer une fourchette avec le Range, indiquer un champ obligatoire avec Required ou bien carrément personnaliser le formalisme avec une expression rationnelle grâce à RegularExpression.

 
Sélectionnez
[Range(1,4, ErrorMessage="Le nombre de place est compris entre 1 et 4."]
public int NbPlaces { get; set; }     
[Required(ErrorMessage= "Le champs modèle est obligatoire.")]
public string Modele { get; set; }

VII-F. DbContext, oui mais...

Précédemment, je vous ai parlé du DbContext en vous expliquant que c'était une version simplifiée de l'objectContext. C'est effectivement le cas et nombre de méthodes sont masquées pour faciliter l'utilisation du dbContext.

Cependant, pour certains cas, il se peut que vous ayez besoin d'avoir recours à des méthodes exposées uniquement dans l'objectContext. Eh bien, sachez qu'à partir de votre dbContext, il est possible d'obtenir l'accès à un ObjectContext, et pour cause, en interne, c'est un ObjectContext qui fait les traitements.

Pour ce faire, on utilise le fait que DbContext implémente l'interface IObjectContextAdapter, on ajoute alors dans notre GarageContext :

 
Sélectionnez
 public ObjectContext ObjectContext()
 {
     return (this as IObjectContextAdapter).ObjectContext;
 }

Et voilà, nous avons désormais accès à l'ObjectContext interne de notre DbContext !

VIII. Conclusion

Notre tutoriel est enfin fini ! Nous sommes donc partis de simples classes décrivant notre modèle de données pour générer une base de données. Ces mêmes classes nous ont également permis d'interagir avec la base et de manipuler les données, tout ça sans action directe avec un outil du type SQL Management Studio, du pur code !

Certains pourraient se dire que cette solution ne fait pas très sérieuse, voire un peu trop fantaisiste par rapport à la précision de leur script SQL. Sachez que cette solution est régulièrement mise en avant dans des démos officielles, comme j'ai pu le voir au Tech Days de Marseille lors de la session architecture, ainsi que dans des tutoriels sur les sites officiels de Microsoft.

Certains sont allergiques au fait de devoir tout écrire côté code. Si ce n'est pas fait là, c'est fait dans un script SQL, alors pour ceux qui comme moi aime l'approche full code, c'est parfait : on a un outil avec une prise en main très simple et si l'on souhaite pousser la mécanique, des fonctions comme celles de l'API Fluent et les DataAnnotations sont là pour répondre au besoin afin de fournir une bdd ciselée au poil.

J'ai envie de vous citer le blog de Julie Lerman sur le sujet : « Code First and DbContext are now ‘The Entity Framework' » http://thedatafarm.com/blog/data-access/code-first-and-dbcontext-are-now-ldquo-the-entity-framework-rdquo/ que l'on pourrait résumer en disant que maintenant, lorsque l'on parle d'EF, on parle de Code First.

De plus, il suffit de regarder les dernières entrées dans le blog de la team ADO.NET pour se rendre compte de l'importance donnée à CF, c'est à mes yeux une solution simple et efficace.

On la privilégiera tout de même pour une nouvelle base de données. En effet, même s'il est possible de faire du CF à partir d'une base existante, cela reste tout de même moins rapide qu'un simple Drag and Drop dans un modèle edmx, surtout si le nombre de table est important (des add-ons Visual Studio proposent de faire la génération des DTO pour ce cas).

Comme le dit Julie, je vais conclure en vous disant : « Use the best tool for the job […] for *your* job “ »

IX. Liens et références

Si je ne devais citer qu'une référence, ce serait le blog de Julie Lerman. Auteur de « Entity Framework », « Entity Framework 2 », « DbContext » parues chez O'Reilly, en étroite relation avec les équipes EF de Microsoft, c'est LA référence dans le domaine et Julie Lerman est une blogueuse très active.

http://thedatafarm.com/blog/.

Je compléterai avec le blog de la team ADO.NET, également très active :

http://blogs.msdn.com/b/adonet/.

Voici le zip de la solution complète:

CodeFirst.zip.

X. Remerciements

Je tiens à remercier l'ensemble de l'équipe .NET pour ses corrections, en particulier Tomlev. J'aimerais également remercier Max, Djibril et jacques_jean pour leurs aides et relecture orthographique.