Convenções para descoberta de relacionamento

O EF Core usa um conjunto de convenções ao descobrir e criar um modelo com base em classes de tipo de entidade. Este documento resume as convenções usadas para descobrir e configurar relações entre tipos de entidade.

Importante

As convenções descritas aqui podem ser substituídas pela configuração explícita da relação usando atributos de mapeamento ou a API de criação de modelos.

Dica

O código abaixo pode ser encontrado em RelationshipConventions.cs.

Descobrindo navegações

A descoberta de relações começa descobrindo navegações entre tipos de entidade.

Navegações de referência

Uma propriedade de um tipo de entidade é descoberta como uma navegação de referência quando:

  • A propriedade é pública.
  • A propriedade tem um getter e um setter.
    • O setter não precisa ser público; ele pode ser privado ou ter qualquer outra acessibilidade.
    • O setter pode ser Init-only.
  • O tipo de propriedade é, ou pode ser, um tipo de entidade. Isso significa que o tipo
    • Deve ser um tipo de referência.
    • Não deve ter sido configurado explicitamente como um tipo de propriedade primitiva.
    • Não deve ser mapeado como um tipo de propriedade primitiva pelo provedor de banco de dados que está sendo usado.
    • Não deve ser convertido automaticamente em um tipo de propriedade primitiva mapeado pelo provedor de banco de dados que está sendo usado.
  • A propriedade não é estática.
  • A propriedade não é uma propriedade do indexador.

Por exemplo, considere os seguintes tipos de entidade:

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!;
}

Para esses tipos Blog.Author e Author.Blog são descobertos como navegações de referência. Por outro lado, as seguintes propriedades não são descobertas como navegações de referência:

  • Blog.Id, porque int é um tipo primitivo mapeado
  • Blog.Title, porque 'string' é um tipo primitivo mapeado
  • Blog.Uri, porque Uri é convertido automaticamente em um tipo primitivo mapeado
  • Blog.ConsoleKeyInfo, porque ConsoleKeyInfo é um tipo de valor C#
  • Blog.DefaultAuthor, porque a propriedade não tem um setter
  • Author.Id, porque Guid é um tipo primitivo mapeado
  • Author.Name, porque 'string' é um tipo primitivo mapeado
  • Author.BlogId, porque int é um tipo primitivo mapeado

Navegações de coleção

Uma propriedade de um tipo de entidade é descoberta como uma navegação de coleção quando:

  • A propriedade é pública.
  • A propriedade tem um getter. As navegaçãos de coleção podem ter setters, mas isso não é necessário.
  • O tipo de propriedade é ou implementa IEnumerable<TEntity>, onde TEntity está ou pode ser, um tipo de entidade. Isso significa que o tipo de TEntity:
    • Deve ser um tipo de referência.
    • Não deve ter sido configurado explicitamente como um tipo de propriedade primitiva.
    • Não deve ser mapeado como um tipo de propriedade primitiva pelo provedor de banco de dados que está sendo usado.
    • Não deve ser convertido automaticamente em um tipo de propriedade primitiva mapeado pelo provedor de banco de dados que está sendo usado.
  • A propriedade não é estática.
  • A propriedade não é uma propriedade do indexador.

Por exemplo, no código a seguir, ambos Blog.Tags são Tag.Blogs descobertos como navegações de coleção:

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>();
}

Navegação de emparelhamento

Depois que uma navegação que vai de, por exemplo, o tipo de entidade A para o tipo de entidade B for descoberta, ela deverá ser determinada em seguida se essa navegação tiver um inverso indo na direção oposta, ou seja, do tipo de entidade B ao tipo de entidade A. Se esse inverso for encontrado, as duas navegações serão emparelhadas para formar uma única relação bidirecional.

O tipo de relação é determinado por se a navegação e seu inverso são navegações de referência ou coleção. Especificamente:

  • Se uma navegação for uma navegações de coleção e a outra for uma navegação de referência, a relação será um para muitos.
  • Se ambas as navegações forem navegações de referência, a relação será um-para-um.
  • Se ambas as navegações forem navegações de coleção, a relação será muitos para muitos.

A descoberta de cada um desses tipos de relação é mostrada nos exemplos abaixo:

Uma única relação um-para-muitos é descoberta entre Blog e Post é descoberta emparelhando as navegações Blog.Posts e 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; }
}

Uma única relação um-para-um é descoberta entre Blog e Author é descoberta emparelhando as navegações Blog.Author e 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; }
}

Uma única relação de muitos para muitos é descoberta entre Post e Tag é descoberta emparelhando as navegações Post.Tags e 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>();
}

Observação

Esse emparelhamento de navegações poderá estar incorreto se as duas navegações representarem duas relações unidirecionais diferentes. Nesse caso, as duas relações devem ser configuradas explicitamente.

O emparelhamento de relações só funciona quando há uma única relação entre dois tipos. Várias relações entre dois tipos devem ser configuradas explicitamente.

Observação

As descrições aqui estão em termos de relações entre dois tipos diferentes. No entanto, é possível que o mesmo tipo esteja em ambas as extremidades de uma relação e, portanto, para um único tipo ter duas navegações emparelhadas entre si. Isso é chamado de relação de auto-referência.

Descobrindo propriedades de chave estrangeira

Depois que as navegações de uma relação tiverem sido descobertas ou configuradas explicitamente, essas navegações serão usadas para descobrir as propriedades de chave estrangeira apropriadas para a relação. Uma propriedade é descoberta como uma chave estrangeira quando:

  • O tipo de propriedade é compatível com a chave primária ou alternativa no tipo de entidade principal.
    • Os tipos serão compatíveis se forem iguais ou se o tipo de propriedade de chave estrangeira for uma versão anulável do tipo de propriedade de chave primária ou alternativa.
  • O nome da propriedade corresponde a uma das convenções de nomenclatura de uma propriedade de chave estrangeira. As convenções de nomenclatura são:
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • Além disso, se o final dependente tiver sido configurado explicitamente usando a API de criação de modelo e a chave primária dependente for compatível, a chave primária dependente também será usada como chave estrangeira.

Dica

O sufixo "ID" pode ter qualquer maiúscula.

Os tipos de entidade a seguir mostram exemplos para cada uma dessas convenções de nomenclatura.

Post.TheBlogKey é descoberto como a chave estrangeira porque corresponde ao padrão <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 é descoberto como a chave estrangeira porque corresponde ao padrão <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 é descoberto como a chave estrangeira porque corresponde ao padrão <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 é descoberto como a chave estrangeira porque corresponde ao padrão <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; }
}

Observação

No caso de navegação um para muitos, as propriedades de chave estrangeira devem estar no tipo com a navegação de referência, pois essa será a entidade dependente. No caso de relações um-para-um, a descoberta de uma propriedade de chave estrangeira é usada para determinar qual tipo representa o fim dependente da relação. Se nenhuma propriedade de chave estrangeira for descoberta, o final dependente deverá ser configurado usando HasForeignKey. Consulte relações um-para-um para obter exemplos disso.

As regras acima também se aplicam a chaves estrangeiras compostas, em que cada propriedade da composição deve ter um tipo compatível com a propriedade correspondente da chave primária ou alternativa, e cada nome de propriedade deve corresponder a uma das convenções de nomenclatura descritas acima.

Determinando a cardinalidade

O EF usa as navegações descobertas e as propriedades de chave estrangeira para determinar a cardinalidade da relação junto com suas extremidades principais e dependentes:

  • Se houver uma navegação de referência não paga, a relação será configurada como unidirecional um para muitos, com a navegação de referência no final dependente.
  • Se houver uma navegação de coleção não paga, a relação será configurada como unidirecional um para muitos, com a navegação da coleção na extremidade principal.
  • Se houver navegações de referência e coleção emparelhadas, a relação será configurada como bidirecional um para muitos, com a navegação da coleção na extremidade principal.
  • Se uma navegação de referência estiver emparelhada com outra navegação de referência, então:
    • Se uma propriedade de chave estrangeira foi descoberta de um lado, mas não do outro, a relação é configurada como um um-para-um bidirecional, com a propriedade de chave estrangeira no final dependente.
    • Caso contrário, o lado dependente não poderá ser determinado e o EF gerará uma exceção indicando que o dependente deve ser configurado explicitamente.
  • Se uma navegação de coleção estiver emparelhada com outra navegação de coleção, a relação será configurada como um muitos para muitos bidirecionais.

Propriedades de chave estrangeira de sombra

Se o EF determinou o fim dependente da relação, mas nenhuma propriedade de chave estrangeira foi descoberta, o EF criará uma propriedade de sombra para representar a chave estrangeira. A propriedade de sombra:

  • Tem o tipo da propriedade de chave primária ou alternativa no final principal da relação.
    • O tipo é tornado anulável por padrão, tornando a relação opcional por padrão.
  • Se houver uma navegação no final dependente, a propriedade de chave estrangeira de sombra será nomeada usando esse nome de navegação concatenado com o nome da propriedade de chave primária ou alternativa.
  • Se não houver navegação no final dependente, a propriedade de chave estrangeira de sombra será nomeada usando o nome do tipo de entidade principal concatenado com o nome da propriedade de chave primária ou alternativa.

Excluir em cascata

Por convenção, as relações necessárias são configuradas para exclusão em cascata. As relações opcionais são configuradas para não excluir em cascata.

Muitos para muitos

Relações muitos para muitos não têm extremidades principais e dependentes, e nenhuma das extremidades contém uma propriedade de chave estrangeira. Em vez disso, relações muitos para muitos usam um tipo de entidade de junção que contém pares de chaves estrangeiras apontando para qualquer extremidade do muitos para muitos. Considere os seguintes tipos de entidade, para os quais uma relação muitos para muitos é descoberta por convenção:

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>();
}

As convenções usadas nesta descoberta são:

  • O tipo de entidade de junção é nomeado <left entity type name><right entity type name>. Portanto, PostTag neste exemplo.
    • A tabela de junção tem o mesmo nome do tipo de entidade de junção.
  • O tipo de entidade de junção recebe uma propriedade de chave estrangeira para cada direção da relação. Eles são nomeados <navigation name><principal key name>. Portanto, neste exemplo, as propriedades de chave estrangeira são PostsId e TagsId.
    • Para um muitos-para-muitos unidirecional, a propriedade de chave estrangeira sem uma navegação associada é nomeada <principal entity type name><principal key name>.
  • As propriedades de chave estrangeira são não anuláveis, tornando necessárias ambas as relações com a entidade de junção.
    • As convenções de exclusão em cascata significam que essas relações serão configuradas para exclusão em cascata.
  • O tipo de entidade de junção é configurado com uma chave primária composta que consiste nas duas propriedades de chave estrangeira. Portanto, neste exemplo, a chave primária é composta de PostsId e TagsId.

Isso resulta no seguinte modelo EF:

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

E se traduz no seguinte esquema de banco de dados ao usar o 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");

Índices

Por convenção, o EF cria um índice de banco de dados para a propriedade ou as propriedades de uma chave estrangeira. O tipo de índice criado é determinado por:

  • A cardinalidade da relação
  • Se a relação é opcional ou necessária
  • O número de propriedades que compõem a chave estrangeira

Para uma relação um-para-muitos, um índice simples é criado por convenção. O mesmo índice é criado para relações opcionais e necessárias. Por exemplo, no SQLite:

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

Ou no SQL Server:

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

Para uma relação um-para-um necessária, um índice exclusivo é criado. Por exemplo, no SQLite:

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

Ou no SQL Sever:

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

Para relações um-para-um opcionais, o índice criado no SQLite é o mesmo:

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

No entanto, no SQL Server, um filtro IS NOT NULL é adicionado para lidar melhor com valores nulos de chave estrangeira. Por exemplo:

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

Para chaves estrangeiras compostas, um índice é criado cobrindo todas as colunas de chave estrangeira. Por exemplo:

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

Observação

O EF não cria índices para propriedades que já estão cobertas por um índice existente ou restrição de chave primária.

Como parar a criação de índices de EF para chaves estrangeiras

Os índices têm sobrecarga e, conforme perguntado aqui, pode nem sempre ser apropriado criá-los para todas as colunas FK. Para conseguir isso, a ForeignKeyIndexConvention pode ser removida ao construir o modelo:

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

Quando desejado, os índices ainda podem ser criados explicitamente para as colunas de chave estrangeira que precisam delas.

Nomes de restrição de chave estrangeira

Por convenção, as restrições de chave estrangeira são nomeadas FK_<dependent type name>_<principal type name>_<foreign key property name>. Para chaves estrangeiras compostas, <foreign key property name> torna-se uma lista separada de sublinhados de nomes de propriedades de chave estrangeira.

Recursos adicionais