Cambio de las claves externas y las navegaciones

Información general sobre el cambio de las claves externas y las navegaciones

Las relaciones en un modelo Entity Framework Core (EF Core) están representadas mediante claves externas (FK, por sus siglas en inglés). Una FK consta de una o varias propiedades en la entidad dependiente o secundaria de la relación. Esta entidad dependiente o secundaria está asociada a una entidad principal o primaria concreta cuando los valores de las propiedades de clave externa en el elemento dependiente o secundario coinciden con los valores de las propiedades de la clave principal o alternativa (PK) en la entidad de seguridad o elemento primario.

Las claves externas son una buena manera de almacenar y manipular relaciones en la base de datos, pero no son muy fáciles de usar cuando se trabaja con varias entidades relacionadas en el código de la aplicación. Por lo tanto, la mayoría de los modelos de EF Core también superponen las "navegaciones" sobre la representación de FK. Las navegaciones forman referencias de C#/.NET entre instancias de entidad que reflejan las asociaciones encontradas mediante la coincidencia de valores de clave externa con los valores de clave principal o alternativo.

Las navegaciones se pueden usar en ambos lados de la relación, solo en un lado, o no en absoluto, dejando solo la propiedad FK. La propiedad FK se puede ocultar haciendo que sea una propiedad reemplazada. Consulte el artículo Relaciones para obtener más información sobre las relaciones de modelado.

Sugerencia

En este documento se da por supuesto que se comprenden los estados de entidad y los conceptos básicos del seguimiento de cambios de EF Core. Consulte Change Tracking en EF Core para obtener más información sobre estos temas.

Sugerencia

Puede ejecutar y depurar en todo el código de este documento descargando el código de ejemplo de GitHub.

Ejemplo del modelo

El modelo siguiente contiene cuatro tipos de entidad con relaciones entre ellos. Los comentarios del código indican qué propiedades son claves externas, claves principales y navegaciones.

public class Blog
{
    public int Id { get; set; } // Primary key
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Collection navigation
    public BlogAssets Assets { get; set; } // Reference navigation
}

public class BlogAssets
{
    public int Id { get; set; } // Primary key
    public byte[] Banner { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation
}

public class Post
{
    public int Id { get; set; } // Primary key
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; } // Reference navigation

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
}

public class Tag
{
    public int Id { get; set; } // Primary key
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
}

Las tres relaciones de este modelo son:

  • Cada blog puede tener muchas entradas (uno a varios):
    • Blog es el elemento principal o primario.
    • Post es el elemento dependiente o secundario. Contiene la propiedad FK Post.BlogId, cuyo valor debe coincidir con el valor PK Blog.Id del blog relacionado.
    • Post.Blog es una navegación de referencia desde una entrada al blog asociado. Post.Blog es la navegación inversa para Blog.Posts.
    • Blog.Posts es una navegación de colección de un blog a todas las entradas asociadas. Blog.Posts es la navegación inversa para Post.Blog.
  • Cada blog puede tener un recurso (uno a uno):
    • Blog es el elemento principal o primario.
    • BlogAssets es el elemento dependiente o secundario. Contiene la propiedad FK BlogAssets.BlogId, cuyo valor debe coincidir con el valor PK Blog.Id del blog relacionado.
    • BlogAssets.Blog es una navegación de referencia desde los recursos al blog asociado. BlogAssets.Blog es la navegación inversa para Blog.Assets.
    • Blog.Assets es una navegación de referencia desde el blog a los recursos asociados. Blog.Assets es la navegación inversa para BlogAssets.Blog.
  • Cada publicación puede tener muchas etiquetas, y cada etiqueta puede tener muchas publicaciones (varios a varios):
    • Las relaciones de varios a varios son una capa adicional sobre dos relaciones de uno a varios. Las relaciones de varios a varios se tratan más abajo en este documento.
    • Post.Tags es una navegación de colección de una publicación a todas las etiquetas asociadas. Post.Tags es la navegación inversa para Tag.Posts.
    • Tag.Posts es una navegación de colección de una etiqueta a todas las entradas asociadas. Tag.Posts es la navegación inversa para Post.Tags.

Consulte el artículo Relaciones para obtener más información sobre cómo modelar y configurar las relaciones.

Corrección de relaciones

EF Core mantiene las navegaciones alineadas con los valores de clave externa, y viceversa. Es decir, si un valor de clave externa cambia de modo que ahora hace referencia a una entidad principal o primaria diferente, las navegaciones se actualizan para reflejar este cambio. Lo mismo ocurre si se cambia una navegación, los valores de clave externa de las entidades implicadas se actualizan para reflejar este cambio. Esto se denomina "corrección de relaciones".

Corregir por consulta

La corrección se produce primero cuando se consultan entidades desde la base de datos. La base de datos solo tiene valores de clave externa, por lo que cuando EF Core crea una instancia de entidad a partir de la base de datos, usa los valores de clave externa para establecer las navegaciones de referencia y agregar entidades a las navegaciones de colección según corresponda. Por ejemplo, considere una consulta para blogs y sus publicaciones y recursos asociados:

using var context = new BlogsContext();

var blogs = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToList();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Para cada blog, EF Core creará primero una instancia de Blog. A continuación, a medida que cada entrada se carga desde la base de datos, su navegación de referencia Post.Blog se establece para que apunte al blog asociado. Del mismo modo, la publicación se agrega a la navegación de la colección Blog.Posts. Lo mismo sucede con BlogAssets, excepto en el caso concreto en el que ambas navegaciones son referencias. La navegación de Blog.Assets se establece para que apunte a la instancia de recursos y la navegación de BlogAsserts.Blog se establece para que apunte a la instancia de blog.

Al examinar la vista de depuración de seguimiento de cambios después de esta consulta en la que se muestran dos blogs, cada uno con un recurso y dos publicaciones que se están realizando el seguimiento:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

La vista de depuración muestra los valores de clave y las navegaciones. Las navegaciones se muestran mediante los valores de clave principal de las entidades relacionadas. Por ejemplo, Posts: [{Id: 1}, {Id: 2}] en la salida anterior indica que la navegación de la colección de Blog.Posts contiene dos entradas relacionadas con las claves principales 1 y 2 respectivamente. Del mismo modo, para cada entrada asociada al primer blog, la línea Blog: {Id: 1} indica que la navegación de Post.Blog hace referencia al blog con la clave principal 1.

Corrección de entidades con seguimiento local

La corrección de relaciones también se produce entre las entidades devueltas desde una consulta de seguimiento y las entidades ya rastreadas por DbContext. Por ejemplo, considere la posibilidad de ejecutar tres consultas independientes para blogs, publicaciones y recursos:

using var context = new BlogsContext();

var blogs = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var assets = context.Assets.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var posts = context.Posts.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Al examinar de nuevo las vistas de depuración, después de la primera consulta solo se realiza el seguimiento de los dos blogs:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: []

Las navegaciones de referencia Blog.Assets son null y las navegaciones de la colección de Blog.Posts están vacías porque actualmente no se realiza el seguimiento de entidades asociadas por el contexto.

Después de la segunda consulta, las navegaciones de referencia de Blogs.Assets se han corregido hasta que apuntan a las instancias de BlogAsset recién rastreadas. Del mismo modo, las navegaciones de referencia de BlogAssets.Blog se establecen para que apunten a la instancia de Blog ya rastreada de manera adecuada.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: []
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: []
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}

Por último, después de la tercera consulta, las navegaciones de la colección Blog.Posts ahora contienen todas las entradas relacionadas y las referencias de Post.Blog apuntan a la instancia de Blog adecuada:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: 1}
  Posts: [{Id: 1}, {Id: 2}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 1} Unchanged
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 2} Unchanged
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Este es el mismo estado final que se obtuvo con la consulta única original, ya que EF Core corrigió las navegaciones como entidades, incluso cuando provenían de varias consultas diferentes.

Nota:

La corrección nunca hace que se devuelvan más datos de la base de datos. Solo conecta las entidades que ya devuelve la consulta o que ya realiza el seguimiento de DbContext. Consulte Identity Resolution en EF Core para obtener información sobre cómo controlar duplicados al serializar entidades.

Cambio de relaciones mediante las navegaciones

La manera más fácil de cambiar la relación entre dos entidades es manipular una navegación, al tiempo que deja EF Core para corregir la navegación inversa y los valores de FK adecuadamente. Esto se puede hacer:

  • Agregando o quitando una entidad de una navegación de colección.
  • Cambiando una navegación de referencia para que apunte a una entidad diferente o establecerla en null.

Agregar o quitar de las navegaciones de recopilación

Por ejemplo, vamos a mover una de las entradas del blog de Visual Studio al blog de .NET. Esto requiere primero cargar los blogs y entradas y, a continuación, mover la entrada de la colección de navegación en un blog a la colección de navegación en el otro blog:

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");
var vsBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == "Visual Studio Blog");

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Sugerencia

Aquí se necesita una llamada a ChangeTracker.DetectChanges(), porque el acceso a la vista de depuración no provoca ninguna detección automática de cambios.

Esta es la vista de depuración impresa después de ejecutar el código anterior:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: <null>
  Posts: [{Id: 4}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}
  Tags: []
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []
Post {Id: 4} Unchanged
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

La navegación Blog.Posts en el blog de .NET ahora tiene tres entradas (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Del mismo modo, la navegación Blog.Posts en el blog de Visual Studio solo tiene una entrada (Posts: [{Id: 4}]). Esto se espera, ya que el código cambió explícitamente estas colecciones.

Y lo que es más interesante: aunque el código no cambió explícitamente la navegación Post.Blog, se ha corregido para apuntar al blog de Visual Studio (Blog: {Id: 1}). Además, el valor de la clave externa de Post.BlogId se ha actualizado para que coincida con el valor de clave principal del blog de .NET. Este cambio en el valor de FK se guarda en la base de datos cuando se llama a SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='3' (DbType = String), @p0='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

Cambio de navegaciones de referencia

En el ejemplo anterior, una entrada se movió de un blog a otro manipulando la navegación de la colección de entradas en cada blog. Lo mismo se puede lograr cambiando la navegación de referencia Post.Blog para que apunte al nuevo blog. Por ejemplo:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.Blog = dotNetBlog;

La vista de depuración después de este cambio es exactamente la misma que en el ejemplo anterior. Esto se debe a que EF Core detectó el cambio de navegación de referencia y, a continuación, corrigió las navegaciones de colección y el valor de FK para que coincidan.

Cambio de relaciones mediante valores de clave externa

En la sección anterior, las navegaciones manipularon las relaciones dejando automáticamente los valores de clave externa. Esta es la manera recomendada de manipular las relaciones en EF Core. Sin embargo, también es posible manipular los valores de FK directamente. Por ejemplo, podemos mover una entrada de un blog a otro cambiando el valor de clave externa Post.BlogId:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
post.BlogId = dotNetBlog.Id;

Observe cómo esto es muy similar a cambiar la navegación de referencia, como se muestra en el ejemplo anterior.

La vista de depuración después de este cambio vuelve a exactamente la misma que era el caso de los dos ejemplos anteriores. Esto se debe a que EF Core detectó el cambio de valor FK y, a continuación, corrigió tanto las navegaciones de referencia como de colección para que coincidan.

Sugerencia

No es necesario escribir código para manipular todas las navegaciones y los valores de FK cada vez que cambia una relación. Este código es más complicado y debe garantizar cambios coherentes en las claves externas y las navegaciones en cada caso. Si es posible, manipule únicamente una sola navegación, o quizás ambas navegaciones. Si es necesario, solo manipule los valores de FK. Evite manipular las navegaciones y los valores de FK.

Corrección de entidades agregadas o eliminadas

Agregar a una navegación de colección

EF Core realiza las siguientes acciones cuando detecta que se ha agregado una nueva entidad dependiente o secundaria a una navegación de recopilación:

  • Si no se ha hecho el seguimiento de la entidad, se procede a ello. (Normalmente, la entidad estará en el estado de Added. Sin embargo, si el tipo de entidad está configurado para usar claves generadas y se establece el valor de clave principal, se realiza el seguimiento de la entidad en el estado Unchanged).
  • Si la entidad está asociada a una entidad de seguridad o a un elemento primario diferente, entonces, esa relación se corta.
  • La entidad se asocia a la entidad principal o primaria que posee la navegación de la colección.
  • Las navegaciones y los valores de clave externa se fijan para todas las entidades implicadas.

En función de esto, podemos ver que para mover una entrada de un blog a otro no es necesario quitarla de la navegación anterior de la colección antes de agregarla a la nueva. Por lo tanto, el código del ejemplo anterior se puede cambiar desde:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);
dotNetBlog.Posts.Add(post);

Para:

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
dotNetBlog.Posts.Add(post);

EF Core ve que la entrada se ha agregado a un nuevo blog y la quita automáticamente de la colección en el primer blog.

Quitar de las navegaciones de recopilación

Al quitar una entidad dependiente o secundaria de la navegación de colección de la entidad de seguridad o elemento principal o primaria, se produce la separación de la relación con esa entidad de seguridad o elemento principal o primario. Lo que sucede a continuación depende de si la relación es opcional u obligatoria.

Relaciones opcionales

De forma predeterminada, para las relaciones opcionales, el valor de clave externa se establece en null. Esto significa que el elemento dependiente o secundario ya no está asociado a ninguna entidad de seguridad o elemento principal o primaria. Por ejemplo, vamos a cargar un blog y entradas y, a continuación, quitar una de las entradas de la navegación de la colección de Blog.Posts:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

Al examinar la vista de depuración de seguimiento de cambios después de este cambio se muestra que:

  • El FK de Post.BlogId se ha establecido en null (BlogId: <null> FK Modified Originally 1)
  • La navegación de referencia de Post.Blog se ha establecido en null (Blog: <null>)
  • La publicación se ha quitado de la navegación de la colección de Blog.Posts (Posts: [{Id: 1}])
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Observe que la publicación no está marcada como Deleted. Se marca como Modified para que el valor de FK de la base de datos se establezca en null cuando se llame a SaveChanges.

Relaciones obligatorias

No se permite establecer el valor de FK en null (y normalmente no es posible) para las relaciones necesarias. Por lo tanto, la propagación de una relación necesaria significa que la entidad dependiente o secundaria debe volver a estar primaria a una nueva entidad de seguridad o elemento principal o primario, o quitarse de la base de datos cuando se llama a SaveChanges para evitar una infracción de restricción referencial. Esto se conoce como "eliminar huérfanos" y es el comportamiento predeterminado en EF Core para las relaciones necesarias.

Por ejemplo, vamos a cambiar la relación entre blog y entradas que se van a requerir y, a continuación, ejecutar el mismo código que en el ejemplo anterior:

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

Al examinar la vista de depuración después de este cambio se muestra que:

  • La publicación se ha marcado como Deleted, de modo que se eliminará de la base de datos cuando se llame a SaveChanges.
  • La navegación de referencia de Post.Blog se ha establecido en null (Blog: <null>).
  • La publicación se ha quitado de la navegación de la colección de Blog.Posts (Posts: [{Id: 1}]).
Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: <null>
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>
  Tags: []

Tenga en cuenta que el Post.BlogId permanece sin cambios, ya que para una relación necesaria no se puede establecer en null.

Al llamar a SaveChanges, se elimina la publicación huérfana:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Eliminación de tiempo huérfanos y reorganización de la relación jerárquica

De forma predeterminada, al marcar huérfanos como Deleted, se produce tan pronto como se detecta el cambio de relación. Sin embargo, este proceso se puede retrasar hasta que se llame realmente a SaveChanges. Esto puede ser útil para evitar convertir huérfanos de entidades que se han quitado de una entidad de seguridad o elemento principal o primario, pero se volverán a crear elementos principales o primarios con una nueva entidad de seguridad o elemento primario antes de llamar a SaveChanges. ChangeTracker.DeleteOrphansTiming se usa para establecer este tiempo. Por ejemplo:

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;

var post = vsBlog.Posts.Single(e => e.Title.StartsWith("Disassembly improvements"));
vsBlog.Posts.Remove(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

dotNetBlog.Posts.Add(post);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Después de quitar la publicación de la primera colección, el objeto no está marcado como Deleted como lo estaba en el ejemplo anterior. En su lugar, EF Core realiza un seguimiento de que la relación se ha cortado aunque se trata de una relación necesaria. (EF Core considera que el valor de FK es null; aunque realmente no puede ser null, porque el tipo no admite valores null. Esto se conoce como "null conceptual").

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []

Si se llama a SaveChanges, a la vez se elimina la publicación huérfana. Sin embargo, si como en el ejemplo anterior, la publicación está asociada a un nuevo blog antes de llamar a SaveChanges, se corregirá adecuadamente en ese nuevo blog y ya no se considera un huérfano:

Post {Id: 3} Modified
  Id: 3 PK
  BlogId: 1 FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 1}
  Tags: []

SaveChanges llamado en este momento actualizará la publicación en la base de datos en lugar de eliminarla.

También es posible desactivar la eliminación automática de huérfanos. Esto producirá una excepción si se llama a SaveChanges mientras se realiza un seguimiento de un huérfano. Por ejemplo, este código:

var dotNetBlog = context.Blogs.Include(e => e.Posts).Single(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

var post = dotNetBlog.Posts.Single(e => e.Title == "Announcing F# 5");
dotNetBlog.Posts.Remove(post);

context.SaveChanges(); // Throws

Producirá esta excepción:

System.InvalidOperationException: la asociación entre las entidades 'Blog' y 'Post' con el valor de clave '{BlogId: 1}' se ha cortado, pero la relación se marca como necesaria o se requiere implícitamente porque la clave externa no admite valores null. Si se debe eliminar la entidad dependiente o secundaria cuando se ha interrumpido una relación necesaria, configure la relación para usar eliminaciones en cascada.

La eliminación de huérfanos, así como las eliminaciones en cascada, se puede forzar en cualquier momento llamando a ChangeTracker.CascadeChanges(). La combinación de esto con la configuración del tiempo de eliminación huérfano en Never garantizará que los huérfanos nunca se eliminen a menos que EF Core se indique explícitamente que lo haga.

Cambio de una navegación de referencia

Cambiar la navegación de referencia de una relación uno a varios tiene el mismo efecto que cambiar la navegación de la colección en el otro extremo de la relación. Establecer la navegación de referencia de dependiente o secundario en NULL equivale a quitar la entidad de la navegación de colección de la entidad principal o primaria. Todos los cambios de corrección y base de datos se producen como se describe en la sección anterior, incluida la realización de una entidad huérfana si se requiere la relación.

Relaciones de uno a uno opcionales

Para las relaciones uno a uno, el cambio de una navegación de referencia hace que cualquier relación anterior se interrumpa. En el caso de las relaciones opcionales, esto significa que el valor de FK en el elemento dependiente o secundario relacionado anteriormente está establecido en null. Por ejemplo:

using var context = new BlogsContext();

var dotNetBlog = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

La vista de depuración antes de llamar a SaveChanges muestra que los nuevos recursos han reemplazado a los recursos existentes, que ahora se marcan como Modified con un valor FK de BlogAssets.BlogId en null:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482629}
  Posts: []
BlogAssets {Id: -2147482629} Added
  Id: -2147482629 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Modified
  Id: 1 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 1
  Blog: <null>

Esto da como resultado una actualización y una inserción cuando se llama a SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Assets" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2=NULL, @p3='1' (Nullable = true) (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p2, @p3);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Relaciones de uno a uno obligatorias

Al ejecutar el mismo código que en el ejemplo anterior, pero esta vez con una relación uno a uno obligatoria, se muestra que la BlogAssets asociada anteriormente está marcada ahora como Deleted, ya que se convierte en un huérfano cuando el nuevo BlogAssets tiene su lugar:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Assets: {Id: -2147482639}
  Posts: []
BlogAssets {Id: -2147482639} Added
  Id: -2147482639 PK Temporary
  Banner: <null>
  BlogId: 1 FK
  Blog: {Id: 1}
BlogAssets {Id: 1} Deleted
  Id: 1 PK
  Banner: <null>
  BlogId: 1 FK
  Blog: <null>

Esto da como resultado una eliminación y una inserción cuando se llama a SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Assets"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1=NULL, @p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Assets" ("Banner", "BlogId")
VALUES (@p1, @p2);
SELECT "Id"
FROM "Assets"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

El tiempo de marcar huérfanos como eliminados se puede cambiar de la misma manera que se muestra para las navegaciones de colección y tiene los mismos efectos.

Eliminar una entidad

Relaciones opcionales

Cuando una entidad se marca como Deleted, por ejemplo llamando a DbContext.Remove, las referencias a la entidad eliminada se quitan de las navegaciones de otras entidades. Para las relaciones opcionales, los valores de FK de las entidades dependientes se establecen en null.

Por ejemplo, vamos a marcar el blog de Visual Studio como Deleted:

using var context = new BlogsContext();

var vsBlog = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Al examinar la vista de depuración de seguimiento de cambios, antes de llamar a SaveChanges muestra:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Modified
  Id: 2 PK
  Banner: <null>
  BlogId: <null> FK Modified Originally 2
  Blog: <null>
Post {Id: 3} Modified
  Id: 3 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: []
Post {Id: 4} Modified
  Id: 4 PK
  BlogId: <null> FK Modified Originally 2
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: <null>
  Tags: []

Tenga en lo siguiente:

  • El blog se marca como Deleted.
  • Los recursos relacionados con el blog eliminado tienen un valor FK null (BlogId: <null> FK Modified Originally 2) y una navegación de referencia nula (Blog: <null>)
  • Cada publicación relacionada con el blog eliminado tienen un valor FK null (BlogId: <null> FK Modified Originally 2) y una navegación de referencia nula (Blog: <null>)

Relaciones obligatorias

El comportamiento de corrección de las relaciones necesarias es el mismo que para las relaciones opcionales, excepto que las entidades dependientes o secundarias se marcan como Deleted, ya que no pueden existir sin una entidad de seguridad o elemento principal o primaria y deben quitarse de la base de datos cuando se llama a SaveChanges para evitar una excepción de restricción referencial. Esto se conoce como "eliminación en cascada" y es el comportamiento predeterminado en EF Core para las relaciones necesarias. Por ejemplo, ejecutar el mismo código que en el ejemplo anterior, pero con una relación necesaria da como resultado la siguiente vista de depuración antes de llamar a SaveChanges:

Blog {Id: 2} Deleted
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Assets: {Id: 2}
  Posts: [{Id: 3}, {Id: 4}]
BlogAssets {Id: 2} Deleted
  Id: 2 PK
  Banner: <null>
  BlogId: 2 FK
  Blog: {Id: 2}
Post {Id: 3} Deleted
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []
Post {Id: 4} Deleted
  Id: 4 PK
  BlogId: 2 FK
  Content: 'Examine when database queries were executed and measure how ...'
  Title: 'Database Profiling with Visual Studio'
  Blog: {Id: 2}
  Tags: []

Como se esperaba, los elementos dependientes o secundarios ahora se marcan como Deleted. Sin embargo, tenga en cuenta que las navegaciones de las entidades eliminadas no han cambiado. Esto puede parecer extraño, pero evita fragmentar completamente un gráfico eliminado de entidades borrando todas las navegaciones. Es decir, el blog, el recurso y las entradas siguen formando un gráfico de entidades incluso después de haber sido eliminados. Esto facilita mucho la eliminación de un gráfico de entidades de la que era el caso de EF6 en el que el grafo se ha fragmentado.

Tiempo de eliminación en cascada y reorganización de la relación jerárquica

De forma predeterminada, la eliminación en cascada se produce en cuanto el elemento primario o la entidad de seguridad se marca como Deleted. Esto es lo mismo que para eliminar huérfanos, como se ha descrito anteriormente. Al igual que con la eliminación de huérfanos, este proceso se puede retrasar hasta que se llame a SaveChanges, o incluso deshabilitarse completamente, estableciendo en ChangeTracker.CascadeDeleteTiming adecuadamente. Esto resulta útil de la misma manera que para eliminar huérfanos, incluidos los elementos secundarios o dependientes de la reorganización de la relación jerárquica después de la eliminación de una entidad de seguridad o elemento principal o primario.

La eliminación en cascada, así como las eliminaciones de huérfanos, se puede forzar en cualquier momento llamando a ChangeTracker.CascadeChanges(). La combinación de esto con la configuración del tiempo de eliminación en cascada en Never garantizará que las eliminaciones en cascadas nunca ocurran a menos que EF Core se indique explícitamente que lo haga.

Sugerencia

La eliminación en cascada y la eliminación de entidades huérfanas están estrechamente relacionadas. Ambas dan como resultado la eliminación de entidades dependientes o secundarias cuando se interrumpe su relación con la entidad de seguridad o primaria requerida. En el caso de la eliminación en cascada, esta interrupción de la relación tiene lugar porque se elimina la propia entidad de seguridad o primaria. En el caso de las entidades huérfanas, la entidad de seguridad o primaria sigue existiendo, pero ya no está relacionada con las entidades dependientes o secundarias.

Relaciones de varios a varios

Las relaciones de varios a varios en EF Core se implementan mediante una entidad de combinación. Cada lado de la relación de varios a varios está relacionada con esta entidad de combinación con una relación uno a varios. Esta entidad de combinación se puede definir y asignar explícitamente, o bien se puede crear implícita y oculta. En ambos casos, el comportamiento subyacente es el mismo. Examinaremos este comportamiento subyacente en primer lugar para comprender cómo funciona el seguimiento de las relaciones de varios a varios.

Cómo trabajar con relaciones de varios a varios

Considere este modelo de EF Core que crea una relación de varios a varios entre publicaciones y etiquetas mediante un tipo de entidad de combinación definido explícitamente:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Observe que el tipo de entidad de combinación PostTag contiene dos propiedades de clave externa. En este modelo, para que una publicación esté relacionada con una etiqueta, debe haber una entidad de combinación PostTag donde el valor de clave externa PostTag.PostId coincida con el valor de clave principal Post.Id y donde el valor de clave externa PostTag.TagId coincide con el valor de clave principal Tag.Id. Por ejemplo:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Al examinar el vista de depuración de seguimiento de cambios después de ejecutar este código se muestra que la publicación y la etiqueta están relacionadas con la nueva entidad de combinación de PostTag:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]

Tenga en cuenta que las navegaciones de la colección en Post y Tag se han corregido, ya que tienen las navegaciones de referencia en PostTag. Estas relaciones se pueden manipular mediante navegaciones en lugar de valores de FK, igual que en todos los ejemplos anteriores. Por ejemplo, el código anterior se puede modificar para agregar la relación estableciendo las navegaciones de referencia en la entidad de combinación:

context.Add(new PostTag { Post = post, Tag = tag });

Esto da como resultado exactamente el mismo cambio en los FK y las navegaciones que en el ejemplo anterior.

Omitir navegaciones

La manipulación manual de la tabla de combinación puede resultar complicada. Las relaciones de varios a varios se pueden manipular directamente mediante navegaciones de colección especiales que "omiten" la entidad de combinación. Por ejemplo, se pueden agregar dos navegaciones de omisión al modelo anterior; una de Post a Tags y la otra de Etiqueta a Publicaciones:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }

    public IList<Tag> Tags { get; } = new List<Tag>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }

    public IList<Post> Posts { get; } = new List<Post>(); // Skip collection navigation
    public IList<PostTag> PostTags { get; } = new List<PostTag>(); // Collection navigation
}

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public Post Post { get; set; } // Reference navigation
    public Tag Tag { get; set; } // Reference navigation
}

Esta relación de varios a varios requiere la siguiente configuración para asegurarse de que las navegaciones de omisión y las navegaciones normales se usan para la misma relación de varios a varios:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne(t => t.Tag).WithMany(p => p.PostTags),
            j => j.HasOne(t => t.Post).WithMany(p => p.PostTags));
}

Consulte el artículo Relaciones para obtener más información sobre las relaciones de varios a varios.

Omita las navegaciones y se comporte como las navegaciones de recopilación normales. Sin embargo, la forma en que funcionan con valores de clave externa es diferente. Vamos a asociar una publicación con una etiqueta, pero esta vez mediante una navegación de omisión:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Tenga en cuenta que este código no usa la entidad de combinación. En su lugar, simplemente agrega una entidad a una colección de navegación de la misma manera que se haría si se tratase de una relación uno a varios. La vista de depuración resultante es básicamente la misma que antes:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  PostTags: [{PostId: 3, TagId: 1}]
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Added
  PostId: 3 PK FK
  TagId: 1 PK FK
  Post: {Id: 3}
  Tag: {Id: 1}
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  PostTags: [{PostId: 3, TagId: 1}]
  Posts: [{Id: 3}]

Observe que se creó automáticamente una instancia de la entidad de combinación de PostTag con los valores de FK establecidos en los valores PK de la etiqueta y publicación que ahora están asociados. Todas las navegaciones de referencia y colección normales se han corregido para que coincidan con estos valores de FK. Además, dado que este modelo contiene navegaciones de omisión, también se han corregido. En concreto, aunque agregamos la etiqueta a la navegación de omisión de Post.Tags, también se ha corregido la navegación inversa de omisión de Tag.Posts en el otro lado de esta relación para contener la publicación asociada.

Vale la pena tener en cuenta que las relaciones subyacentes de varios a varios se pueden manipular directamente incluso cuando se han superpuesta las navegaciones de omisión. Por ejemplo, la etiqueta y Post podrían asociarse como hicimos antes de introducir las navegaciones de omisión:

context.Add(new PostTag { Post = post, Tag = tag });

O bien, mediante valores de FK:

context.Add(new PostTag { PostId = post.Id, TagId = tag.Id });

Esto seguirá provocando que las navegaciones de omisión se corrigieron correctamente, lo que dará como resultado la misma salida de vista de depuración que en el ejemplo anterior.

Omitir solo navegaciones

En la sección anterior agregamos las navegaciones de omitir además de definir completamente las dos relaciones subyacentes de uno a varios. Esto resulta útil para ilustrar lo que sucede con los valores de FK, pero a menudo no es necesario. En su lugar, la relación de varios a varios se puede definir mediante omitir solo las navegaciones. Esta es la forma en que se define la relación de varios a varios en el modelo en la parte superior de este documento. Con este modelo, podemos asociar de nuevo una publicación y una etiqueta agregando una publicación a la navegación de omisión de Tag.Posts (o, como alternativa, agregando una etiqueta a la navegación de omisión de Post.Tags):

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Al examinar la vista de depuración después de realizar este cambio se revela que EF Core ha creado una instancia de Dictionary<string, object> para representar la entidad de combinación. Esta entidad de combinación contiene tanto propiedades de clave externa PostsId como TagsId, que se han establecido para que coincidan con los valores PK de la publicación y la etiqueta asociadas.

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 1} Added
  PostsId: 3 PK FK
  TagsId: 1 PK FK

Consulte Relaciones para obtener más información sobre las entidades de combinación implícitas y el uso de tipos de entidad de Dictionary<string, object>.

Importante

El tipo CLR que se usa para combinar tipos de entidad de combinación puede cambiar en versiones futuras para mejorar el rendimiento. No dependa de que el tipo de combinación sea Dictionary<string, object> a menos que se haya configurado explícitamente.

Unión de entidades con cargas

Hasta ahora, todos los ejemplos han usado un tipo de entidad de combinación (ya sea explícito o implícito) que contiene solo las dos propiedades de clave externa necesarias para la relación de varios a varios. La aplicación no debe establecer explícitamente ninguno de estos valores de FK al manipular las relaciones porque sus valores proceden de las propiedades de clave principal de las entidades relacionadas. Esto permite a EF Core crear instancias de la entidad de combinación sin que falten datos.

Cargas con valores generados

EF Core admite la adición de propiedades adicionales al tipo de entidad de combinación. Esto se conoce como dar a la entidad de combinación una "carga". Por ejemplo, vamos a agregar una propiedad TaggedOn a la entidad de combinación PostTag:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Payload
}

Esta propiedad de carga no se establecerá cuando EF Core cree una instancia de entidad de combinación. La manera más común de tratar con esto es usar propiedades de carga con valores generados automáticamente. Por ejemplo, la propiedad TaggedOn se puede configurar para usar una marca de tiempo generada por el almacén cuando se inserta cada nueva entidad:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<PostTag>(
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany(),
            j => j.Property(e => e.TaggedOn).HasDefaultValueSql("CURRENT_TIMESTAMP"));
}

Ahora, una publicación se puede etiquetar de la misma manera que antes:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Al examinar la vista de depuración de seguimiento de cambios después de llamar a SaveChanges, muestra que la propiedad de carga se ha establecido correctamente:

Post {Id: 3} Unchanged
  Id: 3 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: <null>
  Tags: [{Id: 1}]
PostTag {PostId: 3, TagId: 1} Unchanged
  PostId: 3 PK FK
  TagId: 1 PK FK
  TaggedOn: '12/29/2020 8:13:21 PM'
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 3}]

Establecer explícitamente valores de carga

A continuación del ejemplo anterior, vamos a agregar una propiedad de carga que no usa un valor generado automáticamente:

public class PostTag
{
    public int PostId { get; set; } // First part of composite PK; FK to Post
    public int TagId { get; set; } // Second part of composite PK; FK to Tag

    public DateTime TaggedOn { get; set; } // Auto-generated payload property
    public string TaggedBy { get; set; } // Not-generated payload property
}

Ahora se puede etiquetar una publicación de la misma manera que antes y la entidad de combinación se seguirá creando automáticamente. A continuación, se puede acceder a esta entidad mediante uno de los mecanismos descritos en Acceso a entidades con seguimiento. Por ejemplo, el código siguiente usa DbSet<TEntity>.Find para acceder a la instancia de entidad de combinación:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

var joinEntity = context.Set<PostTag>().Find(post.Id, tag.Id);

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Una vez que se haya localizado la entidad de combinación, se puede manipular de forma normal, para establecer la propiedad de carga de TaggedBy antes de llamar a SaveChanges.

Nota:

Tenga en cuenta que se requiere una llamada a ChangeTracker.DetectChanges() aquí para dar a EF Core la oportunidad de detectar el cambio de propiedad de navegación y crear la instancia de entidad de combinación antes de que se use Find. Consulte Detección y notificaciones de cambios para obtener más información.

Como alternativa, la entidad de combinación se puede crear explícitamente para asociar una publicación a una etiqueta. Por ejemplo:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

context.Add(
    new PostTag { PostId = post.Id, TagId = tag.Id, TaggedBy = "ajcvickers" });

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Por último, otra manera de establecer los datos de carga es invalidar SaveChanges o usar el evento DbContext.SavingChanges para procesar entidades antes de actualizar la base de datos. Por ejemplo:

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return base.SaveChanges();
}