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 establecedor no tiene que ser público; puede ser privado o tener cualquier otra accesibilidad.
- El establecedor puede ser de solo inicialización.
- El tipo de propiedad es, o podría ser, un tipo de entidad. Esto significa que el tipo:
- Debe ser un tipo de referencia.
- No se debe haber configurado explícitamente como un tipo de propiedad primitivo.
- El proveedor de base de datos no debe asignarlo como un tipo de propiedad primitivo.
- No se debe convertir automáticamente en un tipo de propiedad primitivo asignado por el proveedor de base de datos usado.
- 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
, porqueint
es un tipo primitivo asignado.Blog.Title
, porque "string" es un tipo primitivo asignado.Blog.Uri
, porqueUri
se convierte automáticamente en un tipo primitivo asignado.Blog.ConsoleKeyInfo
, porqueConsoleKeyInfo
es un tipo de valor de C#.Blog.DefaultAuthor
, porque la propiedad no tiene un establecedor.Author.Id
, porqueGuid
es un tipo primitivo asignado.Author.Name
, porque "string" es un tipo primitivo asignado.Author.BlogId
, porqueint
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>
, dondeTEntity
es o podría ser un tipo de entidad. Esto significa que el tipo deTEntity
:- Debe ser un tipo de referencia.
- No se debe haber configurado explícitamente como un tipo de propiedad primitivo.
- El proveedor de base de datos no debe asignarlo como un tipo de propiedad primitivo.
- No se debe convertir automáticamente en un tipo de propiedad primitivo asignado por el proveedor de base de datos usado.
- 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 sonPostsId
yTagsId
.- 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>
.
- 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
- 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
yTagsId
.
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.