Отладка отслеживания изменений

Средство отслеживания изменений Entity Framework Core (EF Core) создает два типа выходных данных для отладки:

  • Данный компонент ChangeTracker.DebugView обеспечивает удобочитаемое представление всех отслеживаемых сущностей.
  • Сообщения журнала на уровне отладки создаются, когда трекер изменений обнаруживает состояние и исправляет связи.

Совет

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

Совет

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

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

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

Доступ к режиму отладки трекера изменений из отладчика Visual Studio

Доступ к нему также можно получить непосредственно из кода, например для отправки представления отладки в консоль:

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

Представление отладки имеет короткую форму и длинную форму. В короткой форме показаны отслеживаемые сущности, их состояние и ключевые значения. Длинная форма также включает все значения свойств, а также значения навигации и состояние.

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

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

using var context = new BlogsContext();

var blogs = await context.Blogs
    .Include(e => e.Posts).ThenInclude(e => e.Tags)
    .Include(e => e.Assets)
    .ToListAsync();

// Mark something Added
blogs[0].Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many new features and..."
    });

// Mark something Deleted
blogs[1].Posts.Remove(blogs[1].Posts[1]);

// Make something Modified
blogs[0].Name = ".NET Blog (All new!)";

context.ChangeTracker.DetectChanges();

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

Blog {Id: 1} Modified AK {AssetsId: ed727978-1ffe-4709-baee-73913e8e44a0}
Blog {Id: 2} Unchanged AK {AssetsId: 3a54b880-2b9d-486b-9403-dc2e52d36d65}
BlogAssets {Id: 3a54b880-2b9d-486b-9403-dc2e52d36d65} Unchanged FK {Id: 3a54b880-2b9d-486b-9403-dc2e52d36d65}
BlogAssets {Id: ed727978-1ffe-4709-baee-73913e8e44a0} Unchanged FK {Id: ed727978-1ffe-4709-baee-73913e8e44a0}
Post {Id: -2147482643} Added FK {BlogId: 1}
Post {Id: 1} Unchanged FK {BlogId: 1}
Post {Id: 2} Unchanged FK {BlogId: 1}
Post {Id: 3} Unchanged FK {BlogId: 2}
Post {Id: 4} Deleted FK {BlogId: 2}
PostTag (Dictionary<string, object>) {PostsId: 1, TagsId: 1} Unchanged FK {PostsId: 1} FK {TagsId: 1}
PostTag (Dictionary<string, object>) {PostsId: 1, TagsId: 3} Unchanged FK {PostsId: 1} FK {TagsId: 3}
PostTag (Dictionary<string, object>) {PostsId: 2, TagsId: 1} Unchanged FK {PostsId: 2} FK {TagsId: 1}
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 2} Unchanged FK {PostsId: 3} FK {TagsId: 2}
PostTag (Dictionary<string, object>) {PostsId: 4, TagsId: 2} Deleted FK {PostsId: 4} FK {TagsId: 2}
Tag {Id: 1} Unchanged
Tag {Id: 2} Unchanged
Tag {Id: 3} Unchanged

Примечание.

  • Каждая отслеживаемая сущность отображается со значением первичного ключа (PK). Например, Blog {Id: 1}.
  • Если сущность является типом разделяемой сущности, то также отображается её тип CLR. Например, PostTag (Dictionary<string, object>).
  • Следующий элемент EntityState отображается. Это будет один из Unchanged, Addedили ModifiedDeleted.
  • Значения для любых альтернативных ключей (AKs) показаны ниже. Например, AK {AssetsId: ed727978-1ffe-4709-baee-73913e8e44a0}.
  • Наконец, отображаются значения для всех внешних ключей (FKs). Например, FK {PostsId: 4} FK {TagsId: 2}.

Долгосрочная перспектива

Длинное представление можно отправить в консоль так же, как и короткое представление:

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

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

Blog {Id: 1} Modified
  Id: 1 PK
  AssetsId: 'ed727978-1ffe-4709-baee-73913e8e44a0' AK
  Name: '.NET Blog (All new!)' Modified Originally '.NET Blog'
  Assets: {Id: ed727978-1ffe-4709-baee-73913e8e44a0}
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  AssetsId: '3a54b880-2b9d-486b-9403-dc2e52d36d65' AK
  Name: 'Visual Studio Blog'
  Assets: {Id: 3a54b880-2b9d-486b-9403-dc2e52d36d65}
  Posts: [{Id: 3}]
BlogAssets {Id: 3a54b880-2b9d-486b-9403-dc2e52d36d65} Unchanged
  Id: '3a54b880-2b9d-486b-9403-dc2e52d36d65' PK FK
  Banner: <null>
  Blog: {Id: 2}
BlogAssets {Id: ed727978-1ffe-4709-baee-73913e8e44a0} Unchanged
  Id: 'ed727978-1ffe-4709-baee-73913e8e44a0' PK FK
  Banner: <null>
  Blog: {Id: 1}
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many new fe...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
  Tags: []
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: [{Id: 1}, {Id: 3}]
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: [{Id: 1}]
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: [{Id: 2}]
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: <null>
  Tags: [{Id: 2}]
PostTag (Dictionary<string, object>) {PostsId: 1, TagsId: 1} Unchanged
  PostsId: 1 PK FK
  TagsId: 1 PK FK
PostTag (Dictionary<string, object>) {PostsId: 1, TagsId: 3} Unchanged
  PostsId: 1 PK FK
  TagsId: 3 PK FK
PostTag (Dictionary<string, object>) {PostsId: 2, TagsId: 1} Unchanged
  PostsId: 2 PK FK
  TagsId: 1 PK FK
PostTag (Dictionary<string, object>) {PostsId: 3, TagsId: 2} Unchanged
  PostsId: 3 PK FK
  TagsId: 2 PK FK
PostTag (Dictionary<string, object>) {PostsId: 4, TagsId: 2} Deleted
  PostsId: 4 PK FK
  TagsId: 2 PK FK
Tag {Id: 1} Unchanged
  Id: 1 PK
  Text: '.NET'
  Posts: [{Id: 1}, {Id: 2}]
Tag {Id: 2} Unchanged
  Id: 2 PK
  Text: 'Visual Studio'
  Posts: [{Id: 3}, {Id: 4}]
Tag {Id: 3} Unchanged
  Id: 3 PK
  Text: 'EF Core'
  Posts: [{Id: 1}]

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

Значения свойств

Для каждого свойства длинное представление показывает, является ли свойство частью первичного ключа (PK), альтернативным ключом (AK) или внешним ключом (FK). Например:

  • Blog.Id — это свойство первичного ключа: Id: 1 PK
  • Blog.AssetsId — это альтернативное свойство ключа: AssetsId: 'ed727978-1ffe-4709-baee-73913e8e44a0' AK
  • Post.BlogId — это свойство внешнего ключа: BlogId: 2 FK
  • BlogAssets.Id является первичным ключом и свойством внешнего ключа: Id: '3a54b880-2b9d-486b-9403-dc2e52d36d65' PK FK

Измененные значения свойств помечаются таким образом, а исходное значение свойства также отображается. Например, Name: '.NET Blog (All new!)' Modified Originally '.NET Blog'.

Наконец, сущности с временными значениями ключей указывают на то, Added что значение является временным. Например, Id: -2147482643 PK Temporary.

Значения навигации отображаются с использованием значений первичных ключей тех сущностей, на которые они ссылаются. Например, в выходных данных выше запись 3 связана с блогом 2. Это означает, что навигация Post.Blog указывает на экземпляр Blog с идентификатором 2. Это показано как Blog: {Id: 2}.

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

Ведение журнала отслеживания изменений

Средство отслеживания изменений регистрирует сообщения в DebugLogLevel всякий раз, когда обнаруживает изменения свойств или навигации. Например, когда ChangeTracker.DetectChanges() вызывается, как описано в начале этого документа, и включен отладочный журнал, создаются следующие записи журнала:

dbug: 12/30/2020 13:52:44.815 CoreEventId.DetectChangesStarting[10800] (Microsoft.EntityFrameworkCore.ChangeTracking)
      DetectChanges starting for 'BlogsContext'.
dbug: 12/30/2020 13:52:44.818 CoreEventId.PropertyChangeDetected[10802] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The unchanged property 'Blog.Name' was detected as changed from '.NET Blog' to '.NET Blog (All new!)' and will be marked as modified for entity with key '{Id: 1}'.
dbug: 12/30/2020 13:52:44.820 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The 'Blog' entity with key '{Id: 1}' tracked by 'BlogsContext' changed state from 'Unchanged' to 'Modified'.
dbug: 12/30/2020 13:52:44.821 CoreEventId.CollectionChangeDetected[10804] (Microsoft.EntityFrameworkCore.ChangeTracking)
      1 entities were added and 0 entities were removed from navigation 'Blog.Posts' on entity with key '{Id: 1}'.
dbug: 12/30/2020 13:52:44.822 CoreEventId.ValueGenerated[10808] (Microsoft.EntityFrameworkCore.ChangeTracking)
      'BlogsContext' generated temporary value '-2147482638' for the property 'Id.Post'.
dbug: 12/30/2020 13:52:44.822 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'BlogsContext' started tracking 'Post' entity with key '{Id: -2147482638}'.
dbug: 12/30/2020 13:52:44.827 CoreEventId.CollectionChangeDetected[10804] (Microsoft.EntityFrameworkCore.ChangeTracking)
      0 entities were added and 1 entities were removed from navigation 'Blog.Posts' on entity with key '{Id: 2}'.
dbug: 12/30/2020 13:52:44.827 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The 'Post' entity with key '{Id: 4}' tracked by 'BlogsContext' changed state from 'Unchanged' to 'Modified'.
dbug: 12/30/2020 13:52:44.829 CoreEventId.CascadeDeleteOrphan[10003] (Microsoft.EntityFrameworkCore.Update)
      An entity of type 'Post' with key '{Id: 4}' changed to 'Deleted' state due to severed required relationship to its parent entity of type 'Blog'.
dbug: 12/30/2020 13:52:44.829 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The 'Post' entity with key '{Id: 4}' tracked by 'BlogsContext' changed state from 'Modified' to 'Deleted'.
dbug: 12/30/2020 13:52:44.829 CoreEventId.CollectionChangeDetected[10804] (Microsoft.EntityFrameworkCore.ChangeTracking)
      0 entities were added and 1 entities were removed from navigation 'Blog.Posts' on entity with key '{Id: 2}'.
dbug: 12/30/2020 13:52:44.831 CoreEventId.CascadeDelete[10002] (Microsoft.EntityFrameworkCore.Update)
      A cascade state change of an entity of type 'PostTag' with key '{PostsId: 4, TagsId: 2}' to 'Deleted' occurred due to the deletion of its parent entity of type 'Post' with key '{Id: 4}'.
dbug: 12/30/2020 13:52:44.831 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
      The 'PostTag' entity with key '{PostsId: 4, TagsId: 2}' tracked by 'BlogsContext' changed state from 'Unchanged' to 'Deleted'.
dbug: 12/30/2020 13:52:44.831 CoreEventId.DetectChangesCompleted[10801] (Microsoft.EntityFrameworkCore.ChangeTracking)
      DetectChanges completed for 'BlogsContext'.

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

Идентификатор события Описание
CoreEventId.DetectChangesStarting DetectChanges() запускается
CoreEventId.DetectChangesCompleted DetectChanges() завершено
CoreEventId.PropertyChangeDetected Обычное значение свойства изменилось
CoreEventId.ForeignKeyChangeDetected Изменено значение свойства внешнего ключа
CoreEventId.CollectionChangeDetected Навигация по коллекции без пропуска содержала добавленные или удаленные связанные сущности.
CoreEventId.ReferenceChangeDetected Навигация по ссылке была изменена, чтобы указать другую сущность или задать значение NULL
CoreEventId.StartedTracking EF Core приступил к отслеживанию сущности
CoreEventId.StateChanged Изменился EntityState сущности
CoreEventId.ValueGenerated Значение было создано для свойства
CoreEventId.SkipCollectionChangeDetected Навигация по пропускающей коллекции была изменена: добавлены или удалены связанные сущности.

Модель

Модель, используемая для приведенных выше примеров, содержит следующие типы сущностей:

public class Blog
{
    public int Id { get; set; } // Primary key
    public Guid AssetsId { get; set; } // Alternate 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 Guid Id { get; set; } // Primary key and foreign key
    public byte[] Banner { get; set; }

    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
}

Модель в основном настраивается по соглашению: это требует всего несколько строк в методе OnModelCreating.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Blog>()
        .Property(e => e.AssetsId)
        .ValueGeneratedOnAdd();

    modelBuilder
        .Entity<BlogAssets>()
        .HasOne(e => e.Blog)
        .WithOne(e => e.Assets)
        .HasForeignKey<BlogAssets>(e => e.Id)
        .HasPrincipalKey<Blog>(e => e.AssetsId);
}