Compartilhar via


Alterando Chaves Estrangeiras e Navegações

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

Os relacionamentos em um modelo do Entity Framework Core (EF Core) são representado usando chaves estrangeiras (FKs). Uma FK consiste em uma ou mais propriedades na entidade dependente ou filho no relacionamento. Essa entidade dependente/filho é associada a uma determinada entidade de segurança/pai quando os valores das propriedades de chave estrangeira no dependente/filho correspondem aos valores das propriedades de chave alternativa ou primária (PK) na entidade/pai.

As chaves estrangeiras são uma boa maneira de armazenar e manipular relacionamentos no banco de dados, mas não são muito amigáveis ao lidar com várias entidades relacionadas no código do aplicativo. Portanto, a maioria dos modelos EF Core também colocam "navegações" em camadas sobre a 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 da chave estrangeira com valores da chave primária ou alternativa.

As navegações podem ser usadas em ambos os lados do relacionamento, apenas em um lado, ou não, deixando somente a propriedade FK. A propriedade FK pode ser ocultada tornando-a uma propriedade de sombra. Confira Relacionamentos para obter mais informações sobre relacionamentos de modelagem.

Dica

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

Dica

Você pode executar e depurar em 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 relacionamentos 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
}

Os três relacionamentos nesse modelo são:

  • Cada blog pode ter muitas postagens (um para muitos):
    • Blog é a entidade de segurança/pai.
    • Post é o dependente/filho. Ele contém a propriedade FK Post.BlogId, cujo valor deve corresponder ao valor PK Blog.Id 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 de coleção de um blog para todas as postagens associadas. Blog.Posts é a navegação inversa para Post.Blog.
  • Cada blog pode ter um ativo (um para um):
    • Blog é a entidade de segurança/pai.
    • BlogAssets é o dependente/filho. Ele contém a propriedade FK BlogAssets.BlogId, o valor do qual deve corresponder ao valor PK Blog.Id 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 tags e cada tag pode ter muitas postagens (muitos para muitos):
    • Relacionamentos muitos para muitos são uma camada adicional sobre dois relacionamentos um para muitos. Relacionamentos muitos para muitos são abordados posteriormente neste documento.
    • Post.Tags é uma navegação de coleção de uma postagem para todas as tags associadas. Post.Tags é a navegação inversa para Tag.Posts.
    • Tag.Posts é uma navegação de coleção de uma tag para todas as postagens associadas. Tag.Posts é a navegação inversa para Post.Tags.

Confira Relacionamentos para obter mais informações sobre como modelar e configurar relacionamentos.

Correção de relacionamento

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

Correção por consulta

A correção ocorre primeiro quando as entidades são consultadas do banco de dados. O banco de dados tem somente valores da chave estrangeira, portanto, quando o EF Core cria uma instância de entidade do banco de dados, ele usa os valores da chave estrangeira para definir navegações de referência e adicionar entidades às navegações 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 = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToList();

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

Para cada blog, o EF Core criará primeiro uma instância Blog. Então, à medida que cada postagem é carregada do banco de dados, sua navegação de referência Post.Blog é definida para apontar para o blog associado. Da mesma forma, a postagem é adicionada à navegação de coleção Blog.Posts. A mesma coisa acontece com BlogAssets, só que nesse caso ambas as navegações são referências. A navegação Blog.Assets é configurada para apontar para a instância de ativos e a navegação BlogAsserts.Blog é configurada para apontar para a instância do blog.

Observar a exibição de depuração do rastreador de alterações após esta consulta mostra dois blogs, cada um com um ativo 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 mostradas usando os valores de chave primária das entidades relacionadas. Por exemplo, Posts: [{Id: 1}, {Id: 2}] na saída acima indica que a navegação de coleção Blog.Posts contém duas postagens relacionadas com chaves primárias 1 e 2 respectivamente. Da mesma forma, para cada postagem associada ao primeiro blog, a linha Blog: {Id: 1} indica que a navegação Post.Blog faz referência ao Blog com chave primária 1.

Correção para entidades rastreadas localmente

A correção de relacionamento também acontece entre entidades retornadas de uma consulta de rastreamento e entidades já rastreadas 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 = context.Blogs.ToList();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

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

Observando novamente as exibições de depuração, após a primeira consulta somente os dois blogs serão rastreados:

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 navegações de referência Blog.Assets são nulas e as navegações de coleção Blog.Posts estão vazias porque nenhuma entidade associada está sendo rastreada atualmente pelo contexto.

Após a segunda consulta, as navegações de referência Blogs.Assets foram corrigidas para apontar para as instâncias recentemente rastreadas BlogAsset. Da mesma forma, as navegações de referência BlogAssets.Blog são definidas para apontar para a instância Blog apropriada já 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}

Finalmente, após a terceira consulta, as navegações de coleção Blog.Posts agora contêm todos as postagens relacionados, e as referências Post.Blog 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 alcançado com a consulta única original, uma vez que o EF Core corrigiu 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. Ela conecta somente entidades que já foram retornadas pela consulta ou já rastreadas pelo DbContext. Confira Resolução de Identidade no EF Core para obter informações sobre como lidar com duplicatas ao serializar entidades.

Alterando relacionamentos usando navegações

A maneira mais fácil de alterar o relacionamento entre duas entidades é manipular uma navegação, deixando o EF Core corrigir a navegação inversa e os valores FK adequadamente. 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ção

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 de um blog para a coleção de navegação do outro blog:

using var context = new BlogsContext();

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

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

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

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

context.SaveChanges();

Dica

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

Esta é a exibiçã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 navegação Blog.Posts no Blog .NET passou a contar com três postagens (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Da mesma forma, a Blog.Posts navegação no blog do Visual Studio tem somente uma postagem (Posts: [{Id: 4}]). Isso deve ser esperado, pois o código alterou explicitamente essas coleções.

O mais interessante é que mesmo que o código não tenha alterado explicitamente a navegação Post.Blog, ele foi corrigido para apontar para o blog do Visual Studio (Blog: {Id: 1}). Além disso, o valor da chave estrangeira Post.BlogId foi atualizado para corresponder ao valor da chave primária do blog .NET. Essa alteração no valor FK persistiu 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ções 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 navegação de referência Post.Blog 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 na navegação de referência e, em seguida, corrigiu as navegações de coleção e o valor FK para corresponder.

Alterando relacionamentos usando valores da chave estrangeira

Na seção anterior, os relacionamentos foram manipulados pelas navegações deixando os valores da chave estrangeira para serem atualizados automaticamente. Essa é a maneira recomendada de manipular relacionamentos 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 chave estrangeira Post.BlogId:

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, como mostrado no exemplo anterior.

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

Dica

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

Correção 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 estado Added. 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á rastreada no estado Unchanged.)
  • Se a entidade estiver associada a uma entidade de segurança/pai diferente, esse relacionamento será rompido.
  • A entidade torna-se associada à entidade de segurança/pai que tem a navegação de coleção.
  • As navegações e os valores da chave estrangeira são fixados para todas as entidades envolvidas.

Com base nisso, podemos ver que para mover uma postagem de um blog para outro, na verdade 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 do primeiro blog.

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

A remoção de uma entidade dependente/filho de navegação de coleção da entidade de segurança/pai causa o rompimento do relacionamento com essa entidade de segurança/pai. O que acontece a seguir depende se o relacionamento é opcional ou necessário.

Relações opcionais

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

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

Observar o modo de exibição de depuração de controle de alterações após essa alteração mostra que:

  • O FK Post.BlogId foi definido como nulo (BlogId: <null> FK Modified Originally 1)
  • A navegação de referência Post.Blog foi definida como nula (Blog: <null>)
  • A postagem foi removida da navegação de 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. É marcada como Modified para que o valor 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 os relacionamentos necessários. Portanto, romper um relacionamento necessário significa que a entidade dependente/filho deve ser re-parentada a uma nova entidade de segurança/pai ou removida do banco de dados quando SaveChanges for chamado para evitar uma violação de restrição referencial. Isso é conhecido como "exclusão de órfãos" e é o comportamento padrão no EF Core para os relacionamentos necessários.

Por exemplo, vamos alterar o relacionamento entre blog e postagens para serem necessários e depois executar o mesmo código do exemplo anterior:

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

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

  • A postagem foi marcada como Deleted de modo que será excluída do banco de dados quando SaveChanges for chamado.
  • A navegação de referência Post.Blog foi definida como nula (Blog: <null>).
  • A postagem foi removida da navegação de 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} 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 permanece inalterado, pois para um relacionamento necessário 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 e a nova paternidade dos órfãos

Por padrão, a marcação de órfãos como Deleted acontece assim que a alteração do relacionamento é detectada. No entanto, esse processo pode ser adiado até que SaveChanges seja realmente chamado. Isso pode ser útil para evitar tornar órfãs de entidades que foram removidas de uma entidade de segurança/pai, mas que serão re-parentadas com uma nova entidade de segurança/pai antes que SaveChanges seja chamado. 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);

context.SaveChanges();

Após remover a postagem da primeira coleção o objeto não fica marcado como Deleted no exemplo anterior. Em vez disso, o EF Core está monitorando que o relacionamento foi rompido, mesmo que seja um relacionamento necessário. (O valor 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 chamado, então ela será corrigida apropriadamente 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 SaveChanges for chamado enquanto um órfão estiver sendo rastreado. Por exemplo, este código:

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

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

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

context.SaveChanges(); // Throws

Lançará esta exceção:

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

A exclusão de órfãos, bem como exclusões em cascata, pode ser forçada a qualquer momento chamando ChangeTracker.CascadeChanges(). Combinar isso com a definição do tempo órfão de exclusão garantirá Never que os órfãos nunca sejam excluídos, 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 um relacionamento um para muitos tem o mesmo efeito que alterar a navegação de coleção na outra extremidade do relacionamento. Definir a navegação de referência de dependente/filho como nula equivale a remover a entidade de navegação de coleção da entidade de segurança/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 o relacionamento for necessária.

Relacionamentos um para um opcionais

Para relacionamentos um para um, alterar uma navegação de referência faz com que qualquer relacionamento anterior seja rompido. Para relacionamentos 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 = context.Blogs.Include(e => e.Assets).Single(e => e.Name == ".NET Blog");
dotNetBlog.Assets = new BlogAssets();

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

context.SaveChanges();

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

Relacionamentos um para um necessários

A execução do mesmo código do exemplo anterior, mas desta vez com um relacionamento um para um necessário, mostra que o BlogAssets associado anteriormente agora está marcado como Deleted, pois se torna ó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 tempo de marcação de ó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, por chamada DbContext.Remove, as referências à entidade excluída são removidas das navegações de outras entidades. Para relacionamentos 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 = context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .Single(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

context.SaveChanges();

Observar 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 têm 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 relacionamentos necessários é o mesmo que para relacionamentos opcionais, exceto que as entidades dependentes/filho são marcadas como Deleted, pois não podem existir sem uma entidade de segurança/pai 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 os relacionamentos necessárias. Por exemplo, executar o mesmo código do exemplo anterior, mas com um relacionamento necessá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. Entretanto, observe que as navegações nas entidades excluídas não foram alteradas. Isso pode parecer estranho, mas evita destruir completamente um gráfico de entidades excluído, limpando todas as navegações. Ou seja, o blog, o ativo e as postagens ainda formam um gráfico de entidades mesmo depois de terem sido excluídos. Isso torna muito mais fácil cancelar a exclusão de um gráfico de entidades do que no EF6, onde o gráfico foi fragmentado.

Tempo de exclusão em cascata e o gerenciamento do domínio pai

Por padrão, a exclusão em cascata ocorre assim que o pai/entidade de segurança é marcado como Deleted. Isso é o mesmo que a exclusão de órfãos, como descrito anteriormente. Assim como acontece com a exclusão de órfãos, esse processo pode ser atrasado até que SaveChanges seja chamado, ou até mesmo desabilitado totalmente, configurando-se ChangeTracker.CascadeDeleteTiming adequadamente. Isto é útil da mesma forma que é para exclusão de órfãos, inclusive para o gerenciamento do domínio pai e de filhos/dependentes após a exclusão de uma entidade de segurança/pai.

As exclusões em cascata, assim como a exclusão de órfãos, podem ser forçadas a qualquer momento chamando ChangeTracker.CascadeChanges(). Combinar isso com a configuração do tempo de exclusão em cascata garantirá Never que as exclusões em cascata nunca aconteçam, 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/filho quando o relacionamento com a entidade de segurança/pai necessária é rompido. Para exclusão em cascata, essa separação ocorre porque a entidade de segurança/pai é excluída. Para órfãos, a entidade de segurança/pai ainda existe, mas não está mais relacionada às entidades dependentes/filho.

Relacionamento muitos para muitos

Relacionamentos muitos para muitos no EF Core são implementados usando uma entidade de junção. Cada lado do relacionamento muitos para muitos está relacionado a essa entidade de junção com um relacionamento um para muitos. Essa entidade de junção pode ser definida e mapeada explicitamente ou pode ser criada implicitamente e ocultada. Em ambos os casos, o comportamento subjacente é o mesmo. Examinaremos primeiro esse comportamento subjacente para entender como funciona o rastreamento de relacionamentos muitos para muitos.

Quantos relacionamentos muitos para muitos funcionam

Considere esse modelo do EF Core que cria um relacionamento muitos para muitos entre postagens e tags usando um tipo de entidade de junção 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 seja relacionada a uma tag, deve haver uma entidade de junção PostTag onde o valor da chave estrangeira PostTag.PostId corresponda ao valor da chave primária Post.Id e onde o valor da chave estrangeira PostTag.TagId corresponda ao valor da chave primária Tag.Id. Por exemplo:

using var context = new BlogsContext();

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

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

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

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

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

Observe que as navegações de coleção em Post e Tag foram corrigidas, assim como as navegações de referência em PostTag. Estes relacionamentos podem ser manipulados por navegações em vez de valores FK, tal como em todos os exemplos anteriores. Por exemplo, o código acima pode ser modificado para adicionar o relacionamento definindo as navegações de referência na entidade de junção:

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

Isso resulta exatamente na mesma alteração nos FKs e nas navegações do exemplo anterior.

Saltos de navegação

Manipular a tabela de junção manualmente pode ser complicado. Os relacionamentos muitos para muitos podem ser manipulados diretamente usando navegações de coleção especiais que "ignoram" a entidade de junção. Por exemplo, dois saltos de navegação podem ser adicionados ao modelo acima; um de Postagem para Tags e o outro de Tag para Postagens:

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
}

Esse relacionamento muitos para muitos requer a seguinte configuração para garantir que os saltos de navegação e as navegações normais sejam usadas para o mesmo relacionamento 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));
}

Confira Relacionamentos para obter mais informações sobre como mapear relacionamentos muitos para muitos.

Os saltos de navegação se parecem e se comportam como navegações de coleção normais. No entanto, a maneira como eles trabalham com os valores da chave estrangeira é diferente. Vamos associar uma postagem a uma tag, mas desta vez usando um salto de navegação:

using var context = new BlogsContext();

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

post.Tags.Add(tag);

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

Observe que esse código não usa a entidade de junção. Em vez disso, somente adiciona uma entidade a uma coleção de navegação da mesma forma que seria feito se este fosse um relacionamento 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 de entidade de junção PostTag foi criada automaticamente com valores FK definidos para os valores PK da tag e postagem que agora estão associados. Todas as navegações normais de referência e coleção foram corrigidas para corresponder a esses valores FK. Além disso, como esse modelo contém saltos de navegação, eles também foram corrigidos. Especificamente, embora tenhamos adicionado a tag à navegação de ignorar Post.Tags, o salto de navegação inverso Tag.Posts no outro lado desse relacionamento também foi corrigido para conter a postagem associada.

É importante observar que os relacionamentos subjacentes muitos para muitos ainda podem ser manipulados diretamente, mesmo quando os saltos de navegação foram colocados em camadas na parte superior. Por exemplo, a tag e a postagem poderiam ser associadas como fizemos antes de introduzir os saltos de navegação:

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 os saltos de navegação sejam corrigidos corretamente, resultando na mesma saída da exibição de depuração do exemplo anterior.

Somente saltos de navegação

Na seção anterior, adicionamos saltos de navegação, além de definir completamente os dois relacionamentos subjacentes um para muitos. Isso é útil para ilustrar o que acontece com os valores FK, mas muitas vezes é desnecessário. Em vez disso, o relacionamento muitos para muitos pode ser definido usando somente saltos de navegação. É assim que o relacionamento muitos para muitos é definido no modelo no início deste documento. Usando este modelo, podemos associar novamente uma postagem e uma tag adicionando uma postagem ao salto de navegação Tag.Posts (ou, alternativamente, adicionando uma tag ao salto de navegação Post.Tags):

using var context = new BlogsContext();

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

post.Tags.Add(tag);

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

Observar a exibição de depuração após 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 PostsId e TagsId propriedades de chave estrangeira que foram definidas para corresponder aos valores PK da postagem e da tag que estão 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

Confira Relacionamentos para obter mais informações sobre entidades de junção implícitas e o uso de tipos de entidade Dictionary<string, object>.

Importante

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

Unir entidades com conteúdos

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

Conteúdos 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 um "conteúdo". Por exemplo, vamos adicionar a propriedade TaggedOn à 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 propriedade TaggedOn 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.SaveChanges();

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

Observar a exibição de depuração do rastreador de alterações após chamar SaveChanges mostra que a propriedade de conteúdo foi definida 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 os valores de conteúdo

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 forma que antes, e a entidade de junção ainda será criada automaticamente. Essa entidade pode então ser acessada usando um dos mecanismos descritos em Acessando Entidades Rastreadas. 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

context.SaveChanges();

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

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

Observação

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

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

using var context = new BlogsContext();

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

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

context.SaveChanges();

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

Finalmente, outra maneira de definir os dados de conteúdo é substituindo SaveChanges ou usando o evento DbContext.SavingChanges para processar entidades antes de atualizar o banco de dados. Por exemplo:

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

    return base.SaveChanges();
}