Conventions de découverte de relations

EF Core utilise un ensemble de conventions lors de la découverte et de la génération d’un modèle en fonction des classes de type d’entité. Ce document récapitule les conventions utilisées pour la découverte et la configuration des relations entre les types d’entités.

Important

Les conventions décrites ici peuvent être remplacées par une configuration explicite de la relation à l’aide d’attributs de mappage ou de l’API de génération de modèles.

Conseil

Le code ci-dessous se trouve dans RelationshipConventions.cs.

Détection des navigations

La détection de relations commence par celle des navigations entre les types d’entités.

Navigations de référence

Une propriété d’un type d’entité est découverte comme navigation de référence quand :

  • La propriété est publique.
  • La propriété a une méthode getter et une méthode setter.
    • La méthode setter n’a pas besoin d’être publique ; elle peut être privée ou avoir n’importe quelle autre accessibilité.
    • La méthode setter peut être Init uniquement.
  • Le type de propriété est ou peut être un type d’entité. Cela signifie que le type
    • Doit être un type référence.
    • Ne doit pas avoir été configuré explicitement en tant que type de propriété primitif.
    • Ne doit pas être mappé en tant que type de propriété primitif par le fournisseur de base de données en cours d’utilisation.
    • Ne doit pas être automatiquement convertible en type de propriété primitif mappé par le fournisseur de base de données en cours d’utilisation.
  • La propriété n’est pas statique.
  • La propriété n’est pas une propriété d’indexeur.

Par exemple, considérons les types d’entités suivants :

public class Blog
{
    // Not discovered as reference navigations:
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public Uri? Uri { get; set; }
    public ConsoleKeyInfo ConsoleKeyInfo { get; set; }
    public Author DefaultAuthor => new() { Name = $"Author of the blog {Title}" };

    // Discovered as a reference navigation:
    public Author? Author { get; private set; }
}

public class Author
{
    // Not discovered as reference navigations:
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public int BlogId { get; set; }

    // Discovered as a reference navigation:
    public Blog Blog { get; init; } = null!;
}

Pour ces types, Blog.Author et Author.Blog sont découverts comme navigations de référence. En revanche, les propriétés suivantes ne sont pas découvertes comme navigations de référence :

  • Blog.Id, car int est un type primitif mappé
  • Blog.Title, car « string » est un type primitif mappé
  • Blog.Uri, car Uri est automatiquement converti en type primitif mappé
  • Blog.ConsoleKeyInfo, car ConsoleKeyInfo est un type de valeur C#
  • Blog.DefaultAuthor, car la propriété n’a pas de méthode setter
  • Author.Id, car Guid est un type primitif mappé
  • Author.Name, car « string » est un type primitif mappé
  • Author.BlogId, car int est un type primitif mappé

Navigations de collection

Une propriété d’un type d’entité est découverte comme navigation de collection quand :

  • La propriété est publique.
  • La propriété a une méthode getter. Les navigations de collection peuvent avoir des méthodes setter, mais ce n’est pas obligatoire.
  • Le type de propriété est ou implémente IEnumerable<TEntity>, où TEntity est un type d’entité. Cela signifie que le type TEntity :
    • Doit être un type référence.
    • Ne doit pas avoir été configuré explicitement en tant que type de propriété primitif.
    • Ne doit pas être mappé en tant que type de propriété primitif par le fournisseur de base de données en cours d’utilisation.
    • Ne doit pas être automatiquement convertible en type de propriété primitif mappé par le fournisseur de base de données en cours d’utilisation.
  • La propriété n’est pas statique.
  • La propriété n’est pas une propriété d’indexeur.

Par exemple, dans le code suivant, Blog.Tags et Tag.Blogs sont découverts comme navigations de collection :

public class Blog
{
    public int Id { get; set; }
    public List<Tag> Tags { get; set; } = null!;
}

public class Tag
{
    public Guid Id { get; set; }
    public IEnumerable<Blog> Blogs { get; } = new List<Blog>();
}

Appariement des navigations

Une fois qu’une navigation allant, par exemple, du type d’entité A au type d’entité B est découverte, il convient ensuite de déterminer si cette navigation a un sens inverse qui va dans le sens opposé, de type d’entité B au type d’entité A. Si un tel inverse est trouvé, les deux navigations sont appariées pour former une relation bidirectionnelle unique.

Le type de relation est déterminé selon que la navigation et son inverse sont des navigations de référence ou de collection. Plus précisément :

  • Si une navigation est une navigation de collection et que l’autre est une navigation de référence, la relation est une-à-plusieurs.
  • Si les deux navigations sont des navigations de référence, la relation est une-à-une.
  • Si les deux navigations sont des navigations de collection, la relation est plusieurs-à-plusieurs.

La découverte de chacun de ces types de relation est illustrée dans les exemples ci-dessous :

Une relation une-à-plusieurs unique est découverte entre Blog et Post par appariement des navigations Blog.Posts et Post.Blog :

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Une relation une-à-une unique est découverte entre Blog et Author par appariement des navigations Blog.Author et Author.Blog :

public class Blog
{
    public int Id { get; set; }
    public Author? Author { get; set; }
}

public class Author
{
    public int Id { get; set; }
    public int? BlogId { get; set; }
    public Blog? Blog { get; set; }
}

Une relation plusieurs-à-plusieurs unique est découverte entre Post et Tag par appariement des navigations Post.Tags et Tag.Posts :

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Remarque

Cet appariement de navigations peut être incorrecte si les deux navigations représentent deux relations unidirectionnelles différentes. Dans ce cas, les deux relations doivent être configurées explicitement.

L’appariement de relations fonctionne uniquement lorsqu’il existe une relation unique entre deux types. Plusieurs relations entre deux types doivent être configurées explicitement.

Remarque

Les descriptions ici sont en termes de relations entre deux types différents. Toutefois, il est possible que le même type soit aux deux fins d’une relation. Par conséquent, un type unique peut avoir deux navigations jumelées entre elles. Cela s’appelle une relation d’autoréférencement.

Détection des propriétés d’une clé étrangère

Une fois que les navigations d’une relation ont été découvertes ou configurées explicitement, ces navigations permettent de découvrir les propriétés de clé étrangère appropriées pour la relation. Une propriété est découverte en tant que clé étrangère lorsque :

  • Le type de propriété est compatible avec la clé primaire ou alternative sur le type d’entité principal.
    • Les types sont compatibles s’ils sont identiques ou si le type de propriété de clé étrangère est une version pouvant accepter la valeur Null du type de propriété de clé primaire ou secondaire.
  • Le nom de la propriété correspond à l’une des conventions d’affectation de noms d’une propriété de clé étrangère. Les conventions d’affectation de noms sont les suivantes :
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • En outre, si l’extrémité dépendante a été explicitement configurée à l’aide de l’API de génération de modèles et que la clé primaire dépendante est compatible, la clé primaire dépendante servira également de clé étrangère.

Conseil

Le suffixe « ID » peut avoir n’importe quelle casse.

Les types d’entités suivants présentent des exemples pour chacune de ces conventions d’affectation de noms.

Post.TheBlogKey est découvert comme clé étrangère, car elle correspond au modèle <navigation property name><principal key property name> :

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.TheBlogID est découvert comme clé étrangère, car elle correspond au modèle <navigation property name>Id :

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? TheBlogID { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.BlogKey est découvert comme clé étrangère, car elle correspond au modèle <principal entity type name><principal key property name> :

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? BlogKey { get; set; }
    public Blog? TheBlog { get; set; }
}

Post.Blogid est découvert comme clé étrangère, car elle correspond au modèle <principal entity type name>Id :

public class Blog
{
    public int Key { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int? Blogid { get; set; }
    public Blog? TheBlog { get; set; }
}

Remarque

Dans le cas des navigations une-à-plusieurs, les propriétés de clé étrangère doivent se trouver sur le type avec la navigation de référence, car il s’agit de l’entité dépendante. Dans le cas de relations une-à-une, la détection d’une propriété de clé étrangère permet de déterminer quel type représente l’extrémité dépendante de la relation. Si aucune propriété de clé étrangère n’est détectée, l’extrémité dépendante doit être configurée via HasForeignKey. Veuillez consulter la rubrique Relations une-à-une pour obtenir des exemples de ce problème.

Les règles ci-dessus s’appliquent également aux clés étrangères composites, où chaque propriété du composite doit avoir un type compatible avec la propriété correspondante de la clé primaire ou secondaire, et chaque nom de propriété doit correspondre à l’une des conventions d’affectation de noms décrites ci-dessus.

Détermination de la cardinalité

EF utilise les navigations découvertes et les propriétés de clé étrangère pour déterminer la cardinalité de la relation avec ses fins principales et dépendantes :

  • S’il existe une navigation de référence non appariée, la relation est configurée comme unidirectionnelle une-à-plusieurs, avec la navigation de référence sur l’extrémité dépendante.
  • S’il existe une navigation de collection non appariée, la relation est configurée comme unidirectionnelle une-à-plusieurs, avec la navigation de collection sur l’extrémité principale.
  • S’il existe des navigations de référence et de collection appariées, la relation est configurée comme bidirectionnelle une-à-plusieurs, avec la navigation de collection sur l’extrémité principale.
  • Si une navigation de référence est appariée à une autre navigation de référence, alors :
    • Si une propriété de clé étrangère a été découverte d’un côté mais pas de l’autre, la relation est configurée comme bidirectionnelle une-à-une, avec la propriété de clé étrangère sur l’extrémité dépendante.
    • Sinon, le côté dépendant ne peut pas être déterminé et EF émet une exception indiquant que le côté dépendant doit être configuré explicitement.
  • Si une navigation de collection est associée à une autre navigation de collection, la relation est configurée comme bidirectionnelle plusieurs-à-plusieurs.

Propriétés cachées de clé étrangère

Si EF a déterminé l’extrémité dépendante de la relation, mais qu’aucune propriété de clé étrangère n’a été découverte, EF crée une propriété cachée pour représenter la clé étrangère. La propriété cachée :

  • Possède le type de la propriété de clé primaire ou secondaire à l’extrémité de la relation.
    • Le type peut accepter la valeur Null par défaut, ce qui rend la relation facultative par défaut.
  • S’il existe une navigation sur l’extrémité dépendante, la propriété cachée de clé étrangère est nommée à l’aide de ce nom de navigation concaténé avec le nom de propriété de clé primaire ou secondaire.
  • S’il n’existe aucune navigation sur l’extrémité dépendante, la propriété cachée de clé étrangère est nommée à l’aide du nom de navigation cde type d’entité principal concaténé avec le nom de propriété de clé primaire ou secondaire.

Suppression en cascade

Par convention, les relations obligatoires sont configurées pour la suppression en cascade. Les relations facultatives sont configurées pour l’absence de suppression en cascade.

Plusieurs-à-plusieurs

Les relations plusieurs-à-plusieurs n’ont pas d’extrémités principales et dépendantes. D’autre part, aucune des deux extrémités ne contient de propriété de clé étrangère. Au lieu de cela, les relations plusieurs-à-plusieurs utilisent un type d’entité de jointure qui contient des paires de clés étrangères pointant vers l’une ou l’autre des extrémités de la relation plusieurs-à-plusieurs. Examinez les types d’entités suivants, pour lesquels une relation plusieurs-à-plusieurs est découverte par convention :

public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; } = new List<Tag>();
}

public class Tag
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

Les conventions utilisées dans cette détection sont les suivantes :

  • Le type d’entité de jointure est nommé <left entity type name><right entity type name>. Donc, PostTag dans cet exemple.
    • La table de jointure porte le même nom que le type d’entité de jointure.
  • Le type d’entité de jointure reçoit une propriété de clé étrangère pour chaque direction de la relation. Ces éléments sont nommés <navigation name><principal key name>. Ainsi, dans cet exemple, les propriétés de clé étrangère sont PostsId et TagsId.
    • Pour une relation plusieurs-à-plusieurs unidirectionnelle, la propriété de clé étrangère sans navigation associée est nommée <principal entity type name><principal key name>.
  • Les propriétés de clé étrangère sont non-nullables, ce qui rend les deux relations à l’entité de jointure obligatoires.
    • Les conventions de suppression en cascade signifient que ces relations seront configurées pour la suppression en cascade.
  • Le type d’entité de jointure est configuré avec une clé primaire composite composée des deux propriétés de clé étrangère. Ainsi, dans cet exemple, la clé primaire est constituée de PostsId et de TagsId.

Cela génère le modèle EF suivant :

Model:
  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Tags (ICollection<Tag>) CollectionTag Inverse: Posts
    Keys:
      Id PK
  EntityType: Tag
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Skip navigations:
      Posts (ICollection<Post>) CollectionPost Inverse: Tags
    Keys:
      Id PK
  EntityType: PostTag (Dictionary<string, object>) CLR Type: Dictionary<string, object>
    Properties:
      PostsId (no field, int) Indexer Required PK FK AfterSave:Throw
      TagsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
    Keys:
      PostsId, TagsId PK
    Foreign keys:
      PostTag (Dictionary<string, object>) {'PostsId'} -> Post {'Id'} Cascade
      PostTag (Dictionary<string, object>) {'TagsId'} -> Tag {'Id'} Cascade
    Indexes:
      TagsId

Et se traduit par le schéma de base de données suivant quand vous utilisez SQLite :

CREATE TABLE "Posts" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Posts" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "Tag" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Tag" PRIMARY KEY AUTOINCREMENT);

CREATE TABLE "PostTag" (
    "PostsId" INTEGER NOT NULL,
    "TagsId" INTEGER NOT NULL,
    CONSTRAINT "PK_PostTag" PRIMARY KEY ("PostsId", "TagsId"),
    CONSTRAINT "FK_PostTag_Posts_PostsId" FOREIGN KEY ("PostsId") REFERENCES "Posts" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_PostTag_Tag_TagsId" FOREIGN KEY ("TagsId") REFERENCES "Tag" ("Id") ON DELETE CASCADE);

CREATE INDEX "IX_PostTag_TagsId" ON "PostTag" ("TagsId");

Index

Par convention, EF crée un index de base de données pour la propriété ou les propriétés d’une clé étrangère. Le type d’index créé est déterminé par :

  • La cardinalité de la relation
  • La question de savoir si la relation est facultative ou obligatoire
  • Le nombre de propriétés qui composent la clé étrangère

Pour une relation une-à-plusieurs, un index simple est créé par convention. Le même index est créé pour les relations facultatives et obligatoires. Par exemple, sur SQLite :

CREATE INDEX "IX_Post_BlogId" ON "Post" ("BlogId");

Ou sur SQL Server :

CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

Pour une relation une-à-une obligatoire, un index unique est créé. Par exemple, sur SQLite :

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

Ou sur SQL Server :

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]);

Pour les relations une-à-une facultatives, l’index créé sur SQLite est le même :

CREATE UNIQUE INDEX "IX_Author_BlogId" ON "Author" ("BlogId");

Toutefois, sur SQL Server, un filtre IS NOT NULL est ajouté pour mieux gérer les valeurs de clés étrangères nulles. Par exemple :

CREATE UNIQUE INDEX [IX_Author_BlogId] ON [Author] ([BlogId]) WHERE [BlogId] IS NOT NULL;

Pour les clés étrangères composites, un index est créé couvrant toutes les colonnes de clés étrangères. Par exemple :

CREATE INDEX "IX_Post_ContainingBlogId1_ContainingBlogId2" ON "Post" ("ContainingBlogId1", "ContainingBlogId2");

Remarque

EF ne crée pas d’index pour les propriétés déjà couvertes par une contrainte d’index ou de clé primaire existante.

Comment arrêter la création d’index par EF pour les clés étrangères

Les index ont une surcharge et, comme demandé ici, il n'est pas toujours approprié de les créer pour toutes les colonnes FK. Pour y parvenir, le paramètre ForeignKeyIndexConvention peut être supprimé lors de la construction du modèle :

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Lorsque vous le souhaitez, les index peuvent toujours être explicitement créés pour les colonnes qui n’en ont pas besoin.

Noms des contraintes de clés étrangères

Par convention, les contraintes de clé étrangère sont nommées FK_<dependent type name>_<principal type name>_<foreign key property name>. Pour les clés étrangères composites, <foreign key property name> devient une liste des noms de propriétés de clé étrangère séparés par un trait de soulignement.

Ressources supplémentaires