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


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

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

Связи в модели 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 — это зависимый или дочерний объект. Он содержит свойство 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);

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

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

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: связь между сущностями "Блог" и "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();

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

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

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

Если сущность помечена как DeletedDbContext.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) и навигацию со ссылкой 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 = 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, как и ссылочные навигацииPostTag.Tag Эти связи можно управлять навигациями вместо значений 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 = 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 переходу, 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> для представления сущности соединения. Эта сущность соединения содержит PostsId TagsId свойства внешнего ключа, которые были заданы для сопоставления значений 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
}

Теперь запись может быть помечена так же, как и раньше, и сущность соединения по-прежнему будет создана автоматически. После этого доступ к этой сущности можно получить с помощью одного из механизмов, описанных в разделе Accessing Tracked Entity. Например, приведенный ниже код используется 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();
}