Compartir a través de


Cambio de claves externas y navegación

Información general sobre las claves externas y las navegaciones

Las relaciones en un modelo de Entity Framework Core (EF Core) se representan mediante claves externas (FK). Un 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 determinada cuando los valores de las propiedades de clave externa en el elemento dependiente o secundario coinciden con los valores de las propiedades de 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 en C#/.NET entre instancias de entidad que reflejan las asociaciones encontradas por coincidencia de valores de clave externa con los valores de clave principal o clave alternativa.

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 sombra. Consulte Relaciones para obtener más información sobre el modelado de relaciones.

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 todo el código de este documento descargando el código de ejemplo de GitHub.

Modelo de ejemplo

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

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 principal o el principal (padre).
    • Post es el elemento dependiente o hijo. 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 para recoger todas las entradas relacionadas desde un blog. Blog.Posts es la navegación inversa para Post.Blog.
  • Cada blog puede tener un recurso (uno a uno):
    • Blog es el principal o el principal (padre).
    • BlogAssets es el elemento dependiente o hijo. 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 (uno a uno).
    • Las relaciones de varios a varios son un nivel adicional sobre dos relaciones de uno a varios. Las relaciones de muchos a muchos se abordarán más adelante 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 publicaciones asociadas. Tag.Posts es la navegación inversa para Post.Tags.

Consulte Relaciones para obtener más información sobre cómo modelar y configurar relaciones.

Ajuste 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. Del mismo modo, 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".

Ajuste mediante 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

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

Para cada blog, EF Core creará primero una Blog instancia. A continuación, a medida que cada entrada se carga desde la base de datos, su Post.Blog navegación de referencia 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 este caso, ambas navegaciones son referencias. La navegación Blog.Assets está configurada para apuntar a la instancia de recursos, y la navegación BlogAsserts.Blog está configurada para apuntar a la instancia del blog.

Al examinar la vista de depuración de seguimiento de cambios después de esta consulta se muestran dos blogs, cada uno con un recurso y dos publicaciones a las que se realiza 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 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 Blog.Posts contiene dos publicaciones relacionadas con las claves principales 1 y 2, respectivamente. Del mismo modo, para cada entrada asociada al primer blog, la Blog: {Id: 1} línea indica que la Post.Blog navegación hace referencia al blog con la clave principal 1.

Corrección de entidades rastreadas localmente

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

using var context = new BlogsContext();

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

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

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

Al examinar de nuevo las vistas de depuración, después de la primera consulta solo se rastrean 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 Blog.Assets navegaciones de referencia son nulas y las Blog.Posts navegaciones de colección están vacías porque actualmente no se realiza el seguimiento de entidades asociadas por el contexto.

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

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 Post.Blog apuntan a la instancia 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 logró con la consulta única original, ya que EF Core ajustó las navegaciones mientras las entidades eran rastreadas, 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 son devueltas por la consulta o que ya son rastreadas por el DbContext. Consulte Identity Resolution in EF Core (Resolución de identidades 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 mediante:

  • Agregar o quitar una entidad de una navegación de colección.
  • Cambiar 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 = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(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);

await context.SaveChangesAsync();

Sugerencia

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

Esta es la vista de depuración mostrada después de ejecutar el código de arriba.

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 Blog.Posts navegación en el blog de .NET ahora tiene tres entradas (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Del mismo modo, la Blog.Posts navegación 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.

Más interesantemente, aunque el código no cambió explícitamente la Post.Blog navegación, se ha corregido para apuntar al blog de Visual Studio (Blog: {Id: 1}). Además, el Post.BlogId valor de clave externa se ha actualizado para que coincida con el valor de clave principal del blog de .NET. Este cambio en el valor de FK en se conserva 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 las 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. Se puede lograr lo mismo 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 relaciones se manipularon a través de las navegaciones, dejando que los valores de clave externa se actualicen automáticamente. 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 Post.BlogId clave externa:

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 es exactamente igual que en el caso de los dos ejemplos anteriores. Esto se debe a que EF Core detectó el cambio de valor de FK y, a continuación, corrigió las navegaciones de referencia y colección para que coincidan.

Sugerencia

No escriba 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 solo una navegación o quizás ambas. Si es necesario, solo manipule los valores de FK. Evite manipular las navegaciones y los valores de FK.

Ajuste de entidades agregadas o eliminadas

Agregar a la navegación de una 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 realiza el seguimiento de la entidad, se realiza el seguimiento. (Normalmente, la entidad estará en estado 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 Unchanged estado).
  • Si la entidad está asociada a un principal o padre diferente, entonces esa relación se rompe.
  • 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 de:

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

A:

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 una navegación de colección

Al quitar una entidad dependiente o secundaria de la navegación de la colección de la entidad principal o primaria, ocasiona la ruptura de la relación con esa entidad principal o primaria. Lo que sucede a continuación depende de si la relación es opcional o necesaria.

Relaciones opcionales

De forma predeterminada, para las relaciones opcionales, el valor de clave externa se establece en NULL. Esto significa que el dependiente/secundario ya no está relacionado con ningún principal/elemento primario. Por ejemplo, vamos a cargar un blog y entradas y, a continuación, quitar una de las entradas de la navegación de la Blog.Posts colección:

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 observa que:

  • La Post.BlogId FK se ha establecido en nulo (BlogId: <null> FK Modified Originally 1)
  • La Post.Blog navegación de referencia se ha establecido en NULL (Blog: <null>)
  • La publicación se ha quitado de la navegación de la colección 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 necesarias

No se permite establecer el valor de FK en NULL (y normalmente no es posible) para las relaciones necesarias. Por lo tanto, la ruptura de una relación necesaria significa que la entidad dependiente o secundaria debe ser asignada a un nuevo principal/padre, o eliminada de la base de datos cuando se llama a SaveChanges, para evitar una violació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, ejecute 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 manera que se eliminará de la base de datos cuando se llame a SaveChanges.
  • La Post.Blog navegación de referencia se ha establecido en NULL (Blog: <null>).
  • La publicación se ha eliminado de la navegación de colección 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: []

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

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 re-parenting

De forma predeterminada, marcar huérfanos como Deleted sucede en cuanto 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 en huérfanos entidades que se han quitado de un principal/padre, pero serán reasociados con un nuevo principal/padre 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);

await context.SaveChangesAsync();

Después de quitar la publicación de la primera colección, el objeto no está marcado como Deleted en el ejemplo anterior. En su lugar, EF Core está realizando un seguimiento de que la relación se ha severado 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 "conceptual 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: []

Al ejecutar SaveChanges ahora, se eliminaría 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 vinculará adecuadamente con ese nuevo blog y ya no se considerará 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 = await context.Blogs.Include(e => e.Posts).SingleAsync(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);

await context.SaveChangesAsync(); // Throws

Lanzará esta excepción:

System.InvalidOperationException: La asociación entre las entidades 'Blog' y 'Post' con el valor de clave '{BlogId: 1}' ha sido interrumpida, pero la relación está marcada como necesaria o se requiere implícitamente porque la clave externa no puede admitir valores NULL. Si se debe eliminar la entidad dependiente o secundaria cuando se ha severado una relación necesaria, configure la relación para usar eliminaciones en cascada.

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

Cambio de una navegación de referencia

Cambiar la navegación de referencia en una relación de uno a varios tiene el mismo efecto que modificar la navegación de la colección en el otro lado 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 ajustes y cambios en la base de datos se producen como se describe en la sección anterior, incluida la posibilidad de convertir la entidad en huérfana si se requiere la relación.

Relaciones uno a uno opcionales

Para las relaciones uno a uno, el cambio de una navegación de referencia provoca que cualquier relación previa se rompa. 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 = await context.Blogs.Include(e => e.Assets).SingleAsync(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

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

await context.SaveChangesAsync();

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 nulo BlogAssets.BlogId :

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 necesarias

Al ejecutar el mismo código que en el ejemplo anterior, pero esta vez con una relación uno a uno necesaria, se muestra que el BlogAssets previamente asociado ahora está marcado como Deleted, ya que se convierte en un huérfano cuando el nuevo BlogAssets toma 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>

A continuación, se produce 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 momento en el que se marcan los huérfanos como eliminados se puede modificar de manera similar a como se indica para las navegaciones en colecciones y tiene los mismos efectos.

Eliminación de 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .SingleAsync(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

await context.SaveChangesAsync();

Al examinar la vista de depuración de seguimiento de cambios antes de llamar a SaveChanges se 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 está marcado como Deleted.
  • Los recursos relacionados con el blog eliminado tienen un valor FK nulo (BlogId: <null> FK Modified Originally 2) y una navegación de referencia nula (Blog: <null>)
  • Cada entrada relacionada con el blog eliminado tiene un valor FK nulo (BlogId: <null> FK Modified Originally 2) y una navegación de referencia nula (Blog: <null>)

Relaciones necesarias

El comportamiento de corrección de las relaciones necesarias es el mismo que para las relaciones opcionales, excepto que las entidades dependientes/hijo se marcan como Deleted ya que no pueden existir sin una entidad principal/madre y deben quitarse de la base de datos cuando se llame 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, las personas dependientes/niños ahora están marcadas 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.

Sincronización de eliminación en cascada y reasignación de padre

De forma predeterminada, la eliminación en cascada se produce en cuanto la entidad principal 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 ChangeTracker.CascadeDeleteTiming correctamente. Esto resulta útil de la misma manera que para eliminar huérfanos, incluidos los hijos o dependientes al reasignar su relación parental tras la eliminación de un principal/padre.

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

Sugerencia

La eliminación en cascada y la eliminación de huérfanos están estrechamente relacionadas. Ambos dan lugar a la eliminación de entidades dependientes o secundarias cuando se rompe la relación con su entidad principal o primaria requerida. Para la eliminación en cascada, esta separación se produce porque la entidad principal o padre se elimina. En el caso de los huérfanos, la entidad principal 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 unió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 se puede crear implícitamente y ocultamente. En ambos casos, el comportamiento subyacente es el mismo. Examinaremos primero este comportamiento subyacente para comprender cómo funciona el seguimiento de las relaciones de muchos a muchos.

¿Cómo funcionan las relaciones de muchos a muchos?

Considere este modelo de EF Core que crea una relación de muchos a muchos entre entradas y etiquetas mediante un tipo de entidad de interconexió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 PostTag tipo de entidad de combinación 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 PostTag.PostId valor de clave externa coincida con el Post.Id valor de clave principal y donde el PostTag.TagId valor de clave externa coincida con el valor de Tag.Id clave principal. Por ejemplo:

using var context = new BlogsContext();

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

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

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

Al examinar la vista de depuración de seguimiento de cambios después de ejecutar este código, se observa que la publicación y la etiqueta están relacionadas mediante la nueva PostTag entidad de unión.

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}]

Observe que se han corregido las navegaciones de la colección en Post y Tag, así como 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 las 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 añadir dos enlaces para saltar al modelo anterior: uno de Publicación a Etiquetas, y el otro 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 Relaciones para obtener más información sobre la asignación de relaciones de muchos a muchos.

Las navegaciones de salto parecen y se comportan como las navegaciones de colecció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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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 join. 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 PostTag entidad de combinación con los valores de FK asignados a los valores PK de la etiqueta y la publicación que ahora están vinculado. 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 enlaces de navegación omitidos, estos también han sido corregidos. En concreto, aunque agregamos la etiqueta a la Post.Tags navegación de omisión, la Tag.Posts navegación inversa de omisión en el otro lado de esta relación también ha sido corregida para que contenga la publicación asociada.

Vale la pena considerar que las relaciones subyacentes de varios a varios todavía se pueden manipular directamente incluso cuando se han superpuesto las navegaciones de elusió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 salto se corrijan correctamente, y dará como resultado la misma salida de vista de depuración que en el ejemplo anterior.

Omitir solo las navegaciones

En la sección anterior, se añadieron las navegaciones de salto 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 solo omitir las navegaciones. Esta es la forma en que se define la relación de muchos a muchos en el modelo en la parte superior de este documento. Con este modelo, podemos asociar de nuevo un Post y una Tag agregando una publicación a la Tag.Posts navegación de salto (o, alternativamente, agregando una Tag a la Post.Tags navegación de salto):

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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 las propiedades de clave externa PostsId y TagsId que se han configurado para coincidir con los valores de clave primaria de la publicación y la etiqueta que están 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 Dictionary<string, object>.

Importante

El tipo CLR que se usa para combinar tipos de entidad por convenció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 muchos a muchos. 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 útil". Por ejemplo, vamos a agregar la propiedad TaggedOn a la entidad de unió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 útil no se asignará cuando EF Core cree una instancia de entidad de unión. La manera más común de tratar con esto es usar propiedades de carga con valores generados automáticamente. Por ejemplo, la TaggedOn propiedad 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

await context.SaveChangesAsync();

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

Al examinar la vista de depuración del rastreador de cambios después de llamar a SaveChanges, se muestra que la propiedad de payload 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 unió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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

await context.SaveChangesAsync();

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

Una vez que se ha localizado la entidad de combinación, se puede manipular de la manera normal: en este ejemplo, para establecer la TaggedBy propiedad de carga antes de llamar a SaveChanges.

Nota:

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

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

using var context = new BlogsContext();

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

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

await context.SaveChangesAsync();

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

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

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>())
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
        }
    }

    return await base.SaveChangesAsync(cancellationToken);
}