변경 내용 추적기 디버깅

EF Core(Entity Framework Core) 변경 추적기는 디버깅에 도움이 되는 두 가지 종류의 출력을 생성합니다.

  • ChangeTracker.DebugView는 추적 중인 모든 엔터티에 대해 사람이 읽을 수 있는 보기를 제공합니다.
  • 디버그 수준 로그 메시지는 변경 추적기가 상태를 검색하고 관계를 수정할 때 생성됩니다.

이 문서에서는 엔터티 상태와 EF Core 변경 내용 추적의 기본 사항을 이해한다고 가정합니다. 이러한 항목에 대한 자세한 내용은 EF Core의 변경 내용 추적을 참조하세요.

GitHub에서 샘플 코드를 다운로드하여 이 문서의 모든 코드를 실행하고 디버그할 수 있습니다.

변경 내용 추적기 디버그 보기

변경 내용 추적기 디버그 보기는 IDE의 디버거에서 액세스할 수 있습니다. 예를 들어 Visual Studio를 사용하면 다음과 같습니다.

Visual Studio 디버거에서 변경 추적기 디버그 보기에 액세스

코드에서 직접 액세스할 수도 있습니다. 예를 들어 디버그 보기를 콘솔로 보냅니다.

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

디버그 보기에는 짧은 양식과 긴 양식이 있습니다. 짧은 양식에는 추적된 엔터티, 해당 상태 및 키 값이 표시됩니다. 긴 양식에는 모든 속성과 탐색 값 및 상태도 포함됩니다.

짧은 보기

이 문서의 끝에 표시된 모델을 사용하여 디버그 보기 예제를 살펴보겠습니다. 먼저 일부 엔터티를 추적하고 다른 상태로 배치하므로 변경 내용 추적 데이터를 볼 수 있습니다.

using var context = new BlogsContext();

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

// 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, Modified, Deleted 중 하나가 됩니다.
  • AK(대체 키)에 대한 값은 다음에 표시됩니다. 예: AK {AssetsId: ed727978-1ffe-4709-baee-73913e8e44a0}.
  • 마지막으로 모든 외래 키(FK)에 대한 값이 표시됩니다. 예: 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 탐색은 ID가 2인 Blog 인스턴스를 가리킵니다. 이는 Blog: {Id: 2}로 표시됩니다.

이 경우 여러 관련 엔터티가 있을 수 있다는 점을 제외하고 컬렉션 탐색에도 동일합니다. 예를 들어 컬렉션 탐색 Blog.Posts에는 각각 키 값 1, 2 및 -2147482643이 있는 세 개의 엔터티가 포함됩니다. 이는 [{Id: 1}, {Id: 2}, {Id: -2147482643}]으로 표시됩니다.

변경 내용 추적기 로깅

변경 추적기는 속성 또는 탐색 변경 내용을 검색할 때마다 Debug LogLevel에서 메시지를 기록합니다. 예를 들어 이 문서의 맨 위에 있는 코드에서 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'.

다음 표에서는 변경 내용 추적기 로깅 메시지를 요약합니다.

이벤트 ID 설명
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);
}