変更トラッカーによるデバッグ

Entity Framework Core (EF Core) の変更トラッカーは、デバッグに役立つ 2 種類の出力を生成します。

  • ChangeTracker.DebugView は、追跡対象のすべてのエンティティについて、人が判読可能なビューを提供します
  • 変更トラッカーによって、状態と、リレーションシップの修正が検出されると、デバッグレベルのログ メッセージが生成されます

ヒント

このドキュメントは、エンティティの状態と、EF Core での変更の追跡に関する基本を理解していることが前提となっています。 これらのトピックの詳細については、「EF Core での変更の追跡」を参照してください。

ヒント

このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。

変更トラッカーのデバッグ ビュー

変更トラッカーのデバッグ ビューには、IDE のデバッガーでアクセスできます。 たとえば、Visual Studio の場合は次のようになります。

Accessing the change tracker debug view from the Visual Studio debugger

また、コードから直接アクセスすることもできます。たとえば、コンソールにデバッグ ビューを送信することができます。

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 を次に示します。 これは、UnchangedAddedModified、または 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 がそれぞれ指定された 3 つのエンティティが含まれています。 これは、[{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'.

次の表に、変更トラッカーのログ メッセージの概要を示します。

イベント 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);
}