Compartilhar via


Alterando chaves estrangeiras e navegações

Visão geral de chaves estrangeiras e navegações

As relações em um modelo do EF Core (Entity Framework Core) são representadas usando FKs (chaves estrangeiras). Um FK consiste em uma ou mais propriedades na entidade dependente ou filho na relação. Essa entidade dependente/filho está associada a uma entidade principal/pai específica quando os valores das propriedades de chave estrangeira do dependente/filho correspondem aos valores das propriedades de chave alternativa ou chave primária (PK) na entidade principal/pai.

Chaves estrangeiras são uma boa maneira de armazenar e manipular relações no banco de dados, mas não são muito amigáveis ao trabalhar com várias entidades relacionadas no código do aplicativo. Portanto, a maioria dos modelos do EF Core também sobrepõem-se às "navegações" na representação FK. As navegações formam referências C#/.NET entre instâncias de entidade que refletem as associações encontradas pela correspondência de valores de chave estrangeira com valores de chave primária ou alternativos.

As navegaçãos podem ser usadas em ambos os lados da relação, apenas de um lado ou não, deixando apenas a propriedade FK. A propriedade FK pode ser ocultada tornando-a uma propriedade de sombra. Consulte Relações para obter mais informações sobre relações de modelagem.

Dica

Este documento pressupõe que os estados de entidade e as noções básicas do controle de alterações do EF Core sejam compreendidos. Consulte Controle de Alterações no EF Core para obter mais informações sobre esses tópicos.

Dica

Você pode executar e depurar todo o código neste documento baixando o código de exemplo do GitHub.

Modelo de exemplo

O modelo a seguir contém quatro tipos de entidade com relações entre eles. Os comentários no código indicam quais propriedades são chaves estrangeiras, chaves primárias e navegação.

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
}

As três relações neste modelo são:

  • Cada blog pode ter muitas postagens (um para muitos):
    • Blog é o principal/pai.
    • Post é o dependente/filho. Ele contém a propriedade Post.BlogId FK, cujo valor deve corresponder ao valor Blog.Id PK do blog relacionado.
    • Post.Blog é uma navegação de referência de uma postagem para o blog associado. Post.Blog é a navegação inversa para Blog.Posts.
    • Blog.Posts é uma navegação que conecta um blog a todas as postagens associadas. Blog.Posts é a navegação inversa para Post.Blog.
  • Cada blog pode ter um ativo (um para um):
    • Blog é o principal/pai.
    • BlogAssets é o dependente/filho. Ele contém a propriedade BlogAssets.BlogId FK, cujo valor deve corresponder ao valor Blog.Id PK do blog relacionado.
    • BlogAssets.Blog é uma navegação de referência dos ativos para o blog associado. BlogAssets.Blog é a navegação inversa para Blog.Assets.
    • Blog.Assets é uma navegação de referência do blog para os ativos associados. Blog.Assets é a navegação inversa para BlogAssets.Blog.
  • Cada postagem pode ter muitas etiquetas e cada etiqueta pode ter muitas postagens (muitos para muitos):
    • Relações muitos-a-muitos são uma camada adicional sobre duas relações um-para-muitos. Relações de muitos para muitos são abordadas posteriormente neste documento.
    • Post.Tags é uma navegação de uma postagem para todas as etiquetas associadas. Post.Tags é a navegação inversa para Tag.Posts.
    • Tag.Posts é uma navegação de coleção de uma etiqueta para todas as postagens associadas. Tag.Posts é a navegação inversa para Post.Tags.

Consulte Relações para obter mais informações sobre como modelar e configurar relações.

Correção de relação

O EF Core mantém as navegações alinhadas com os valores de chave estrangeira e vice-versa. Ou seja, se um valor de chave estrangeira for alterado de modo que agora se refira a uma entidade principal/pai diferente, as navegaçãos serão atualizadas para refletir essa alteração. Da mesma forma, se uma navegação for alterada, os valores de chave estrangeira das entidades envolvidas serão atualizados para refletir essa alteração. Isso é chamado de "ajuste de relacionamento".

Ajuste por consulta

A correção acontece primeiro quando as entidades são consultadas no banco de dados. O banco de dados tem apenas valores de chave estrangeira, portanto, quando o EF Core cria uma instância de entidade do banco de dados, ele usa os valores de chave estrangeira para definir as navegaçãos de referência e adicionar entidades às navegaçãos de coleção conforme apropriado. Por exemplo, considere uma consulta para blogs e suas postagens e ativos associados:

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, o EF Core criará primeiro uma Blog instância. Em seguida, à medida que cada postagem é carregada do banco de dados, sua Post.Blog navegação de referência é definida para apontar para o blog associado. Da mesma forma, a postagem é adicionada à navegação da Blog.Posts coleção. A mesma coisa acontece com BlogAssets, exceto neste caso, quando ambas as navegações são referências. A Blog.Assets navegação é definida para apontar para a instância de ativos e a BlogAsserts.Blog navegação é definida para apontar para a instância do blog.

Examinar a exibição de depuração do rastreador de alterações após essa consulta mostra dois blogs, cada um com um asset e duas postagens sendo rastreadas:

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: []

A exibição de depuração mostra os valores de chave e as navegações. As navegações são exibidas usando os valores das chaves primárias das entidades relacionadas. Por exemplo, Posts: [{Id: 1}, {Id: 2}] na saída acima indica que a navegação da coleção Blog.Posts contém duas postagens relacionadas com as chaves primárias 1 e 2, respectivamente. Da mesma forma, para cada postagem associada ao primeiro blog, a Blog: {Id: 1} linha indica que a Post.Blog navegação faz referência ao Blog com a chave primária 1.

Correção para entidades monitoradas localmente

O ajuste de relações também ocorre entre entidades retornadas de uma consulta de acompanhamento e entidades já gerenciadas pelo DbContext. Por exemplo, considere a execução de três consultas separadas para blogs, postagens e ativos:

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

Olhando novamente para as exibições de depuração, após a primeira consulta, somente os dois blogs são acompanhados:

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: []

As Blog.Assets navegações de referência são nulas e as navegações de Blog.Posts coleção estão vazias porque nenhuma entidade associada está sendo rastreada pelo contexto.

Após a segunda consulta, as Blogs.Assets navegações de referência foram corrigidas para apontar para as novas instâncias rastreadas BlogAsset. Da mesma forma, as BlogAssets.Blog navegações de referência são definidas para apontar para a instância Blog já rastreada apropriada.

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 fim, após a terceira consulta, as navegações da Blog.Posts coleção agora contêm todas as postagens relacionadas e as Post.Blog referências apontam para a instância apropriada Blog:

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: []

Esse é o mesmo estado final que foi alcançado com a consulta única original, já que o EF Core ajustou as navegações à medida que as entidades eram rastreadas, mesmo quando provenientes de várias consultas diferentes.

Observação

A correção nunca faz com que mais dados sejam retornados do banco de dados. Ele só conecta entidades que já são retornadas pela consulta ou já acompanhadas pelo DbContext. Consulte Resolução de Identidade no EF Core para obter informações sobre como lidar com duplicatas ao serializar entidades.

Alterando relações usando navegações

A maneira mais fácil de alterar a relação entre duas entidades é manipulando uma navegação, deixando o EF Core para corrigir adequadamente a navegação inversa e os valores FK. Isso pode ser feito das seguintes maneiras:

  • Adicionando ou removendo uma entidade de uma navegação de coleção.
  • Alterar uma navegação de referência para apontar para uma entidade diferente ou defini-la como nula.

Adicionando ou removendo das navegações de coleções

Por exemplo, vamos mover uma das postagens do blog do Visual Studio para o blog do .NET. Isso requer primeiro carregar os blogs e postagens e, em seguida, mover a postagem da coleção de navegação em um blog para a coleção de navegação no outro 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();

Dica

Aqui, é necessária uma chamada para ChangeTracker.DetectChanges() porque acessar a exibição de depuração não causa a detecção automática de alterações.

Esta é a visualização de depuração impressa depois de executar o código acima.

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: []

A Blog.Posts navegação no Blog do .NET agora tem três postagens (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Da mesma forma, a Blog.Posts navegação no blog do Visual Studio tem apenas uma postagem (Posts: [{Id: 4}]). Isso deve ser esperado, pois o código alterou explicitamente essas coleções.

Mais interessante, mesmo que o código não tenha alterado explicitamente a Post.Blog navegação, ele foi corrigido para apontar para o blog do Visual Studio (Blog: {Id: 1}). Além disso, o valor da Post.BlogId chave estrangeira foi atualizado para corresponder ao valor da chave primária do blog .NET. Essa alteração no valor do FK é então mantida no banco de dados quando SaveChanges é chamado:

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

Alterando as navegaçãos de referência

No exemplo anterior, uma postagem foi movida de um blog para outro manipulando a navegação de coleção de postagens em cada blog. A mesma coisa pode ser obtida alterando a Post.Blog navegação de referência para apontar para o novo blog. Por exemplo:

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

A exibição de depuração após essa alteração é exatamente a mesma do exemplo anterior. Isso ocorre porque o EF Core detectou a alteração de navegação de referência e, em seguida, corrigiu as navegaçãos de coleção e o valor FK para corresponder.

Alterando relações usando valores de chave estrangeira

Na seção anterior, as relações eram manipuladas por navegações, deixando os valores das chaves estrangeiras para serem atualizados automaticamente. Essa é a maneira recomendada de manipular relações no EF Core. No entanto, também é possível manipular valores FK diretamente. Por exemplo, podemos mover uma postagem de um blog para outro alterando o valor da Post.BlogId chave estrangeira:

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

Observe como isso é muito semelhante à alteração da navegação de referência, conforme mostrado no exemplo anterior.

A exibição de depuração após essa alteração é novamente exatamente igual ao caso dos dois exemplos anteriores. Isso ocorre porque o EF Core detectou a alteração do valor do FK e, em seguida, corrigiu as navegaçãos de referência e coleção para corresponder.

Dica

Não escreva código para manipular todas as navegações e valores de FK cada vez que uma relação muda. Esse código é mais complicado e deve garantir alterações consistentes em chaves e navegações estrangeiras em todos os casos. Se possível, basta manipular uma única navegação ou talvez ambas as navegaçãos. Se necessário, basta manipular valores FK. Evite manipular as navegaçãos e os valores do FK.

Ajuste para entidades adicionadas ou excluídas

Adicionando a uma navegação de coleção

O EF Core executa as seguintes ações quando detecta que uma nova entidade dependente/filho foi adicionada a uma navegação de coleção:

  • Se a entidade não for rastreada, ela será rastreada. (A entidade geralmente estará no Added estado. No entanto, se o tipo de entidade estiver configurado para usar chaves geradas e o valor da chave primária estiver definido, a entidade será controlada no Unchanged estado.)
  • Se a entidade estiver associada a um principal ou pai diferente, essa relação será rompida.
  • A entidade torna-se associada ao principal/mãe que possui a navegação da coleção.
  • As navegações e os valores de chave estrangeira são ajustados para todas as entidades envolvidas.

Com base nisso, podemos ver que, para mover uma postagem de um blog para outro, não precisamos removê-la da navegação de coleção antiga antes de adicioná-la à nova. Portanto, o código do exemplo acima pode ser alterado de:

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

O EF Core vê que a postagem foi adicionada a um novo blog e a remove automaticamente da coleção no primeiro blog.

Removendo de uma navegação de coleção

Remover uma entidade dependente/filho da navegação da coleção do principal/pai causa a separação da relação com essa entidade/pai. O que acontece a seguir depende se a relação é opcional ou necessária.

Relações opcionais

Por padrão, para relações opcionais, o valor da chave estrangeira é definido como nulo. Isso significa que o dependente/filho não está mais associado a nenhum principal/pai. Por exemplo, vamos carregar um blog e postagens e remover uma das postagens da navegação da Blog.Posts coleção:

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

Examinar a exibição de depuração do controle de alterações após essa alteração mostra que:

  • O Post.BlogId FK foi definido como nulo (BlogId: <null> FK Modified Originally 1)
  • A Post.Blog navegação de referência foi definida como nula (Blog: <null>)
  • A postagem foi removida da navegação da coleção 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 a postagem não está marcada como Deleted. Ele é marcado como Modified para que o valor do FK no banco de dados seja definido como nulo quando SaveChanges for chamado.

Relações necessárias

A definição do valor FK como nulo não é permitida (e geralmente não é possível) para as relações necessárias. Portanto, cortar uma relação necessária significa que a entidade dependente/filho deve ser atribuída a um novo principal/pai ou removida do banco de dados quando SaveChanges é chamado, para evitar uma violação de restrição referencial. Isso é conhecido como "excluir órfãos" e é o comportamento padrão no EF Core para as relações necessárias.

Por exemplo, vamos alterar a relação entre o blog e as postagens a serem necessárias e, em seguida, executar o mesmo código que no exemplo anterior:

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

Examinar a exibição de depuração após essa alteração mostra que:

  • A postagem foi marcada como Deleted de forma que será excluída do banco de dados quando SaveChanges for chamado.
  • A Post.Blog navegação de referência foi definida como nula (Blog: <null>).
  • A postagem foi removida da navegação de Blog.Posts coleção (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 o Post.BlogId restante permanece inalterado, pois para uma relação necessária ele não pode ser definido como nulo.

Chamar SaveChanges resulta na exclusão da postagem órfã:

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

Excluir o tempo de órfãos e redefinir a paternidade

Por padrão, a marcação de órfãos como Deleted ocorre imediatamente após a detecção da alteração de relação detectada. No entanto, esse processo pode ser adiado até que SaveChanges seja realmente chamado. Isso pode ser útil para evitar que se tornem órfãs entidades que foram removidas de um principal/mãe, mas serão re-parentadas com um novo principal/mãe antes do chamado ao SaveChanges. ChangeTracker.DeleteOrphansTiming é usado para definir esse tempo. Por exemplo:

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

Depois de remover a postagem da primeira coleção, o objeto não é marcado como Deleted estava no exemplo anterior. Em vez disso, o EF Core está monitorando que a relação foi cortada , mesmo sendo uma relação obrigatória. (O valor do FK é considerado nulo pelo EF Core, embora não possa realmente ser nulo porque o tipo não é anulável. Isso é conhecido como "nulo conceitual".)

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: []

Chamar SaveChanges neste momento resultaria na exclusão da postagem órfã. No entanto, se como no exemplo acima, a postagem estiver associada a um novo blog antes de SaveChanges ser chamada, ela será corrigida adequadamente para esse novo blog e não será mais considerada órfã:

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 chamado neste ponto atualizará a postagem no banco de dados em vez de excluí-la.

Também é possível desativar a exclusão automática de órfãos. Isso resultará em uma exceção se a função SaveChanges for invocada enquanto uma entidade órfã estiver sendo rastreada. Por exemplo, 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

Lançará esta exceção:

System.InvalidOperationException: a associação entre entidades 'Blog' e 'Post' com o valor de chave '{BlogId: 1}' foi rompida, mas a relação é marcada como necessária ou é implicitamente necessária porque a chave estrangeira não é anulável. Se a entidade dependente/filho precisar ser excluída quando uma relação necessária for cortada, configure a relação para usar exclusões em cascata.

As exclusões de órfãos e exclusões em cascata podem ser forçadas a qualquer momento chamando o método ChangeTracker.CascadeChanges(). Combinar isso com a definição do tempo de exclusão de órfãos garantirá que os órfãos nunca sejam excluídos Never, a menos que o EF Core seja explicitamente instruído a fazê-lo.

Alterando uma navegação de referência

Alterar a navegação de referência de uma relação um-para-muitos tem o mesmo efeito que alterar a navegação da coleção na outra extremidade da relação. Definir a navegação de referência de dependente/filho como nulo é equivalente a remover a entidade da navegação de coleção do principal/pai. Todas as alterações de correção e de banco de dados ocorrem conforme descrito na seção anterior, incluindo tornar a entidade órfã se a relação for necessária.

Relações um-para-um opcionais

Para relações um-para-um, alterar uma navegação de referência faz com que qualquer relação anterior seja cortada. Para relações opcionais, isso significa que o valor do FK no dependente/filho relacionado anteriormente está definido como nulo. Por exemplo:

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

A exibição de depuração antes de chamar SaveChanges mostra que os novos ativos substituíram os ativos existentes, que agora estão marcados como Modified com um 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>

Isso resulta em uma atualização e uma inserção quando SaveChanges é chamado:

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

Relações um-para-um necessárias

A execução do mesmo código do exemplo anterior, mas desta vez com uma relação um-para-um necessária, mostra que o anteriormente associado BlogAssets agora está marcado como Deleted, uma vez que ele se torna um órfão quando o novo BlogAssets toma seu 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>

Em seguida, isso resulta em uma exclusão e uma inserção quando SaveChanges é chamado:

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

O momento de marcar órfãos como excluídos pode ser alterado da mesma forma mostrada para navegações de coleção e tem os mesmos efeitos.

Excluindo uma entidade

Relações opcionais

Quando uma entidade é marcada como Deleted, por exemplo, chamando DbContext.Remove, as referências à entidade excluída são removidas das navegaçãos de outras entidades. Para relações opcionais, os valores FK em entidades dependentes são definidos como nulos.

Por exemplo, vamos marcar o blog do 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();

Examinar a exibição de depuração do rastreador de alterações antes de chamar SaveChanges mostra:

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: []

Observe que:

  • O blog está marcado como Deleted.
  • Os ativos relacionados ao blog excluído têm um valor FK nulo (BlogId: <null> FK Modified Originally 2) e uma navegação de referência nula (Blog: <null>)
  • Cada postagem relacionada ao blog excluído tem um valor FK nulo (BlogId: <null> FK Modified Originally 2) e uma navegação de referência nula (Blog: <null>)

Relações necessárias

O comportamento de correção para relações obrigatórias é o mesmo que para relações opcionais, a diferença é que as entidades dependentes/filhas são marcadas como Deleted porque não podem existir sem uma entidade principal/mãe e devem ser removidas do banco de dados quando SaveChanges é chamado para evitar uma exceção de restrição referencial. Isso é conhecido como "exclusão em cascata" e é o comportamento padrão no EF Core para as relações necessárias. Por exemplo, executar o mesmo código do exemplo anterior, mas com um relacionamento obrigatório resulta na seguinte exibição de depuração antes de SaveChanges ser chamado:

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 esperado, os dependentes/filhos agora estão marcados como Deleted. No entanto, observe que as navegações nas entidades excluídas não foram alteradas. Isso pode parecer estranho, mas evita despedaçar completamente um grafo excluído de entidades, limpando todas as navegações. Ou seja, o blog, o ativo e as postagens ainda formam um grafo de entidades mesmo depois de terem sido excluídos. Isso facilita muito a exclusão de um grafo de entidades do que foi o caso no EF6 em que o grafo foi fragmentado.

Gestão de tempo de exclusão em cascata e reassociação de parentesco

Por padrão, a exclusão em cascata ocorre assim que o pai/principal é marcado como Deleted. Isso é o mesmo que para excluir órfãos, conforme descrito anteriormente. Assim como com a exclusão de órfãos, esse processo pode ser adiado até que SaveChanges seja chamado ou até mesmo desabilitado inteiramente, definindo-o ChangeTracker.CascadeDeleteTiming adequadamente. Isso é útil da mesma forma que é para excluir órfãos, incluindo para reatribuir a responsabilidade de filhos/dependentes após a exclusão de um responsável/principal.

Exclusões em cascata, assim como a exclusão de órfãos, podem ser realizadas a qualquer momento ao chamar ChangeTracker.CascadeChanges(). Combinar isso com a configuração do tempo de exclusão em cascata para Never garantirá que as exclusões em cascata nunca ocorram, a menos que o EF Core seja explicitamente instruído a fazê-lo.

Dica

A exclusão em cascata e a exclusão de órfãos estão intimamente relacionadas. Ambos resultam na exclusão de entidades dependentes/filhas quando a relação com a entidade principal/mãe necessária é cortada. Para exclusão em cascata, essa separação ocorre porque o elemento principal/pai é excluído. Para órfãos, a entidade principal/pai ainda existe, mas não está mais relacionada às entidades dependentes/filho.

Relacionamentos de muitos para muitos

Relações muitos-para-muitos no EF Core são implementadas usando uma entidade de associação. Cada lado da relação muitos-para-muitos está relacionado a essa entidade associativa com uma relação de um-para-muitos. Essa entidade de junção pode ser definida e mapeada explicitamente ou pode ser criada implicitamente e oculta. Em ambos os casos, o comportamento subjacente é o mesmo. Examinaremos inicialmente esse comportamento subjacente para entender como funciona o acompanhamento das relações de muitos para muitos.

Como funcionam as relações de muitos para muitos

Considere esse modelo do EF Core que cria uma relação muitos para muitos entre postagens e tags usando um tipo de entidade intermediária definido explicitamente.

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 o tipo de entidade de junção PostTag contém duas propriedades de chave estrangeira. Nesse modelo, para que uma postagem esteja relacionada a uma tag, deve haver uma entidade de junção PostTag, onde o valor da chave estrangeira PostTag.PostId corresponde ao valor da chave primária Post.Id, e onde o valor da chave estrangeira PostTag.TagId corresponde ao valor da chave primária Tag.Id. Por exemplo:

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

Examinar a exibição de depuração do rastreador de alterações após executar este código mostra que a postagem e a etiqueta estão relacionadas pela nova PostTag entidade de junção:

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 as navegações das coleções em Post e Tag foram corrigidas, assim como as navegações de referência em PostTag. Essas relações podem ser manipuladas por navegaçãos em vez de valores FK, assim como em todos os exemplos anteriores. Por exemplo, o código acima pode ser modificado para adicionar a relação definindo as navegaçãos de referência na entidade de junção:

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

Isso resulta exatamente na mesma alteração para FKs e navegaçãos como no exemplo anterior.

Ignorar navegação

Manipular a tabela de junção manualmente pode ser complicado. Relações muitos para muitos podem ser manipuladas diretamente usando navegações de coleção especiais que "ignoram" a entidade intermediária. Por exemplo, duas navegações de ignorar podem ser adicionadas ao modelo acima; uma de Post para Tags e a outra de Tag para Posts.

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
}

Essa relação muitos para muitos requer a seguinte configuração para garantir que as navegações de salto e as navegações normais sejam usadas para a mesma relação muitos para muitos.

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 Relações para obter mais informações sobre como mapear relações muitos para muitos.

Navegações simples têm a mesma aparência e comportamento que navegações de coleção normais. No entanto, a maneira como eles trabalham com valores de chave estrangeira é diferente. Vamos associar uma postagem a uma tag, mas desta vez usando uma navegação para pular:

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

Observe que esse código não usa a entidade de junção. Em vez disso, simplesmente adiciona uma entidade à coleção de navegação da mesma maneira que faria se fosse uma relação de um-para-muitos. A exibição de depuração resultante é essencialmente a mesma de 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 uma instância da PostTag entidade de junção foi criada automaticamente com valores FK definidos para os valores PK da marca e postagem que agora estão associados. Todas as navegaçãos normais de referência e coleção foram corrigidas para corresponder a esses valores FK. Além disso, como esse modelo contém navegações alternativas, elas também foram corrigidas. Especificamente, mesmo que tenhamos adicionado a marca à Post.Tags navegação de pular, a Tag.Posts navegação inversa de pular do outro lado dessa relação também foi corrigida para conter a postagem associada.

É importante notar que as relações subjacentes de muitos para muitos ainda podem ser manipuladas diretamente, mesmo quando as navegações de salto foram sobrepostas. Por exemplo, a etiqueta e o Post podem ser associados como fizemos antes de introduzir as navegações de salto.

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

Ou usando valores FK:

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

Isso ainda fará com que as navegações de salto sejam corrigidas, resultando na mesma saída de exibição de depuração que no exemplo anterior.

Ignorar somente navegação

Na seção anterior, adicionamos navegações de pular além de definir totalmente as duas relações um-para-muitos subjacentes. Isso é útil para ilustrar o que acontece com os valores do FK, mas muitas vezes é desnecessário. Em vez disso, a relação muitos para muitos pode ser definida usando apenas navegações ignoradas. É assim que a relação muitos para muitos é definida no modelo na parte superior deste documento. Usando esse modelo, podemos associar novamente uma postagem e uma etiqueta adicionando uma publicação à Tag.Posts navegação ignorada (ou, como alternativa, adicionando uma etiqueta à Post.Tags navegação ignorada):

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

Examinar a exibição de depuração depois de fazer essa alteração revela que o EF Core criou uma instância de Dictionary<string, object> para representar a entidade de junção. Essa entidade de junção contém tanto PostsId quanto TagsId propriedades de chave estrangeira que foram configuradas para corresponder aos valores de chave primária da postagem e da marca associadas.

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 Relações para obter mais informações sobre entidades de junção implícitas e o uso de tipos de entidades Dictionary<string, object>.

Importante

O tipo CLR usado para unir tipos de entidade por convenção pode mudar em versões futuras para melhorar o desempenho. Não dependa do tipo de junção ser Dictionary<string, object> , a menos que isso tenha sido configurado explicitamente.

Unir entidades com cargas

Até agora, todos os exemplos usaram um tipo de entidade de junção (seja explícito ou implícito) que contém apenas as duas propriedades de chave estrangeira necessárias para a relação muitos para muitos. Nenhum desses valores de FK precisa ser definido explicitamente pelo aplicativo ao manipular relações porque seus valores vêm das propriedades de chave primária das entidades relacionadas. Isso permite que o EF Core crie instâncias da entidade de junção sem dados ausentes.

Cargas de dados com valores gerados

O EF Core dá suporte à adição de propriedades adicionais ao tipo de entidade de junção. Isso é conhecido como dar à entidade de junção uma "carga útil". Por exemplo, vamos adicionar TaggedOn propriedade à entidade de junção 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
}

Essa propriedade de conteúdo não será definida quando o EF Core criar uma instância de entidade de junção. A maneira mais comum de lidar com isso é usar propriedades de conteúdo com valores gerados automaticamente. Por exemplo, a TaggedOn propriedade pode ser configurada para usar um carimbo de data/hora gerado pelo repositório quando cada nova entidade é inserida:

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

Uma postagem agora pode ser marcada da mesma maneira 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);

Verificar a exibição de depuração do rastreador de alterações após chamar SaveChanges mostra que a propriedade payload foi configurada adequadamente.

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

Definindo explicitamente valores de carga

Seguindo o exemplo anterior, vamos adicionar uma propriedade de conteúdo que não usa um valor gerado automaticamente:

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
}

Uma postagem agora pode ser marcada da mesma maneira que antes e a entidade de junção ainda será criada automaticamente. Essa entidade pode ser acessada usando um dos mecanismos descritos no Accessing Tracked Entities. Por exemplo, o código a seguir usa DbSet<TEntity>.Find para acessar a instância de entidade de junção:

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

Depois que a entidade de junção for localizada, ela poderá ser manipulada da maneira normal — neste exemplo, para definir a propriedade de carga útil de TaggedBy antes de chamar SaveChanges.

Observação

Observe que é necessário fazer uma chamada para ChangeTracker.DetectChanges() aqui, a fim de dar ao EF Core a chance de detectar a alteração da propriedade de navegação e criar a instância da entidade de junção antes que Find seja usada. Consulte Detecção de Alterações e Notificações para obter mais informações.

Como alternativa, a entidade de associação pode ser criada explicitamente para associar uma postagem a uma etiqueta. Por exemplo:

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 fim, outra maneira de definir dados de carga é substituindo SaveChanges ou usando o DbContext.SavingChanges evento para processar entidades antes de atualizar o banco de dados. Por exemplo:

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