Изменение внешних ключей и навигаций

Обзор внешних ключей и навигаций

Связи в модели Entity Framework Core (EF Core) представлены с помощью внешних ключей (FK). FK состоит из одного или нескольких свойств зависимой или дочерней сущности в связи. Эта зависимая или дочерняя сущность связана с заданной основной или родительской сущностью, когда значения свойств внешнего ключа в зависимых или дочерних объектах соответствуют значениям свойств альтернативного или первичного ключа (PK) в субъекте или родительском объекте.

Внешние ключи — это хороший способ хранения связей в базе данных и управления ими, но не очень удобный при работе с несколькими связанными сущностями в коде приложения. Таким образом, большинство моделей EF Core также слоев "навигации" по представлению FK. Навигации формируют ссылки на C#/.NET между экземплярами сущностей, которые отражают связи, найденные путем сопоставления значений внешнего ключа с первичными или альтернативными значениями ключей.

Навигации можно использовать на обеих сторонах связи, только на одной стороне или вообще, оставляя только свойство FK. Свойство FK можно скрыть, сделав его теневым свойством. Дополнительные сведения о моделировании связей см. в разделе "Связи".

Совет

В этом документе предполагается, что рассматриваются состояния сущностей и основы отслеживания изменений EF Core. Дополнительные сведения об этих разделах см. в Отслеживание изменений в EF Core.

Совет

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Пример модели

Следующая модель содержит четыре типа сущностей с связями между ними. Комментарии в коде указывают, какие свойства являются внешними ключами, первичными ключами и навигацией.

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
}

Три связи в этой модели:

  • У каждого блога может быть много записей (один ко многим):
    • Blog — это субъект или родитель.
    • Post параметр является зависимым или дочерним. Он содержит свойство Post.BlogIdFK, значение которого должно соответствовать Blog.Id значению PK связанного блога.
    • Post.Blog — это эталонная навигация из записи в связанный блог. Post.Blog — это обратная навигация для Blog.Posts.
    • Blog.Posts — это навигация по коллекции из блога на все связанные записи. Blog.Posts — это обратная навигация для Post.Blog.
  • Каждый блог может иметь один ресурс (один к одному):
    • Blog — это субъект или родитель.
    • BlogAssets параметр является зависимым или дочерним. Он содержит свойство BlogAssets.BlogIdFK, значение которого должно соответствовать Blog.Id значению PK связанного блога.
    • BlogAssets.Blog — это эталонная навигация из ресурсов в связанный блог. BlogAssets.Blog — это обратная навигация для Blog.Assets.
    • Blog.Assets — это эталонная навигация из блога в связанные ресурсы. Blog.Assets — это обратная навигация для BlogAssets.Blog.
  • Каждая запись может иметь много тегов, а каждый тег может содержать много записей (многие ко многим):
    • Связи "многие ко многим" являются дополнительным слоем двух связей "один ко многим". Связи "многие ко многим" рассматриваются далее в этом документе.
    • Post.Tags — это навигация по коллекции из записи на все связанные теги. Post.Tags — это обратная навигация для Tag.Posts.
    • Tag.Posts — это навигация по коллекции из тега во все связанные записи. Tag.Posts — это обратная навигация для Post.Tags.

Дополнительные сведения о моделировать и настраивать связи см. в разделе "Связи ".

Исправление связей

EF Core поддерживает навигацию в соответствии со значениями внешнего ключа и наоборот. То есть, если значение внешнего ключа изменится таким образом, чтобы оно ссылалось на другую сущность субъекта или родителя, переходы обновляются, чтобы отразить это изменение. Аналогичным образом, если навигация изменена, значения внешнего ключа участвующих сущностей обновляются, чтобы отразить это изменение. Это называется "исправление связей".

Исправление по запросу

Исправление сначала возникает при запросе сущностей из базы данных. База данных имеет только значения внешнего ключа, поэтому, когда EF Core создает экземпляр сущности из базы данных, которая использует значения внешнего ключа для задания ссылочных навигаций и добавления сущностей в навигации коллекции соответствующим образом. Например, рассмотрим запрос к блогам и связанным с ним записям и ресурсам:

using var context = new BlogsContext();

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

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

Для каждого блога EF Core сначала создаст Blog экземпляр. Затем, когда каждая запись загружается из базы данных, ее Post.Blog эталонная навигация задается, чтобы она указывала на связанный блог. Аналогичным образом запись добавляется в навигацию Blog.Posts по коллекции. То же самое происходит с BlogAssets, за исключением того, что обе навигации являются ссылками. Навигация Blog.Assets устанавливается для указания экземпляра ресурсов, а BlogAsserts.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: []

В представлении отладки отображаются как ключевые значения, так и навигации. Навигации отображаются с использованием значений первичного ключа связанных сущностей. Например, в выходных данных выше указано, Posts: [{Id: 1}, {Id: 2}] что Blog.Posts навигация по коллекции содержит две связанные записи с первичными ключами 1 и 2 соответственно. Аналогичным образом, для каждой записи, связанной с первым блогом Blog: {Id: 1} , строка указывает, что Post.Blog навигация ссылается на блог с первичным ключом 1.

Исправление локально отслеживаемых сущностей

Исправление связей также происходит между сущностями, возвращаемыми из запроса отслеживания, и сущности, которые уже отслеживаются DbContext. Например, рассмотрите возможность выполнения трех отдельных запросов для блогов, записей и ресурсов:

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

Снова просматривая представления отладки, после первого запроса отслеживаются только два блога:

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

Ссылочные Blog.Assets навигации имеют значение NULL, а Blog.Posts навигации коллекции пусты, так как связанные сущности в настоящее время не отслеживаются контекстом.

После второго запроса ссылочные навигации были исправлены до того, Blogs.Assets чтобы они указывали на новые отслеживаемые BlogAsset экземпляры. Аналогичным образом ссылочные навигации задаются так, BlogAssets.Blog чтобы они указывали на соответствующий уже отслеживаемый Blog экземпляр.

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}

Наконец, после третьего запроса Blog.Posts навигации коллекции теперь содержат все связанные записи и Post.Blog ссылки указывают на соответствующий 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: []

Это то же конечное состояние, что и при выполнении исходного отдельного запроса, так как EF Core исправил навигацию по мере отслеживания сущностей, даже при получении из нескольких различных запросов.

Примечание

Исправление никогда не приводит к возврату дополнительных данных из базы данных. Он подключает только сущности, которые уже возвращаются запросом или уже отслеживаются DbContext. Сведения об обработке дубликатов при сериализации сущностей см. в разделе "Разрешение удостоверений" в EF Core .

Изменение связей с помощью навигаций

Самый простой способ изменить связь между двумя сущностями — управлять навигацией, оставляя EF Core для исправления обратных значений навигации и FK соответствующим образом. Как это можно сделать:

  • Добавление или удаление сущности из навигации по коллекции.
  • Изменение ссылочной навигации для указания на другую сущность или присвоение ему значения NULL.

Добавление или удаление из навигаций коллекции

Например, давайте переместим одну из записей из блога Visual Studio в блог .NET. Для этого сначала требуется загрузка блогов и записей, а затем перемещение записи из коллекции навигации в одном блоге в коллекцию навигации на другом блоге:

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

Совет

Вызов необходим здесь, так как доступ к ChangeTracker.DetectChanges() представлению отладки не вызывает автоматического обнаружения изменений.

Это представление отладки, напечатанное после выполнения приведенного выше кода:

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

Навигация Blog.Posts в блоге .NET теперь содержит три записи (Posts: [{Id: 1}, {Id: 2}, {Id: 3}]). Аналогичным образом навигация Blog.Posts в блоге Visual Studio содержит только одну запись (Posts: [{Id: 4}]). Это следует ожидать, так как код явно изменил эти коллекции.

Более интересно, даже если код явно не изменил навигацию Post.Blog , он был исправлен, чтобы указать на блог Visual Studio (Blog: {Id: 1}). Кроме того, Post.BlogId значение внешнего ключа было обновлено в соответствии со значением первичного ключа блога .NET. Это изменение значения FK в базе данных сохраняется при вызове SaveChanges:

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

Изменение ссылочных навигаций

В предыдущем примере запись была перемещена из одного блога в другой, управляя навигацией по коллекции записей в каждом блоге. То же самое можно достичь, изменив навигацию Post.Blog по ссылке, чтобы она указывала на новый блог. Пример:

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

Представление отладки после этого изменения точно такое же , как в предыдущем примере. Это связано с тем, что EF Core обнаружила изменение ссылочной навигации, а затем исправила навигацию коллекции и значение FK для сопоставления.

Изменение связей с помощью значений внешнего ключа

В предыдущем разделе отношения управлялись навигацией, оставляя значения внешнего ключа обновляться автоматически. Это рекомендуемый способ управления связями в EF Core. Однако можно также напрямую управлять значениями FK. Например, можно переместить запись из одного блога в другой, изменив Post.BlogId значение внешнего ключа:

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

Обратите внимание, что это очень похоже на изменение ссылочной навигации, как показано в предыдущем примере.

Представление отладки после этого изменения снова совпадает с предыдущими двумя примерами. Это связано с тем, что EF Core обнаружила изменение значения FK, а затем исправила навигацию ссылок и коллекций для сопоставления.

Совет

Не создавайте код для управления всеми навигациями и значениями FK при каждом изменении связи. Такой код является более сложным и должен обеспечить согласованные изменения внешних ключей и навигаций в каждом случае. По возможности просто управляйте одной навигацией или, возможно, обе навигации. При необходимости просто управляйте значениями FK. Избегайте управления навигацией и значениями FK.

Исправление для добавленных или удаленных сущностей

Добавление в навигацию по коллекции

EF Core выполняет следующие действия при обнаружении добавления новой зависимой или дочерней сущности в навигацию по коллекции:

  • Если сущность не отслеживается, она отслеживается. (Сущность обычно будет находиться в Added состоянии. Однако если тип сущности настроен для использования созданных ключей и задано значение первичного ключа, то сущность отслеживается в Unchanged состоянии.)
  • Если сущность связана с другим субъектом или родительским элементом, то эта связь будет разорвана.
  • Сущность становится связана с субъектом или родительским объектом, которому принадлежит навигация по коллекции.
  • Навигации и значения внешнего ключа фиксируются для всех участвующих сущностей.

Основываясь на этом, мы видим, что для перемещения записи из одного блога в другой нам на самом деле не нужно удалять его из старой навигации коллекции, прежде чем добавлять его в новый. Поэтому код из приведенного выше примера можно изменить следующим образом:

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

на:

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

EF Core видит, что запись добавлена в новый блог и автоматически удаляет ее из коллекции в первом блоге.

Удаление из навигации по коллекции

Удаление зависимой или дочерней сущности из навигации по коллекции основного или родительского объекта приводит к разрыву связи с этим субъектом или родительским объектом. Дальнейшие действия зависят от того, является ли связь необязательной или обязательной.

Необязательные связи

По умолчанию для необязательных связей значение внешнего ключа имеет значение NULL. Это означает, что зависимый или дочерний объект больше не связан с каким-либо участником или родительским элементом. Например, давайте загрузим блог и записи, а затем удалим одну из записей из навигации Blog.Posts по коллекции:

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

Просмотр представления отладки отслеживания изменений после этого изменения показывает следующее:

  • Для Post.BlogId FK задано значение NULL (BlogId: <null> FK Modified Originally 1)
  • Навигация по ссылке Post.Blog имеет значение NULL (Blog: <null>)
  • Запись удалена из 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: []

Обратите внимание, что запись не помечена как Deleted. Оно помечается так Modified , чтобы значение FK в базе данных было задано равным NULL при вызове SaveChanges.

Обязательные связи

Установка значения FK в значение NULL не допускается (обычно это невозможно) для необходимых связей. Таким образом, отключение необходимой связи означает, что зависимые или дочерние сущности должны быть повторно родительскими для нового участника или родителя или удалены из базы данных при вызове SaveChanges, чтобы избежать нарушения ссылочного ограничения. Это называется удалением потерянных объектов и поведением по умолчанию в EF Core для необходимых связей.

Например, давайте изменим связь между блогом и записями, чтобы они требовались, а затем выполните тот же код, что и в предыдущем примере:

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

При просмотре представления отладки после этого изменения показано следующее:

  • Запись помечена как Deleted такая, чтобы она была удалена из базы данных при вызове SaveChanges.
  • Для Post.Blog навигации по ссылке задано значение NULL (Blog: <null>).
  • Запись удалена из 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: []

Обратите внимание, что осталось Post.BlogId неизменным, так как для требуемой связи его нельзя задать равным NULL.

Вызов SaveChanges приводит к удалению потерянной записи:

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

Удаление потерянных значений времени и повторное воспитание

По умолчанию помечает потерянные потерянные файлы, как Deleted только обнаруживается изменение связи. Однако этот процесс может быть отложен до фактического вызова SaveChanges. Это может быть полезно, чтобы избежать создания потерянных сущностей, которые были удалены из одного субъекта или родителя, но будут повторно родительскими с новым субъектом или родительским элементом перед вызовом SaveChanges. ChangeTracker.DeleteOrphansTiming используется для задания этого времени. Пример:

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

После удаления записи из первой коллекции объект не помечается как Deleted в предыдущем примере. Вместо этого EF Core отслеживает, что связь разорвана , даже если это обязательная связь. (Значение FK считается null ef Core, даже если оно не может быть пустым, так как тип не допускает значения NULL. Это называется "концептуальным 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: []

Вызов SaveChanges в настоящее время приведет к удалению потерянной записи. Однако, если, как в приведенном выше примере, запись связана с новым блогом перед вызовом SaveChanges, то она будет исправлена соответствующим образом для этого нового блога и больше не считается потерянным:

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, вызываемая на этом этапе, обновит запись в базе данных, а не удаляет ее.

Также можно отключить автоматическое удаление потерянных объектов. Это приведет к исключению, если функция SaveChanges вызывается во время отслеживания потерянных файлов. Например, этот код:

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

Вызовет следующее исключение:

System.InvalidOperationException: связь между сущностями Blog и Post со значением ключа "{BlogId: 1}" была разорвана, но связь помечена как обязательная или неявно требуется, так как внешний ключ не допускает значения NULL. Если зависимая или дочерняя сущность должна быть удалена при удалении необходимой связи, настройте связь для использования каскадных удалений.

Удаление потерянных объектов, а также каскадных удалений может быть принудительно выполнено в любое время путем вызова ChangeTracker.CascadeChanges(). Объединяя это с настройкой времени удаления потерянных файлов, чтобы гарантировать, что Never потерянные файлы никогда не удаляются, если ef Core не будет явно указан для этого.

Изменение ссылочной навигации

Изменение ссылочной навигации связи "один ко многим" оказывает такое же влияние, как изменение навигации коллекции на другом конце связи. Установка ссылочной навигации зависимого или дочернего значения null эквивалентна удалению сущности из навигации коллекции субъекта или родителя. Все исправления и изменения базы данных происходят, как описано в предыдущем разделе, включая создание сущности потерянным, если требуется связь.

Необязательные связи "один к одному"

Для связей "один к одному" изменение ссылочной навигации приводит к тому, что любая предыдущая связь будет разорвана. Для необязательных связей это означает, что для значения FK ранее связанного зависимого или дочернего элемента задано значение NULL. Пример:

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

Представление отладки перед вызовом SaveChanges показывает, что новые ресурсы заменили существующие ресурсы, которые теперь помечены как Modified значения null BlogAssets.BlogId FK:

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>

Это приводит к обновлению и вставке при вызове SaveChanges:

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

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

Обязательные связи "один к одному"

Выполнение того же кода, что и в предыдущем примере, но на этот раз с требуемой связью "один к одному" показывает, что ранее связанная BlogAssets теперь помечена как Deleted,так как она становится потерянным, когда новое BlogAssets происходит:

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>

Это приведет к удалению и вставке при вызове SaveChanges:

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

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

Время маркировки потерянных объектов как удаленных можно изменить так же, как показано для навигации по коллекции и имеет те же эффекты.

Удаление сущности

Необязательные связи

Если сущность помечается как Deletedвызываемая DbContext.Remove, то ссылки на удаленную сущность удаляются из навигации других сущностей. Для необязательных связей значения FK в зависимых сущностях имеют значение NULL.

Например, давайте помечаем блог Visual Studio следующим образом 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();

Просмотр представления отладки средства отслеживания изменений перед вызовом SaveChanges показывает:

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

Обратите внимание на указанные ниже моменты.

  • Блог помечен как Deleted.
  • Ресурсы, связанные с удаленным блогом, имеют значение null FK (BlogId: <null> FK Modified Originally 2) и навигацию по пустой ссылке (Blog: <null>)
  • Каждая запись, связанная с удаленным блогом, имеет значение NULL FK (BlogId: <null> FK Modified Originally 2) и навигацию по пустой ссылке (Blog: <null>)

Обязательные связи

Поведение исправления для обязательных связей совпадает с необязательными связями, за исключением того, что зависимые или дочерние сущности помечены как Deleted поскольку они не могут существовать без субъекта или родителя и должны быть удалены из базы данных при вызове SaveChanges, чтобы избежать исключения ссылочного ограничения. Это называется каскадным удалением и поведением по умолчанию в EF Core для необходимых связей. Например, выполнение того же кода, что и в предыдущем примере, но с требуемым отношением приводит к следующему представлению отладки перед вызовом SaveChanges:

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

Как и ожидалось, зависимые или дочерние элементы теперь помечаются как Deleted. Однако обратите внимание, что навигации по удаленным сущностям не изменились. Это может показаться странным, но это позволяет избежать полного измельчения удаленного графа сущностей путем очистки всех переходов. То есть блог, ресурс и записи по-прежнему формируют граф сущностей даже после удаления. Это значительно упрощает удаление графа сущностей, чем в EF6, где граф был измельчено.

Каскадное удаление времени и повторное родителей

По умолчанию каскадный удаление происходит, как только родитель или субъект помечаются как Deleted. Это аналогично удалению потерянных объектов, как описано выше. Как и при удалении потерянных объектов, этот процесс может быть отложен до вызова SaveChanges или даже полностью отключен, задав соответствующий параметр ChangeTracker.CascadeDeleteTiming . Это полезно так же, как и для удаления потерянных объектов, в том числе для повторного создания дочерних или зависимых элементов после удаления субъекта или родителя.

Каскадные удаления, а также удаление потерянных объектов могут быть вынуждены в любое время путем вызова ChangeTracker.CascadeChanges(). Объединяя это с настройкой времени каскадного удаления, чтобы Never обеспечить каскадное удаление никогда не происходит, если ef Core явно не будет ручено сделать это.

Совет

Концепции каскадного удаления и удаления потерянных объектов тесно взаимосвязаны. В обоих случаях при разрыве связи с основной или родительской сущностью ее зависимые и дочерние сущности удаляются. При каскадном удалении это происходит вследствие удаления самой основной или родительской сущности. При удалении потерянных объектов основная или родительская сущность по-прежнему существует, но при этом больше не связана с зависимыми и дочерними сущностями.

Связи "много ко многим".

Связи "многие ко многим" в EF Core реализуются с помощью сущности соединения. Каждая сторона связи "многие ко многим" связана с этой сущностью соединения с отношением "один ко многим". До EF Core 5.0 эта сущность соединения должна была явно определяться и сопоставляться. Начиная с EF Core 5.0, его можно создать неявно и скрыто. Однако в обоих случаях базовое поведение одинаково. Сначала рассмотрим это базовое поведение, чтобы понять, как работает отслеживание связей "многие ко многим".

Сколько связей "многие ко многим" работают

Рассмотрим эту модель EF Core, которая создает связь "многие ко многим" между записями и тегами с помощью явно определенного типа сущности соединения:

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
}

Обратите внимание, что тип сущности PostTag соединения содержит два свойства внешнего ключа. В этой модели для публикации, связанной с тегом, должна быть сущность соединения PostTag, в которой PostTag.PostId значение внешнего ключа совпадает Post.Id со значением первичного ключа и где PostTag.TagId значение внешнего ключа соответствует значению первичного Tag.Id ключа. Пример:

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

Просмотр представления отладки средства отслеживания изменений после выполнения этого кода показывает, что запись и тег связаны с новой 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}]

Обратите внимание, что навигация Post по коллекции и Tag исправлена, как и ссылки на PostTag. Эти связи можно управлять навигацией вместо значений FK, как и во всех предыдущих примерах. Например, приведенный выше код можно изменить, чтобы добавить связь, задав ссылочные навигации для сущности соединения:

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

Это приводит к тому же изменению клавиш FK и навигации, что и в предыдущем примере.

Пропуск переходов

Примечание

Переходы по пропуску появились в EF Core 5.0.

Управление таблицей соединения вручную может быть громоздким. Начиная с EF Core 5.0, связи "многие ко многим" можно управлять непосредственно с помощью специальных навигаций по коллекции, которые "пропускают" сущность соединения. Например, две переходы пропуска можно добавить в модель выше; один из записей в теги, а другой — от тега к записям:

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
}

Эта связь "многие ко многим" требует следующей конфигурации, чтобы обеспечить пропуск навигаций и нормальных переходов для одной и той же связи "многие ко многим":

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

Дополнительные сведения о сопоставлении связей "многие ко многим".

Пропуск переходов выглядит и ведет себя как обычные навигации коллекции. Однако способ работы с значениями внешнего ключа отличается. Давайте свяжите запись с тегом, но на этот раз с помощью пропуска навигации:

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

Обратите внимание, что этот код не использует сущность соединения. Вместо этого она просто добавляет сущность в коллекцию навигации так же, как и если бы это было отношение "один ко многим". Полученное представление отладки по сути совпадает со следующими:

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

Обратите внимание, что экземпляр сущности PostTag соединения был создан автоматически со значениями FK, установленными для значений PK тега и записи, которые теперь связаны. Все обычные навигации ссылок и коллекций были исправлены в соответствии с этими значениями FK. Кроме того, так как эта модель содержит пропуск навигации, они также были исправлены. В частности, несмотря на то, что мы добавили тег к Post.Tags навигации skip, Tag.Posts обратная навигация пропуска на другой стороне этой связи также была исправлена, чтобы содержать связанную запись.

Стоит отметить, что базовые связи "многие ко многим" по-прежнему можно манипулировать непосредственно, даже если переходы пропускались поверх. Например, тег и post могут быть связаны, как мы делали, прежде чем вводить переходы по пропуску:

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

Или с помощью значений FK:

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

Это по-прежнему приведет к правильному исправлению переходов пропуска, что приведет к тому же выводу отладочного представления, что и в предыдущем примере.

Пропускать только переходы

В предыдущем разделе мы добавили переходы пропуска в дополнение к полному определению двух базовых связей "один ко многим". Это полезно, чтобы проиллюстрировать, что происходит со значениями FK, но часто не требуется. Вместо этого связь "многие ко многим" можно определить только с помощью пропусков навигаций. Вот как в самой верхней части этого документа определяется связь "многие ко многим". С помощью этой модели можно снова связать post и тег, добавив запись к Tag.Posts переходу (или, кроме того, добавив тег к переходу по пропуску 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);

Просмотр представления отладки после внесения этого изменения показывает, что EF Core создал экземпляр для представления сущности Dictionary<string, object> соединения. Эта сущность соединения содержит свойства внешнего PostsIdTagsId ключа, которые были заданы в соответствии со значениями PK записи и тега, которые связаны.

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

Дополнительные сведения о неявных сущностях соединения и использовании Dictionary<string, object> типов сущностей см. в разделе "Связи".

Важно!

Тип CLR, используемый для типов сущностей соединения по соглашению, может измениться в будущих выпусках, чтобы повысить производительность. Не зависят от типа Dictionary<string, object> соединения, если это не было явно настроено.

Присоединение сущностей с полезными данными

До сих пор все примеры использовали тип сущности соединения (явный или неявный), который содержит только два свойства внешнего ключа, необходимые для связи "многие ко многим". Ни одному из этих значений FK не нужно явно задавать приложением при управлении связями, так как их значения исходят из свойств первичного ключа связанных сущностей. Это позволяет EF Core создавать экземпляры сущности соединения без отсутствующих данных.

Полезные данные с созданными значениями

EF Core поддерживает добавление дополнительных свойств в тип сущности соединения. Это называется предоставление сущности соединения полезными данными. Например, добавим TaggedOn свойство в 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
}

Это свойство полезных данных не будет задано, когда EF Core создает экземпляр сущности соединения. Наиболее распространенным способом решения этой проблемы является использование свойств полезных данных с автоматически созданными значениями. Например, TaggedOn свойство можно настроить для использования метки времени, созданной в магазине, при вставке каждой новой сущности:

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

Теперь запись может быть помечена так же, как и ранее:

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

Просмотр представления отладки средства отслеживания изменений после вызова SaveChanges показывает, что свойство полезных данных задано соответствующим образом:

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

Явное задание значений полезных данных

Далее из предыдущего примера мы добавим свойство полезных данных, которое не использует автоматически созданное значение:

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
}

Теперь запись может быть помечена так же, как и раньше, и сущность соединения по-прежнему будет создана автоматически. Доступ к этой сущности можно получить с помощью одного из механизмов, описанных в разделе "Доступ к отслеживаемой сущности". Например, приведенный ниже код используется DbSet<TEntity>.Find для доступа к экземпляру сущности соединения:

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

После того как сущность соединения будет размещена, ее можно манипулировать обычным способом в этом примере, чтобы задать TaggedBy свойство полезных данных перед вызовом SaveChanges.

Примечание

Обратите внимание, что ChangeTracker.DetectChanges() вызов необходим здесь, чтобы предоставить EF Core возможность обнаружить изменение свойства навигации и создать экземпляр сущности соединения перед Find использованием. Дополнительные сведения см. в статье об обнаружении изменений и уведомлениях .

Кроме того, сущность соединения можно создать явным образом, чтобы связать запись с тегом. Пример:

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

Наконец, другим способом задания полезных данных является переопределение SaveChanges или использование DbContext.SavingChanges события для обработки сущностей перед обновлением базы данных. Пример:

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

    return base.SaveChanges();
}