Convenciones para la detección de relaciones

EF Core usa un conjunto de convenciones para detectar y crear un modelo en función de clases de tipos de entidad. En este documento se resumen las convenciones usadas para detectar y configurar relaciones entre tipos de entidad.

Importante

Las convenciones que se describen aquí se pueden invalidar mediante la configuración explícita de la relación mediante atributos de asignación o la API de creación de modelos.

Sugerencia

El código siguiente se puede encontrar en RelationshipConventions.cs.

Detección de navegaciones

La detección de relaciones comienza con la detección de navegaciones entre tipos de entidad.

Navegaciones de referencia

Una propiedad de un tipo de entidad se detecta como navegación de referencia cuando:

  • La propiedad es pública.
  • La propiedad tiene un captador y un establecedor.
  • El tipo de propiedad es, o podría ser, un tipo de entidad. Esto significa que el tipo:
  • La propiedad no es estática.
  • La propiedad no es una propiedad de indexador.

Por ejemplo, considere los tipos de entidad siguientes:

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 estos tipos, Blog.Author y Author.Blog se detectan como navegaciones de referencia. Por otro lado, las siguientes propiedades no se detectan como navegaciones de referencia:

  • Blog.Id, porque int es un tipo primitivo asignado.
  • Blog.Title, porque "string" es un tipo primitivo asignado.
  • Blog.Uri, porque Uri se convierte automáticamente en un tipo primitivo asignado.
  • Blog.ConsoleKeyInfo, porque ConsoleKeyInfo es un tipo de valor de C#.
  • Blog.DefaultAuthor, porque la propiedad no tiene un establecedor.
  • Author.Id, porque Guid es un tipo primitivo asignado.
  • Author.Name, porque "string" es un tipo primitivo asignado.
  • Author.BlogId, porque int es un tipo primitivo asignado.

Navegaciones de recopilación

Una propiedad de un tipo de entidad se detecta como navegación de recopilación cuando:

  • La propiedad es pública.
  • La propiedad tiene un captador. Las navegaciones de recopilación pueden tener establecedores, pero no es algo necesario.
  • El tipo de propiedad es o implementa IEnumerable<TEntity>, donde TEntity es o podría ser un tipo de entidad. Esto significa que el tipo de TEntity:
  • La propiedad no es estática.
  • La propiedad no es una propiedad de indexador.

Por ejemplo, en el código siguiente, Blog.Tags y Tag.Blogs se detectan como navegaciones de recopilación:

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

Emparejamiento de navegaciones

Una vez que se detecta una navegación que va, por ejemplo, de la entidad de tipo A a la entidad de tipo B, debe determinarse si esta navegación tiene una inversa que va en la dirección opuesta, es decir, de la entidad de tipo B a la entidad de tipo A. Si se encuentra tal inversa, entonces las dos navegaciones se emparejan para formar una única relación bidireccional.

El tipo de relación viene determinado por el hecho de que la navegación y su inversa sean navegaciones de referencia o de recopilación. Concretamente:

  • Si una navegación es de recopilación y la otra es de referencia, la relación es de uno a varios.
  • Si ambas navegaciones son navegaciones de referencia, la relación es de uno a uno.
  • Si ambas navegaciones son de recopilación, la relación es de varios a varios.

La detección de cada uno de estos tipos de relación se muestra en los ejemplos siguientes:

Una única relación de uno a varios se detecta entre Blog y Post mediante el emparejamiento de las navegaciones Blog.Posts y 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; }
}

Una única relación de uno a uno se detecta entre Blog y Author mediante el emparejamiento de las navegaciones Blog.Author y 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; }
}

Una única relación de varios a varios se detecta entre Post y Tag mediante el emparejamiento de las navegaciones Post.Tags y 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>();
}

Nota

Este emparejamiento de navegaciones puede ser incorrecto si las dos navegaciones representan dos relaciones unidireccionales diferentes. En este caso, las dos relaciones deben configurarse explícitamente.

El emparejamiento de relaciones solo funciona cuando hay una única relación entre dos tipos. La presencia de varias relaciones entre dos tipos se debe configurar explícitamente.

Nota

Aquí se describen las relaciones entre dos tipos diferentes. Sin embargo, es posible que el mismo tipo esté en ambos extremos de una relación y que, por tanto, un único tipo tenga dos navegaciones emparejadas entre sí. Esto se conoce como relación con referencia automática.

Detección de propiedades de clave externa

Una vez que las navegaciones de una relación se han detectado o configurado explícitamente, estas se usan para detectar las propiedades de clave externa adecuadas para la relación. Una propiedad se detecta como una clave externa cuando:

  • El tipo de propiedad es compatible con la clave principal o alternativa en el tipo de entidad principal.
    • Los tipos son compatibles si son iguales o si el tipo de propiedad de clave externa es una versión que admite valores NULL del tipo de propiedad de clave principal o alternativa.
  • El nombre de la propiedad coincide con una de las convenciones de nomenclatura de una propiedad de clave externa. Las convenciones de nomenclatura son:
    • <navigation property name><principal key property name>
    • <navigation property name>Id
    • <principal entity type name><principal key property name>
    • <principal entity type name>Id
  • Además, si el extremo dependiente se ha configurado explícitamente mediante la API de creación de modelos y la clave principal dependiente es compatible, esta también se usará como clave externa.

Sugerencia

El sufijo "Id" puede estar en mayúscula o minúscula.

Los siguientes tipos de entidad muestran ejemplos de cada una de estas convenciones de nomenclatura.

Post.TheBlogKey se detecta como clave externa porque coincide con el patrón <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 se detecta como clave externa porque coincide con el patrón <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 se detecta como clave externa porque coincide con el patrón <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 se detecta como clave externa porque coincide con el patrón <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; }
}

Nota

En el caso de las navegaciones de uno a varios, las propiedades de clave externa deben estar en el tipo con la navegación de referencia, ya que será la entidad dependiente. En el caso de relaciones de uno a uno, se usa la detección de una propiedad de clave externa para determinar qué tipo representa el extremo dependiente de la relación. Si no se detecta ninguna propiedad de clave externa, el extremo dependiente debe configurarse mediante HasForeignKey. Consulte Relaciones de uno a uno para ver ejemplos de esto.

Las reglas anteriores también se aplican a claves externas compuestas, donde cada propiedad del compuesto debe tener un tipo compatible con la propiedad correspondiente de la clave principal o alternativa, y cada nombre de propiedad debe coincidir con una de las convenciones de nomenclatura descritas anteriormente.

Determinación de la cardinalidad

EF usa las navegaciones detectadas y las propiedades de clave externa para determinar la cardinalidad de la relación junto con sus extremos principal y dependiente:

  • Si hay una, la navegación de referencia no emparejada, la relación se configura como de uno a varios unidireccional, con la navegación de referencia en el extremo dependiente.
  • Si hay una, la navegación de recopilación no emparejada, la relación se configura como de uno a varios unidireccional, con la navegación de la recopilación en el extremo principal.
  • Si hay navegaciones de referencia y recopilación emparejadas, la relación se configura como de uno a varios bidireccional, con la navegación de recopilación en el extremo principal.
  • Si una navegación de referencia está emparejada con otra del mismo tipo, entonces:
    • Si se detectó una propiedad de clave externa en un lado, pero no el otro, la relación se configura como de uno a uno bidireccional, con la propiedad de clave externa en el extremo dependiente.
    • De lo contrario, no se puede determinar el lado dependiente y EF produce una excepción que indica que el dependiente debe configurarse explícitamente.
  • Si una navegación de recopilación está emparejada con otra del mismo tipo, la relación se configura como de muchos a muchos bidireccional.

Propiedades de clave externa reemplazadas

Si EF ha determinado el extremo dependiente de la relación, pero no se detectó ninguna propiedad de clave externa, creará una propiedad reemplazada para representar la clave externa. La propiedad reemplazada:

  • Tiene el tipo de la propiedad de clave principal o alternativa en el extremo principal de la relación.
    • El tipo pasa de forma predeterminada a admitir valores NULL, lo que hace que la relación sea opcional de forma predeterminada.
  • Si hay una navegación en el extremo dependiente, la propiedad de clave externa reemplazada se nombra utilizando este nombre de navegación concatenado con el nombre de la propiedad de clave principal o alternativa.
  • Si no hay ninguna navegación en el extremo dependiente, la propiedad de clave externa reemplazada se nombra mediante el nombre del tipo de entidad principal concatenado con el nombre de la propiedad de clave principal o alternativa.

Eliminación en cascada

Por convención, las relaciones obligatorias están configuradas para la eliminación en cascada. Las relaciones opcionales están configuradas para no eliminarse en cascada.

Varios a varios

Las relaciones de varios a varios no tienen extremos principal y dependiente, y ninguno de ellos contiene una propiedad de clave externa. En su lugar, las relaciones de varios a varios usan un tipo de entidad de combinación que contiene pares de claves externas que apuntan a cualquiera de los extremos de las relaciones de varios a varios. Tenga en cuenta los siguientes tipos de entidad, en los que una relación de varios a varios se detecta por convención:

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

Las convenciones usadas en esta detección son:

  • El tipo de entidad de combinación se nombra como <left entity type name><right entity type name>. En este ejemplo, PostTag.
    • La tabla de combinación tiene el mismo nombre que el tipo de entidad de combinación.
  • El tipo de entidad de combinación recibe una propiedad de clave externa para cada dirección de la relación. Se nombran como <navigation name><principal key name>. Por lo tanto, en este ejemplo, las propiedades de clave externa son PostsId y TagsId.
    • En el caso de una relación de varios a varios unidireccional, la propiedad de clave externa sin una navegación asociada se nombra como <principal entity type name><principal key name>.
  • Las propiedades de clave externa no admiten valores NULL, lo que hace que ambas relaciones con la entidad de combinación sean obligatorias.
    • Las convenciones de eliminación en cascada significan que estas relaciones se configurarán para la eliminación en cascada.
  • El tipo de entidad de combinación se configura con una clave principal compuesta que consta de las dos propiedades de clave externa. Por lo tanto, en este ejemplo, la clave principal se compone de PostsId y TagsId.

El resultado es el siguiente modelo de 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

Y se traduce en el siguiente esquema de base de datos cuando se usa 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 convención, EF crea un índice de base de datos para la propiedad o las propiedades de una clave externa. El tipo de índice creado viene determinado por:

  • La cardinalidad de la relación
  • Si la relación es opcional u obligatoria
  • El número de propiedades que componen la clave externa

En una relación de uno a varios, se crea un índice sencillo por convención. El mismo índice se crea para las relaciones opcionales y obligatorias. Por ejemplo, en SQLite:

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

O en SQL Server:

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

En una relación de uno a uno obligatoria, se crea un índice único. Por ejemplo, en SQLite:

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

O en SQL Sever:

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

En las relaciones de uno a uno opcionales, el índice creado en SQLite es el mismo:

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

Sin embargo, en SQL Server, se agrega un filtro IS NOT NULL para controlar mejor los valores de clave externa NULL. Por ejemplo:

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

Para las claves externas compuestas, se crea un índice que abarca todas las columnas de clave externa. Por ejemplo:

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

Nota

EF no crea índices para las propiedades que ya están cubiertas por un índice existente o una restricción de clave principal.

Impedir que EF cree índices para claves externas

Los índices tienen sobrecarga y, como se pregunta aquí, puede que no siempre sea apropiado crearlos para todas las columnas FK. Para lograrlo, se puede quitar ForeignKeyIndexConvention al compilar el modelo:

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

Cuando se desea, los índices se pueden crear explícitamente para aquellas columnas de clave externa que los necesiten.

Nombres de restricciones de clave externa

Por convención, las restricciones de clave externa se llaman FK_<dependent type name>_<principal type name>_<foreign key property name>. En el caso de las claves externas compuestas, <foreign key property name> se convierte en una lista de nombres de propiedad de clave externa separados por caracteres de subrayado.

Recursos adicionales