Share via


EF Core での ID 解決

DbContext では、指定された主キー値を持つエンティティ インスタンスを 1 つだけ追跡できます。 これは、同じキー値を持つエンティティの複数のインスタンスを 1 つのインスタンスに解決する必要があることを意味します。 これは "ID 解決" と呼ばれます。 ID 解決により、エンティティのリレーションシップやプロパティ値についてあいまいさのない一貫したグラフが Entity Framework Core (EF Core) で追跡されます。

ヒント

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

ヒント

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

はじめに

次のコードでは、エンティティに対してクエリを実行してから、同じ主キー値を持つ別のインスタンスをアタッチしようとします。

using var context = new BlogsContext();

var blogA = context.Blogs.Single(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };

try
{
    context.Update(blogB); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

このコードを実行すると、次の例外が発生します。

System.InvalidOperationException: キー値 '{Id: 1}' を持つ別のインスタンスが既に追跡されているため、エンティティ型 'Blog' のインスタンスを追跡できません。 既存のエンティティをアタッチする場合、ある特定のキー値を持つエンティティ インスタンスは 1 つだけアタッチするようにしてください。

次の理由により、EF Core には単一のインスタンスが必要です。

  • プロパティ値は、複数のインスタンス間で異なる場合があります。 データベースを更新するときは、使用するプロパティ値を EF Core が認識している必要があります。
  • 他のエンティティとのリレーションシップは、複数のインスタンス間で異なる場合があります。 たとえば、"ブログ A" は、"ブログ B" とは異なる投稿のコレクションに関連付けられている場合があります。

上記の例外は、通常、次のような状況で発生します。

  • エンティティを更新しようとしたとき
  • シリアル化されたエンティティのグラフを追跡しようとしたとき
  • 自動的には生成されないキー値の設定に失敗したとき
  • 複数の作業単位に対して DbContext インスタンスを再利用するとき

これらの各状況については、以降のセクションで説明します。

エンティティの更新

エンティティを新しい値で更新する方法はいくつかあります。これについては、「EF Core での変更の追跡」と「エンティティの明示的な追跡」で説明されています。 これらの方法の概要を ID 解決の観点から以下に説明します。 注意すべき重要な点は、各方法ではクエリを使用するか、Update または Attach のいずれかの呼び出しを使用しますが、両方を使用することはないということです。

Update を呼び出す

更新するエンティティは、SaveChanges に使用する DbContext に対するクエリから取得されたものではないことがよくあります。 たとえば、Web アプリケーションで、POST 要求の情報からエンティティ インスタンスが作成される場合があります。 これを処理する最も簡単な方法は、DbContext.Update または DbSet<TEntity>.Update を使用することです。 次に例を示します。

public static void UpdateFromHttpPost1(Blog blog)
{
    using var context = new BlogsContext();

    context.Update(blog);

    context.SaveChanges();
}

この場合、次のようになります。

  • エンティティのインスタンスが 1 つだけ作成されます。
  • このエンティティ インスタンスは、更新の一環としてデータベースからは照会されません
  • 実際に変更されているかどうかに関係なく、すべてのプロパティ値がデータベースで更新されます。
  • 1 回のデータベース ラウンド トリップが行われます。

クエリを実行し、変更を適用する

通常、POST 要求などの情報からエンティティが作成されるときに、どのプロパティ値が実際に変更されているかは不明です。 多くの場合、前の例で行ったように、データベース内のすべての値を更新するだけで十分です。 ただし、アプリケーションが多数のエンティティを処理していて、そのうちの少数だけが実際に変更されている場合は、送信される更新を制限すると役立つことがあります。 これを実現するには、クエリを実行して、データベース内に現在存在しているエンティティを追跡してから、それらの追跡対象エンティティに変更を適用します。 次に例を示します。

public static void UpdateFromHttpPost2(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    trackedBlog.Name = blog.Name;
    trackedBlog.Summary = blog.Summary;

    context.SaveChanges();
}

この場合、次のようになります。

  • エンティティのインスタンスが 1 つだけ追跡されます。Find クエリによってデータベースから返されたものです。
  • UpdateAttach、などは使用されません
  • 実際に変更されているプロパティ値だけがデータベースで更新されます。
  • 2 回のデータベース ラウンド トリップが行われます。

EF Core には、このようなプロパティ値を転送するためのヘルパーがいくつかあります。 たとえば、PropertyValues.SetValues では指定されたオブジェクトからすべての値をコピーし、追跡対象オブジェクトに設定します。

public static void UpdateFromHttpPost3(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(blog);

    context.SaveChanges();
}

SetValues は、エンティティ型のプロパティと一致するプロパティ名を持つデータ転送オブジェクト (DTO) など、さまざまなオブジェクトの種類を受け入れます。 次に例を示します。

public static void UpdateFromHttpPost4(BlogDto dto)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(dto.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(dto);

    context.SaveChanges();
}

または、名前/値のディクショナリをプロパティ値として受け入れます。

public static void UpdateFromHttpPost5(Dictionary<string, object> propertyValues)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(propertyValues["Id"]);

    context.Entry(trackedBlog).CurrentValues.SetValues(propertyValues);

    context.SaveChanges();
}

このようなプロパティ値の使用方法の詳細については、「追跡対象のエンティティへのアクセス」を参照してください。

元の値を使用する

ここまでの各方法では、クエリを実行してから更新を行うか、変更されているかどうかに関係なくすべてのプロパティ値を更新しました。 変更された値のみを、クエリを実行せずに更新の一環として更新するには、変更されたプロパティ値に関する特定の情報が必要です。 この情報を取得する一般的な方法は、現在の値と元の値の両方を HTTP Post などで送り返すことです。 次に例を示します。

public static void UpdateFromHttpPost6(Blog blog, Dictionary<string, object> originalValues)
{
    using var context = new BlogsContext();

    context.Attach(blog);
    context.Entry(blog).OriginalValues.SetValues(originalValues);

    context.SaveChanges();
}

このコードでは、変更された値を持つエンティティが最初にアタッチされます。 これにより、EF Core では、エンティティが Unchanged 状態で追跡されます。つまり、どのプロパティ値も変更ありとしてマークされていない状態です。 次に、元の値のディクショナリがこの追跡対象エンティティに適用されます。 これにより、現在の値と元の値が異なっているプロパティが、変更ありとしてマークされます。 現在の値と元の値が同じプロパティは、変更ありとしてマークされません。

この場合、次のようになります。

  • Attach を使用して、エンティティのインスタンスが 1 つだけ追跡されます。
  • このエンティティ インスタンスは、更新の一環としてデータベースからは照会されません
  • 元の値を適用することで、実際に変更されているプロパティ値だけがデータベースで更新されるようになります。
  • 1 回のデータベース ラウンド トリップが行われます。

前のセクションの例と同様に、元の値をディクショナリとして渡す必要はありません。エンティティ インスタンスまたは DTO も機能します。

ヒント

この方法には魅力的な特徴がありますが、エンティティの元の値を Web クライアントとの間で送受信する必要があります。 この複雑さが増すこととメリットを慎重に比較検討してください。多くのアプリケーションでは、より単純な方法の方が実用的です。

シリアル化されたグラフのアタッチ

EF Core では、「外部キーとナビゲーションの変更」で説明されているように、外部キーとナビゲーション プロパティを介して接続されたエンティティのグラフを操作します。 これらのグラフは、EF Core の外部で、たとえば JSON ファイルから作成されている場合、同じエンティティの複数のインスタンスを含んでいることがあります。 グラフを追跡するには、その前にこれらの重複を 1 つのインスタンスに解決する必要があります。

重複のないグラフ

先に進む前に、次の点を認識しておくことが重要です。

  • 多くの場合、シリアライザーには、グラフ内のループや重複インスタンスを処理するオプションがあります。
  • グラフ ルートとして使用するオブジェクトの選択によって、多くの場合、重複を減らしたり除去したりできます。

可能な場合は、シリアル化オプションを使用して、重複が発生しないルートを選択してください。 たとえば、次のコードでは、Json.NET を使用して、ブログのリストをそれぞれに関連付けられた投稿と共にシリアル化しています。

using var context = new BlogsContext();

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

var serialized = JsonConvert.SerializeObject(
    blogs,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

このコードから生成される JSON は次のとおりです。

[
  {
    "Id": 1,
    "Name": ".NET Blog",
    "Summary": "Posts about .NET",
    "Posts": [
      {
        "Id": 1,
        "Title": "Announcing the Release of EF Core 5.0",
        "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
        "BlogId": 1
      },
      {
        "Id": 2,
        "Title": "Announcing F# 5",
        "Content": "F# 5 is the latest version of F#, the functional programming language...",
        "BlogId": 1
      }
    ]
  },
  {
    "Id": 2,
    "Name": "Visual Studio Blog",
    "Summary": "Posts about Visual Studio",
    "Posts": [
      {
        "Id": 3,
        "Title": "Disassembly improvements for optimized managed debugging",
        "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
        "BlogId": 2
      },
      {
        "Id": 4,
        "Title": "Database Profiling with Visual Studio",
        "Content": "Examine when database queries were executed and measure how long the take using...",
        "BlogId": 2
      }
    ]
  }
]

この JSON には、重複するブログや投稿がないことに注目してください。 これは、Update の単純な呼び出しによって、データベース内のこれらのエンティティを更新できることを意味します。

public static void UpdateBlogsFromJson(string json)
{
    using var context = new BlogsContext();

    var blogs = JsonConvert.DeserializeObject<List<Blog>>(json);

    foreach (var blog in blogs)
    {
        context.Update(blog);
    }

    context.SaveChanges();
}

重複の処理

前の例のコードでは、各ブログをそれに関連付けられた投稿と共にシリアル化しました。 これを、各投稿をそれに関連付けられたブログと共にシリアル化するように変更すると、シリアル化された JSON に重複が含まれるようになります。 次に例を示します。

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

シリアル化された JSON は次のようになります。

[
  {
    "Id": 1,
    "Title": "Announcing the Release of EF Core 5.0",
    "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 2,
          "Title": "Announcing F# 5",
          "Content": "F# 5 is the latest version of F#, the functional programming language...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 2,
    "Title": "Announcing F# 5",
    "Content": "F# 5 is the latest version of F#, the functional programming language...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 1,
          "Title": "Announcing the Release of EF Core 5.0",
          "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 3,
    "Title": "Disassembly improvements for optimized managed debugging",
    "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 4,
          "Title": "Database Profiling with Visual Studio",
          "Content": "Examine when database queries were executed and measure how long the take using...",
          "BlogId": 2
        }
      ]
    }
  },
  {
    "Id": 4,
    "Title": "Database Profiling with Visual Studio",
    "Content": "Examine when database queries were executed and measure how long the take using...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 3,
          "Title": "Disassembly improvements for optimized managed debugging",
          "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
          "BlogId": 2
        }
      ]
    }
  }
]

グラフには、同じキー値を持つ複数のブログ インスタンスと、同じキー値を持つ複数の投稿インスタンスが含まれるようになりました。 前の例のようにこのグラフを追跡しようとすると、次の例外がスローされます。

System.InvalidOperationException: キー値 '{Id: 2}' を持つ別のインスタンスが既に追跡されているため、エンティティ型 'Post' のインスタンスを追跡できません。 既存のエンティティをアタッチする場合、ある特定のキー値を持つエンティティ インスタンスは 1 つだけアタッチするようにしてください。

これを修正するには、次の 2 つの方法があります。

  • 参照を保持する JSON シリアル化オプションを使用する
  • グラフの追跡中に ID 解決を実行する

参照を保持する

Json.NET には、これを処理する PreserveReferencesHandling オプションが用意されています。 次に例を示します。

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.All, Formatting = Formatting.Indented
    });

結果として得られる JSON は次のようになります。

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "Id": 1,
      "Title": "Announcing the Release of EF Core 5.0",
      "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
      "BlogId": 1,
      "Blog": {
        "$id": "3",
        "Id": 1,
        "Name": ".NET Blog",
        "Summary": "Posts about .NET",
        "Posts": [
          {
            "$ref": "2"
          },
          {
            "$id": "4",
            "Id": 2,
            "Title": "Announcing F# 5",
            "Content": "F# 5 is the latest version of F#, the functional programming language...",
            "BlogId": 1,
            "Blog": {
              "$ref": "3"
            }
          }
        ]
      }
    },
    {
      "$ref": "4"
    },
    {
      "$id": "5",
      "Id": 3,
      "Title": "Disassembly improvements for optimized managed debugging",
      "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
      "BlogId": 2,
      "Blog": {
        "$id": "6",
        "Id": 2,
        "Name": "Visual Studio Blog",
        "Summary": "Posts about Visual Studio",
        "Posts": [
          {
            "$ref": "5"
          },
          {
            "$id": "7",
            "Id": 4,
            "Title": "Database Profiling with Visual Studio",
            "Content": "Examine when database queries were executed and measure how long the take using...",
            "BlogId": 2,
            "Blog": {
              "$ref": "6"
            }
          }
        ]
      }
    },
    {
      "$ref": "7"
    }
  ]
}

この JSON では、重複が、グラフ内の既存のインスタンスを参照する "$ref": "5" のような参照に置き換えられていることに注目してください。 このグラフも、前述のように、Update の単純な呼び出しによって追跡できます。

.NET 基本クラス ライブラリ (BCL) での System.Text.Json のサポートには、同じ結果が得られる同様のオプションがあります。 次に例を示します。

var serialized = JsonSerializer.Serialize(
    posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });

重複を解決する

シリアル化プロセスで重複を排除できない場合は、ChangeTracker.TrackGraph でこれを処理できます。 TrackGraph は、追跡の前にすべてのエンティティ インスタンスのコールバックを生成することを除き、AddAttachUpdate と同様に機能します。 このコールバックを使用して、エンティティを追跡するか無視することができます。 次に例を示します。

public static void UpdatePostsFromJsonWithIdentityResolution(string json)
{
    using var context = new BlogsContext();

    var posts = JsonConvert.DeserializeObject<List<Post>>(json);

    foreach (var post in posts)
    {
        context.ChangeTracker.TrackGraph(
            post, node =>
            {
                var keyValue = node.Entry.Property("Id").CurrentValue;
                var entityType = node.Entry.Metadata;

                var existingEntity = node.Entry.Context.ChangeTracker.Entries()
                    .FirstOrDefault(
                        e => Equals(e.Metadata, entityType)
                             && Equals(e.Property("Id").CurrentValue, keyValue));

                if (existingEntity == null)
                {
                    Console.WriteLine($"Tracking {entityType.DisplayName()} entity with key value {keyValue}");

                    node.Entry.State = EntityState.Modified;
                }
                else
                {
                    Console.WriteLine($"Discarding duplicate {entityType.DisplayName()} entity with key value {keyValue}");
                }
            });
    }

    context.SaveChanges();
}

このコードでは、グラフ内のエンティティごとに次のことが行われます。

  • エンティティのエンティティ型とキー値を見つけます
  • 変更トラッカーでこのキーを使用してエンティティを検索します
    • エンティティが見つかった場合、エンティティは重複しているため、それ以上のアクションは実行されません
    • エンティティが見つからなかった場合は、状態を Modified に設定して追跡します

このコードを実行すると、出力は次のようになります。

Tracking EntityType: Post entity with key value 1
Tracking EntityType: Blog entity with key value 1
Tracking EntityType: Post entity with key value 2
Discarding duplicate EntityType: Post entity with key value 2
Tracking EntityType: Post entity with key value 3
Tracking EntityType: Blog entity with key value 2
Tracking EntityType: Post entity with key value 4
Discarding duplicate EntityType: Post entity with key value 4

重要

このコードでは、すべての重複が同一であると想定されています。 このため、重複の 1 つを任意に選択して追跡し、他の複製を破棄しても安全です。 重複が異なる可能性がある場合、コードでは、使用するものを選択する方法と、プロパティとナビゲーションの値を組み合わせる方法を決定する必要があります。

Note

わかりやすくするために、このコードでは、各エンティティに Id という名前の主キー プロパティがあることが前提となっています。 これは、抽象基本クラスやインターフェイスにコード化できます。 または、このコードがどの型のエンティティでも機能するように、IEntityType メタデータから 1 つまたは複数の主キー プロパティを取得できます。

キー値の設定の失敗

エンティティ型は、多くの場合、自動的に生成されたキー値を使用するように構成されます。 非複合キーの整数プロパティと GUID プロパティでは、これが既定です。 ただし、自動的に生成されたキー値を使用するようにエンティティ型が構成されていない場合は、エンティティを追跡する前に、明示的なキー値を設定する必要があります。 たとえば、次のエンティティ型を使用するとします。

public class Pet
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }
}

キー値を設定せずに 2 つの新しいエンティティ インスタンスを追跡しようとするコードについて考えてみます。

using var context = new BlogsContext();

context.Add(new Pet { Name = "Smokey" });

try
{
    context.Add(new Pet { Name = "Clippy" }); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

このコードでは、次の例外がスローされます。

System.InvalidOperationException: キー値 '{Id: 0}' を持つ別のインスタンスが既に追跡されているため、エンティティ型 'Pet' のインスタンスを追跡できません。 既存のエンティティをアタッチする場合、ある特定のキー値を持つエンティティ インスタンスは 1 つだけアタッチするようにしてください。

これを修正するには、キー値を明示的に設定するか、生成されたキー値を使用するようにキー プロパティを構成します。 詳細については、「生成される値」を参照してください。

単一の DbContext インスタンスの過剰使用

DbContext は、有効期間の短い作業単位を表すように設計されています。これについては、DbContext の初期化と構成に関するページと、「EF Core での変更の追跡」で詳しく説明されています。 このガイダンスに従えば、同じエンティティの複数のインスタンスを追跡しようとする状況に簡単に対応できます。 一般的な例を次に示します。

  • 同じ DbContext インスタンスを使用して、テスト状態を設定してからテストを実行する。 この結果、多くの場合、DbContext はテストの設定から 1 つのエンティティ インスタンスを引き続き追跡する一方で、その後のテストで新しいインスタンスのアタッチを試みることになります。 代わりに、テスト状態の設定とテスト コードには、別の DbContext インスタンスを使用してください。
  • リポジトリまたは同様のコードで共有の DbContext インスタンスを使用する。 代わりに、リポジトリでは作業単位ごとに 1 つの DbContext インスタンスを使用してください。

ID 解決とクエリ

ID 解決は、エンティティがクエリから追跡されるときに自動的に行われます。 つまり、特定のキー値を持つエンティティ インスタンスが既に追跡されている場合は、新しいインスタンスを作成する代わりに、この追跡されている既存のインスタンスが使用されます。 これは重大な結果になります。つまり、データベースでデータが変更された場合に、それがクエリの結果に反映されません。 作業単位ごとに新しい DbContext インスタンスを使用するのは、この理由からです。これについては、DbContext の初期化と構成に関する記事と、「EF Core での変更の追跡」で詳しく説明されています。

重要

EF Core では、DBSet に対する LINQ クエリは常にデータベースと照合して実行され、データベース内にある情報のみに基づいて結果が返される点を理解することが重要です。 ただし、追跡クエリでは、返されたエンティティが既に追跡されている場合は、データベース内のデータからインスタンスを作成する代わりに、追跡されているインスタンスが使用されます。

追跡されているエンティティをデータベースの最新のデータで更新する必要がある場合は、Reload() または GetDatabaseValues() を使用できます。 詳細については、「追跡対象のエンティティへのアクセス」を参照してください。

追跡クエリとは対照的に、追跡なしクエリは ID 解決を実行しません。 つまり、前に説明した JSON シリアル化の場合と同様に、追跡なしクエリでは重複が返される可能性があります。 クエリの結果がシリアル化されてクライアントに送信される場合、通常これは問題になりません。

ヒント

追跡なしのクエリを定期的に実行した後、返されたエンティティを同じコンテキストにアタッチしないでください。 これは、追跡クエリを使用するよりも遅くなり、正すことも難しくなります。

追跡なしクエリで ID 解決が実行されないのは、実行するとクエリから多数のエンティティをストリーミングするパフォーマンスに影響を与えるためです。 これは、ID 解決には、後で重複を作成する代わりに、返された各インスタンスを使用できるように追跡する必要があるためです。

追跡なしのクエリは、AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>) を使用して ID 解決を実行するように強制できます。 すると、クエリは返されたインスタンスを追跡して (通常の方法の追跡は行いません)、クエリ結果に重複が作成されないようにします。

オブジェクトの等価性のオーバーライド

EF Core では、エンティティ インスタンスを比較するときに、参照の等価性を使用します。 これは、エンティティ型で Object.Equals(Object) がオーバーライドされる場合や、他の方法でオブジェクトの等価性が変更された場合でも同様です。 ただし、等価性のオーバーライドが EF Core の動作に影響を与える可能性がある場所が 1 つあります。コレクション ナビゲーションで参照の等価性ではなくオーバーライドされた等価性が使用され、そのため複数のインスタンスが同一として報告される場合です。

この理由から、エンティティの等価性のオーバーライドは避けることをお勧めします。 使用する場合は、参照の等価性を強制するコレクション ナビゲーションを作成してください。 たとえば、参照の等価性を使用する次のような等値比較子を作成します。

public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
    private ReferenceEqualityComparer()
    {
    }

    public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer();

    bool IEqualityComparer<object>.Equals(object x, object y) => x == y;

    int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}

(.NET 5 以降では、これは ReferenceEqualityComparer として BCL に含まれています)。

その後、コレクション ナビゲーションを作成するときにこの比較子を使用できます。 次に例を示します。

public ICollection<Order> Orders { get; set; }
    = new HashSet<Order>(ReferenceEqualityComparer.Instance);

キー プロパティの比較

等価比較に加えて、キー値の順序付けも必要です。 これは、SaveChanges の 1 回の呼び出しで複数のエンティティを更新する場合に、デッドロックを回避するために重要です。 主キー、代替キー、または外部キーのプロパティに使用されるすべての型、および一意のインデックスに使用される型では、IComparable<T>IEquatable<T> を実装する必要があります。 キーとして通常使用される型 (int、Guid、string など) では、既にこれらのインターフェイスがサポートされています。 キーのカスタムの型には、これらのインターフェイスを追加できます。