Поделиться через


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

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

Связи в модели Entity Framework Core (EF Core) представлены с помощью внешних ключей (FKs). 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 — это зависимый или дочерний объект. Он содержит свойство FK Post.BlogId, значение которого должно соответствовать значению PK Blog.Id связанного блога.
    • Post.Blog — это справочная навигация из записи в связанный блог. Post.Blog — это обратная навигация для Blog.Posts.
    • Blog.Posts — это система навигации блога, ведущая ко всем связанным записям. Blog.Posts — это обратная навигация для Post.Blog.
  • Каждый блог может иметь один актив (один к одному):
    • Blog — это субъект или родитель.
    • BlogAssets — это зависимый или дочерний объект. Он содержит свойство FK BlogAssets.BlogId, значение которого должно соответствовать значению PK Blog.Id связанного блога.
    • 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .ToListAsync();

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 = await context.Blogs.ToListAsync();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

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

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

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 = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");
var vsBlog = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == "Visual Studio Blog");

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

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

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

await context.SaveChangesAsync();

Подсказка

Вызов 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);

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

  • FK Post.BlogId имеет значение 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);

await context.SaveChangesAsync();

После удаления записи из первой коллекции объект не помечается как 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 = await context.Blogs.Include(e => e.Posts).SingleAsync(e => e.Name == ".NET Blog");

context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never;

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

await context.SaveChangesAsync(); // Throws

Будет выброшено исключение:

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

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

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

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

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

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

using var context = new BlogsContext();

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

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

await context.SaveChangesAsync();

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

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

Это приводит к обновлению и вставке при вызове 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 = await context.Blogs
    .Include(e => e.Posts)
    .Include(e => e.Assets)
    .SingleAsync(e => e.Name == "Visual Studio Blog");

context.Remove(vsBlog);

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

await context.SaveChangesAsync();

Просмотр представления отладки отслеживания изменений перед тем, как вызвать 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) и навигацию со ссылкой NULL (Blog: <null>)
  • Каждая запись, связанная с удаленным блогом, имеет значение NULL FK (BlogId: <null> FK Modified Originally 2) и навигацию со ссылкой NULL (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, которая создаёт отношение "многие ко многим" между записями и тегами с помощью явно определенного типа связующей сущности.

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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

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

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

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

Пропуск навигации

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

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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

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

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

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 пропуску навигации, Tag.Posts обратный переход пропуска навигации на другой стороне этой связи также был исправлен, чтобы содержать связанный пост.

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

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

Или использование значений FK:

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

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

Пропускать только навигации

В предыдущем разделе мы добавили навигации для пропуска в дополнение к полному определению двух базовых связей "один-к-многим". Это полезно для иллюстрации того, что происходит со значениями FK, но часто не требуется. Вместо этого можно определить связь "многие ко многим" только с помощью пропуска навигаций. Так в модели на самом верху этого документа определяется связь «многие ко многим». Используя эту модель, мы можем снова связать сообщение и тег, добавив сообщение в навигацию по переходу Tag.Posts (или, наоборот, добавив тег в навигацию по переходу Post.Tags).

using var context = new BlogsContext();

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

post.Tags.Add(tag);

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

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

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>, если это явно не настроено.

Соединение сущностей с данными payload

До сих пор все примеры использовали тип сущности соединения (явный или неявный), содержащий только два свойства внешнего ключа, необходимые для связи "многие ко многим". Ни из этих значений 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 = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

post.Tags.Add(tag);

await context.SaveChangesAsync();

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

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

Теперь публикация может быть помечена так же, как и раньше, и объединяющая сущность по-прежнему будет создана автоматически. После этого доступ к этой сущности можно получить с помощью одного из механизмов, описанных в разделе Accessing Tracked Entity. Например, в приведённом ниже коде используется DbSet<TEntity>.Find, чтобы получить доступ к экземпляру сущности связи.

using var context = new BlogsContext();

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

post.Tags.Add(tag);

context.ChangeTracker.DetectChanges();

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

joinEntity.TaggedBy = "ajcvickers";

await context.SaveChangesAsync();

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

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

Замечание

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

Кроме того, объект соединения можно создать явно, чтобы связать пост с тегом. Рассмотрим пример.

using var context = new BlogsContext();

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

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

await context.SaveChangesAsync();

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

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

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

    return await base.SaveChangesAsync(cancellationToken);
}