Conventions Code First personnalisées

Remarque

EF6 et versions ultérieures uniquement : Les fonctionnalités, les API, etc. décrites dans cette page ont été introduites dans Entity Framework 6. Si vous utilisez une version antérieure, certaines ou toutes les informations ne s’appliquent pas.

Lorsque vous utilisez Code First, votre modèle est calculé à partir de vos classes à l’aide d’un ensemble de conventions. Les Conventions code first par défaut déterminent les éléments comme la clé primaire d’une entité, le nom de la table à laquelle une entité est mappée, ainsi que la précision et l’échelle d’une colonne décimale par défaut.

Parfois, ces conventions par défaut ne sont pas idéales pour votre modèle et vous devez les contourner en configurant de nombreuses entités individuelles à l’aide d’annotations de données ou de l’API Fluent. Conventions Code First personnalisées vous permettent de définir vos propres conventions qui fournissent des paramètres de configuration par défaut pour votre modèle. Dans cette procédure pas à pas, nous allons explorer les différents types de conventions personnalisées et comment les créer.

Conventions basées sur des modèles

Cette page couvre l’API DbModelBuilder pour les conventions personnalisées. Cette API doit être suffisante pour créer la plupart des conventions personnalisées. Toutefois, il existe également la possibilité de créer des conventions basées sur des modèles ( conventions qui manipulent le modèle final une fois qu’il est créé) pour gérer des scénarios avancés. Pour plus d’informations, consultez conventions basées sur des modèles.

 

Notre modèle

Commençons par définir un modèle simple que nous pouvons utiliser avec nos conventions. Ajoutez les classes suivantes à votre projet.

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }
    }

    public class Product
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public decimal? Price { get; set; }
        public DateTime? ReleaseDate { get; set; }
        public ProductCategory Category { get; set; }
    }

    public class ProductCategory
    {
        public int Key { get; set; }
        public string Name { get; set; }
        public List<Product> Products { get; set; }
    }

 

Présentation des conventions personnalisées

Écrivons une convention qui configure toute propriété nommée Clé comme clé primaire pour son type d’entité.

Les conventions sont activées sur le générateur de modèles, accessible en remplaçant OnModelCreating dans le contexte. Mettez à jour la classe ProductContext comme suit :

    public class ProductContext : DbContext
    {
        static ProductContext()
        {
            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
        }

        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Properties()
                        .Where(p => p.Name == "Key")
                        .Configure(p => p.IsKey());
        }
    }

À présent, toute propriété de notre modèle nommé Key sera configurée en tant que clé primaire de l’entité dont elle fait partie.

Nous pourrions également rendre nos conventions plus spécifiques en filtrant sur le type de propriété que nous allons configurer :

    modelBuilder.Properties<int>()
                .Where(p => p.Name == "Key")
                .Configure(p => p.IsKey());

Cela configurera toutes les propriétés appelées Key pour qu'elles soient la clé primaire de leur entité, mais seulement si elles sont un nombre entier.

Une caractéristique intéressante de la méthode IsKey est qu’il est additif. Cela signifie que si vous appelez IsKey sur plusieurs propriétés, elles feront toutes partie d'une clé composite. La seule mise en garde pour cela est que lorsque vous spécifiez plusieurs propriétés pour une clé, vous devez également spécifier un ordre pour ces propriétés. Pour ce faire, appelez la méthode HasColumnOrder comme ci-dessous :

    modelBuilder.Properties<int>()
                .Where(x => x.Name == "Key")
                .Configure(x => x.IsKey().HasColumnOrder(1));

    modelBuilder.Properties()
                .Where(x => x.Name == "Name")
                .Configure(x => x.IsKey().HasColumnOrder(2));

Ce code configure les types de notre modèle pour avoir une clé composite composée de la colonne clé int et de la colonne nom de chaîne. Si nous affichons le modèle dans le concepteur, il se présente comme suit :

composite Key

Un autre exemple de conventions de propriété consiste à configurer toutes les propriétés DateTime de mon modèle pour qu’elles correspondent au type datetime2 dans SQL Server au lieu de datetime. Pour ce faire, procédez comme suit :

    modelBuilder.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));

 

Classes de Convention

Une autre façon de définir des conventions consiste à utiliser une classe de convention pour encapsuler votre convention. Lorsque vous utilisez une classe Convention, vous créez un type qui hérite de la classe Convention dans l’espace de noms System.Data.Entity.ModelConfiguration.Conventions.

Nous pouvons créer une classe de convention avec la convention datetime2 que nous avons montrée précédemment en procédant comme suit :

    public class DateTime2Convention : Convention
    {
        public DateTime2Convention()
        {
            this.Properties<DateTime>()
                .Configure(c => c.HasColumnType("datetime2"));        
        }
    }

Pour indiquer à EF d’utiliser cette convention, vous l’ajoutez à la collection Conventions dans OnModelCreating, qui, si vous suivez la procédure pas à pas, ressemble à ceci :

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Properties<int>()
                    .Where(p => p.Name.EndsWith("Key"))
                    .Configure(p => p.IsKey());

        modelBuilder.Conventions.Add(new DateTime2Convention());
    }

Comme vous pouvez le constater, nous ajoutons une instance de notre convention à la collection de conventions. L’héritage de convention offre un moyen pratique de regrouper et de partager des conventions entre les équipes ou les projets. Par exemple, vous pouvez avoir une bibliothèque de classes avec un ensemble commun de conventions que tous vos projets d’organisation utilisent.

 

Attributs personnalisés

Une autre utilisation intéressante des conventions consiste à permettre aux nouveaux attributs d’être utilisés lors de la configuration d’un modèle. Pour illustrer cela, nous allons créer un attribut que nous pouvons utiliser pour marquer les propriétés string comme non-Unicode.

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class NonUnicode : Attribute
    {
    }

Maintenant, nous allons créer une convention pour appliquer cet attribut à notre modèle :

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
                .Configure(c => c.IsUnicode(false));

Avec cette convention, nous pouvons ajouter l’attribut NonUnicode à l’une de nos propriétés de chaîne, ce qui signifie que la colonne de la base de données sera stockée en tant que varchar au lieu de nvarchar.

Une chose à noter à propos de cette convention est que si vous placez l’attribut NonUnicode sur quelque chose d’autre qu’une propriété de chaîne, il lève une exception. Cela est dû au fait que vous ne pouvez pas configurer IsUnicode sur n’importe quel type autre qu’une chaîne. Si cela se produit, vous pouvez rendre votre convention plus spécifique, afin qu’elle filtre tout ce qui n’est pas une chaîne.

Bien que la convention ci-dessus fonctionne pour définir des attributs personnalisés, il existe une autre API qui peut être beaucoup plus facile à utiliser, en particulier lorsque vous souhaitez utiliser des propriétés de la classe d’attributs.

Pour cet exemple, nous allons mettre à jour notre attribut et le remplacer par un attribut IsUnicode. Il ressemble donc à ceci :

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    internal class IsUnicode : Attribute
    {
        public bool Unicode { get; set; }

        public IsUnicode(bool isUnicode)
        {
            Unicode = isUnicode;
        }
    }

Une fois que nous l’avons, nous pouvons définir un bool sur notre attribut pour indiquer à la convention si une propriété doit être Unicode ou non. Nous pourrions le faire dans la convention que nous avons déjà en accédant à ClrProperty de la classe de configuration comme suit :

    modelBuilder.Properties()
                .Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
                .Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));

C'est assez facile, mais il existe un moyen plus succinct d'y parvenir en utilisant la méthode Having de l'API des conventions. La méthode Having a un paramètre de type Func<PropertyInfo, T> qui accepte PropertyInfo identique à la méthode Where, mais devrait retourner un objet. Si l’objet retourné est null, la propriété n’est pas configurée, ce qui signifie que vous pouvez filtrer les propriétés avec elle comme Where, mais elle est différente dans la mesure où elle capture également l’objet retourné et le transmet à la méthode Configure. Cela fonctionne comme suit :

    modelBuilder.Properties()
                .Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
                .Configure((config, att) => config.IsUnicode(att.Unicode));

Les attributs personnalisés ne sont pas la seule raison d’utiliser la méthode Having, il est utile n’importe où que vous devez raisonner sur un élément sur lequel vous filtrez lors de la configuration de vos types ou propriétés.

 

Configuration des types

Jusqu’à présent, toutes nos conventions ont été pour les propriétés, mais il existe une autre zone de l’API de conventions pour la configuration des types dans votre modèle. L’expérience est similaire aux conventions que nous avons vues jusqu’à présent, mais les options de configuration seront au niveau de l’entité au lieu du niveau de propriété.

L’une des choses que les conventions de niveau type peuvent être vraiment utiles pour modifier la convention d’affectation de noms de table, soit pour mapper à un schéma existant qui diffère de la valeur EF par défaut, soit pour créer une base de données avec une convention d’affectation de noms différente. Pour ce faire, nous avons d’abord besoin d’une méthode qui peut accepter TypeInfo pour un type dans notre modèle et retourner le nom de la table pour ce type :

    private string GetTableName(Type type)
    {
        var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Cette méthode prend un type et retourne une chaîne qui utilise le minuscule avec des traits de soulignement au lieu de CamelCase. Dans notre modèle, cela signifie que la classe ProductCategory sera mappée à une table appelée product_category au lieu de ProductCategories.

Une fois que nous avons cette méthode, nous pouvons l’appeler dans une convention comme suit :

    modelBuilder.Types()
                .Configure(c => c.ToTable(GetTableName(c.ClrType)));

Cette convention configure chaque type de notre modèle pour mapper le nom de la table retourné par notre méthode GetTableName. Cette convention équivaut à appeler la méthode ToTable pour chaque entité du modèle à l’aide de l’API Fluent.

Une chose à noter est que lorsque vous appelez ToTable EF prend la chaîne que vous fournissez comme nom de table exact, sans l’une des pluralisations qu’il ferait normalement lors de la détermination des noms de table. C’est pourquoi le nom de la table de notre convention est product_category au lieu de product_categories. Nous pouvons résoudre cela dans notre convention en appelant nous-mêmes le service de pluralisation.

Dans le code suivant, nous allons utiliser la fonctionnalité Dependency Resolution ajoutée dans EF6 pour récupérer le service de pluralisation que EF aurait utilisé et pluraliser notre nom de table.

    private string GetTableName(Type type)
    {
        var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();

        var result = pluralizationService.Pluralize(type.Name);

        result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);

        return result.ToLower();
    }

Remarque

La version générique de GetService est une méthode d’extension dans l’espace de noms System.Data.Entity.Infrastructure.DependencyResolution, vous devez ajouter une instruction using à votre contexte pour l’utiliser.

ToTable et héritage

Un autre aspect important de ToTable est que si vous mappez explicitement un type à une table donnée, vous pouvez modifier la stratégie de mappage utilisée par EF. Si vous appelez ToTable pour chaque type d’une hiérarchie d’héritage, en passant le nom du type comme celui de la table comme nous l’avons fait ci-dessus, vous allez modifier la stratégie de mappage table par hiérarchie (TPH) par défaut en table par type (TPT). La meilleure façon de décrire cela est un exemple concret :

    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Manager : Employee
    {
        public string SectionManaged { get; set; }
    }

Par défaut, les employés et le responsable sont mappés à la même table (Employés) dans la base de données. La table contient à la fois les employés et les responsables avec une colonne de discriminateur qui vous indique le type d’instance stocké dans chaque ligne. Il s’agit du mappage TPH, car il existe une table unique pour la hiérarchie. Toutefois, si vous appelez ToTable sur les deux classes, chaque type sera mappé à sa propre table, également appelé TPT, car chaque type possède sa propre table.

    modelBuilder.Types()
                .Configure(c=>c.ToTable(c.ClrType.Name));

Le code ci-dessus mappe à une structure de table qui ressemble à ce qui suit :

tpt Example

Vous pouvez éviter cela et gérer le mappage TPH par défaut, de deux façons :

  1. Appelez ToTable avec le même nom de table pour chaque type de la hiérarchie.
  2. Appelez ToTable uniquement sur la classe de base de la hiérarchie, dans notre exemple qui serait employé.

 

Ordre d’exécution

Les conventions fonctionnent de la même manière que l’API Fluent. Cela signifie que si vous écrivez deux conventions qui configurent la même option de la même propriété, la dernière à exécuter gagne. Par exemple, dans le code sous la longueur maximale de toutes les chaînes est définie sur 500, mais nous configurons ensuite toutes les propriétés appelées Name dans le modèle pour avoir une longueur maximale de 250.

    modelBuilder.Properties<string>()
                .Configure(c => c.HasMaxLength(500));

    modelBuilder.Properties<string>()
                .Where(x => x.Name == "Name")
                .Configure(c => c.HasMaxLength(250));

Étant donné que la convention pour définir la longueur maximale sur 250 est après celle qui définit toutes les chaînes sur 500, toutes les propriétés appelées Name dans notre modèle auront une valeur MaxLength de 250 tandis que toutes les autres chaînes, telles que les descriptions, seraient 500. L’utilisation de conventions de cette façon signifie que vous pouvez fournir une convention générale pour les types ou les propriétés dans votre modèle, puis les overide pour les sous-ensembles qui sont différents.

L’API Fluent et les annotations de données peuvent également être utilisées pour remplacer une convention dans des cas spécifiques. Dans notre exemple ci-dessus si nous avions utilisé l’API Fluent pour définir la longueur maximale d’une propriété, nous pourrions l’avoir placée avant ou après la convention, car l’API Fluent plus spécifique gagnera sur la convention de configuration plus générale.

 

Conventions intégrées

Étant donné que les conventions personnalisées peuvent être affectées par les conventions Code First par défaut, il peut être utile d’ajouter des conventions à exécuter avant ou après une autre convention. Pour ce faire, vous pouvez utiliser les méthodes AddBefore et AddAfter de la collection Conventions sur votre DbContext dérivé. Le code suivant ajoute la classe de convention que nous avons créée précédemment afin qu’elle s’exécute avant la convention de découverte de clé intégrée.

    modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());

Cela va être le plus utilisé lors de l’ajout de conventions qui doivent s’exécuter avant ou après les conventions intégrées, une liste des conventions intégrées est disponible ici : espace de noms System.Data.Entity.ModelConfiguration.Conventions.

Vous pouvez également supprimer des conventions que vous ne souhaitez pas appliquer à votre modèle. Pour supprimer une convention, utilisez la méthode Remove. Voici un exemple de suppression de PluralizingTableNameConvention.

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }