追跡対象のエンティティへのアクセス

DbContext によって追跡されるエンティティにアクセスするには、主に 4 つの API があります。

これらのそれぞれについて、以下のセクションでより詳細に説明します。

ヒント

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

ヒント

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

DbContext.Entry インスタンスと EntityEntry インスタンスの使用

Entity Framework Core (EF Core) では、追跡対象のエンティティごとに以下のことが記録されます。

  • エンティティの全体的な状態。 これは UnchangedModifiedAddedDeleted のいずれかです。詳細については、「EF Core での変更の追跡」を参照してください。
  • 追跡対象のエンティティ間のリレーションシップ。 たとえば、投稿が属するブログです。
  • プロパティの "現在の値"。
  • プロパティの "元の値" (この情報を入手できる場合)。 元の値は、エンティティに対してデータベースからクエリが実行されたときに存在していたプロパティ値です。
  • クエリの実行後にどのプロパティ値が変更されているか。
  • プロパティ値に関するその他の情報 ("一時的" な値かどうかなど)。

エンティティ インスタンスを DbContext.Entry に渡すと、指定したエンティティのこの情報へのアクセスを提供する EntityEntry<TEntity> が得られます。 次に例を示します。

using var context = new BlogsContext();

var blog = context.Blogs.Single(e => e.Id == 1);
var entityEntry = context.Entry(blog);

以降のセクションでは、EntityEntry を使用して、エンティティの状態のほか、エンティティのプロパティとナビゲーションの状態にアクセスし、操作する方法を示します。

エンティティの操作

EntityEntry<TEntity> の最も一般的な使い方は、エンティティの現在の EntityState にアクセスすることです。 次に例を示します。

var currentState = context.Entry(blog).State;
if (currentState == EntityState.Unchanged)
{
    context.Entry(blog).State = EntityState.Modified;
}

Entry メソッドは、まだ追跡されていないエンティティに対しても使用できます。 これによって "エンティティの追跡が開始されるわけではありません"。エンティティの状態は、まだ Detached です。 ただしその後、返された EntityEntry を使用してエンティティの状態を変更できます。その時点で、指定した状態になっているエンティティが追跡対象になります。 たとえば次のコードでは、Added としての Blog インスタンスの追跡が開始されます。

var newBlog = new Blog();
Debug.Assert(context.Entry(newBlog).State == EntityState.Detached);

context.Entry(newBlog).State = EntityState.Added;
Debug.Assert(context.Entry(newBlog).State == EntityState.Added);

ヒント

EF6 とは異なり、個々のエンティティの状態を設定しても、接続されたエンティティのすべてが追跡されるようにはなりません。 これにより、状態をこのように設定することは、エンティティのグラフ全体に対して作用する AddAttach、または Update を呼び出すよりも低レベルの操作となっています。

次の表に、EntityEntry を使用してエンティティ全体を扱う方法のまとめを示します。

EntityEntry メンバー 説明
EntityEntry.State エンティティの EntityState を取得および設定します。
EntityEntry.Entity エンティティ インスタンスを取得します。
EntityEntry.Context このエンティティを追跡している DbContext
EntityEntry.Metadata エンティティの型を表す IEntityType メタデータ。
EntityEntry.IsKeySet エンティティにキー値が設定されていたかどうか。
EntityEntry.Reload() プロパティ値を、データベースから読み取った値で上書きします。
EntityEntry.DetectChanges() このエンティティのみに対して変更の検出を強制します。「変更の検出と通知」を参照してください。

1 つのプロパティの操作

EntityEntry<TEntity>.Property のいくつかのオーバーロードを使って、エンティティの個々のプロパティに関する情報にアクセスできます。 たとえば、厳密に型指定された fluent のような API を使用して、次のようにします。

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property(e => e.Name);

プロパティ名を、文字列として代わりに渡すことができます。 次に例を示します。

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property<string>("Name");

その後、返された PropertyEntry<TEntity,TProperty> を使って、プロパティに関する情報にアクセスできます。 たとえばこれは、このエンティティに対してプロパティの現在値を取得して設定するために使用できます。

string currentValue = context.Entry(blog).Property(e => e.Name).CurrentValue;
context.Entry(blog).Property(e => e.Name).CurrentValue = "1unicorn2";

上で使ったどちらの Property メソッドからも、厳密に型指定されたジェネリックの PropertyEntry<TEntity,TProperty> インスタンスが返されます。 このジェネリック型を使用することをお勧めするのは、値型のボックス化を使用せずにプロパティ値にアクセスできるためです。 ただし、エンティティまたはプロパティの型がコンパイル時にわからない場合は、代わりに非ジェネリックの PropertyEntry を取得できます。

PropertyEntry propertyEntry = context.Entry(blog).Property("Name");

これで値型のボックス化を使用して、その型に関係なく任意のプロパティのプロパティ情報にアクセスできます。 次に例を示します。

object blog = context.Blogs.Single(e => e.Id == 1);

object currentValue = context.Entry(blog).Property("Name").CurrentValue;
context.Entry(blog).Property("Name").CurrentValue = "1unicorn2";

次の表に、PropertyEntry によって公開されるプロパティ情報のまとめを示します。

PropertyEntry メンバー 説明
PropertyEntry<TEntity,TProperty>.CurrentValue プロパティの現在の値を取得および設定します。
PropertyEntry<TEntity,TProperty>.OriginalValue 入手できる場合はプロパティの元の値を取得および設定します。
PropertyEntry<TEntity,TProperty>.EntityEntry エンティティの EntityEntry<TEntity> への前方参照。
PropertyEntry.Metadata プロパティの IProperty メタデータ。
PropertyEntry.IsModified このプロパティが変更済みとマークされているかどうかを示し、この状態を変更できます。
PropertyEntry.IsTemporary このプロパティが一時的とマークされているかどうかを示し、この状態を変更できます。

注:

  • プロパティの元の値は、エンティティに対してデータベースからクエリが実行されたときに、プロパティに格納されていた値です。 ただし、エンティティが切断されて、その後、たとえば AttachUpdate といった別の DbContext に明示的にアタッチされた場合は、元の値を使用できません。 この場合、返される元の値は、現在の値と同じになります。
  • SaveChanges は、変更済みとマークされているプロパティだけを更新します。 指定したプロパティ値を EF Core で強制的に更新するには IsModified を true に設定し、プロパティ値が EF Core によって更新されないようにするには false に設定します。
  • 一時的な値は、一般に、EF Core の値ジェネレーターによって生成されます。 プロパティの現在の値を設定すると、一時的な値は指定された値に置き換えられて、プロパティは一時的ではないとマークされます。 値を、それが明示的に設定された後でも強制的に一時的なものにするには、IsTemporary を true に設定します。

1 つのナビゲーションの操作

EntityEntry<TEntity>.ReferenceEntityEntry<TEntity>.CollectionEntityEntry.Navigation のいくつかのオーバーロードを使うと、個々のナビゲーションに関する情報にアクセスできます。

1 つの関連エンティティへの参照ナビゲーションには、Reference メソッドを使ってアクセスします。 参照ナビゲーションでは、1 対多リレーションシップの "1" の側と、1 対 1 リレーションシップの両側が指し示されます。 次に例を示します。

ReferenceEntry<Post, Blog> referenceEntry1 = context.Entry(post).Reference(e => e.Blog);
ReferenceEntry<Post, Blog> referenceEntry2 = context.Entry(post).Reference<Blog>("Blog");
ReferenceEntry referenceEntry3 = context.Entry(post).Reference("Blog");

ナビゲーションは、1 対多や多対多のリレーションシップの "多" の側について使われたときには、関連エンティティのコレクションである場合もあります。 コレクション ナビゲーションにアクセスするには、Collection メソッドを使います。 次に例を示します。

CollectionEntry<Blog, Post> collectionEntry1 = context.Entry(blog).Collection(e => e.Posts);
CollectionEntry<Blog, Post> collectionEntry2 = context.Entry(blog).Collection<Post>("Posts");
CollectionEntry collectionEntry3 = context.Entry(blog).Collection("Posts");

一部の操作はすべてのナビゲーションで共通です。 参照とコレクションのどちらのナビゲーションについても、EntityEntry.Navigation メソッドを使ってこれらにアクセスできます。 すべてのナビゲーションに一緒にアクセスする際には、非ジェネリック アクセスのみを使用できることに注意してください。 次に例を示します。

NavigationEntry navigationEntry = context.Entry(blog).Navigation("Posts");

次の表は、ReferenceEntry<TEntity,TProperty>CollectionEntry<TEntity,TRelatedEntity>NavigationEntry の使い方をまとめたものです。

NavigationEntry メンバー 説明
MemberEntry.CurrentValue ナビゲーションの現在の値を取得および設定します。 これは、コレクション ナビゲーションのコレクション全体です。
NavigationEntry.Metadata ナビゲーションの INavigationBase メタデータ。
NavigationEntry.IsLoaded 関連するエンティティまたはコレクションが、データベースから完全に読み込まれたかどうかを示す値を取得または設定します。
NavigationEntry.Load() 関連するエンティティまたはコレクションをデータベースから読み込みます。「関連データの明示的読み込み」を参照してください。
NavigationEntry.Query() このナビゲーションを IQueryable として読み込むために EF Core によって使用される、さらに構成が可能なクエリ。「関連データの明示的読み込み」を参照してください。

エンティティのすべてのプロパティの操作

EntityEntry.Properties は、エンティティのすべてのプロパティについて、PropertyEntryIEnumerable<T> を返します。 これは、エンティティのすべてのプロパティに対してアクションを実行するために使用できます。 たとえば、任意の DateTime プロパティを DateTime.Now に設定するには、次のようにします。

foreach (var propertyEntry in context.Entry(blog).Properties)
{
    if (propertyEntry.Metadata.ClrType == typeof(DateTime))
    {
        propertyEntry.CurrentValue = DateTime.Now;
    }
}

さらに、EntityEntry には、すべてのプロパティ値を同時に取得して設定するメソッドがいくつか含まれています。 これらのメソッドでは、プロパティとそれらの値のコレクションを表す PropertyValues クラスが使われます。 PropertyValues の取得は、現在の値または元の値に対して、またはデータベースに現在格納されている値に対して行えます。 次に例を示します。

var currentValues = context.Entry(blog).CurrentValues;
var originalValues = context.Entry(blog).OriginalValues;
var databaseValues = context.Entry(blog).GetDatabaseValues();

これらの PropertyValues オブジェクトは、単独ではあまり便利ではありません。 ただし、これらを組み合わせると、エンティティを操作するときに必要な一般的な操作を実行できます。 これは、データ転送オブジェクトを操作する場合や、オプティミスティック同時実行制御の競合を解決する場合に便利です。 以降のセクションでは、いくつかの例を示します。

エンティティまたは DTO からの現在または元の値の設定

エンティティの現在または元の値は、別のオブジェクトから値をコピーすることで更新できます。 たとえば、エンティティ型と同じプロパティを持つ BlogDto データ転送オブジェクト (DTO) について考えます。

public class BlogDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

これを使うと、PropertyValues.SetValues を使って追跡対象エンティティの現在の値を設定できます。

var blogDto = new BlogDto { Id = 1, Name = "1unicorn2" };

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

この手法は、サービス呼び出しや、n 層アプリケーションのクライアントから取得した値を使用してエンティティを更新するときに使用されることがあります。 使用されるオブジェクトのプロパティの名前が、エンティティのプロパティの名前と一致している限り、そのオブジェクトの型がエンティティと同じである必要はないことに注意してください。 上の例では、DTO BlogDto のインスタンスを使用して、追跡対象の Blog エンティティの現在の値を設定しています。

プロパティは、値セットが現在の値と異なっている場合にのみ、変更済みとマークされます。

ディクショナリから現在の値または元の値を設定する

前の例では、エンティティまたは DTO インスタンスからの値を設定しています。 プロパティ値が、名前と値のペアとしてディクショナリに格納されているときには、同じ動作を使用できます。 次に例を示します。

var blogDictionary = new Dictionary<string, object> { ["Id"] = 1, ["Name"] = "1unicorn2" };

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

データベースにある現在の値または元の値の設定

GetDatabaseValues() または GetDatabaseValuesAsync を呼び出し、返されたオブジェクトを使って現在の値、元の値、またはその両方を設定することで、エンティティの現在または元の値をデータベースの最新の値で更新できます。 次に例を示します。

var databaseValues = context.Entry(blog).GetDatabaseValues();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);

現在の値、元の値、またはデータベースの値を含む複製オブジェクトを作成する

CurrentValues、OriginalValues、または GetDatabaseValues から返された PropertyValues オブジェクトを使うと、PropertyValues.ToObject() を使ってエンティティの複製を作成できます。 次に例を示します。

var clonedBlog = context.Entry(blog).GetDatabaseValues().ToObject();

ToObject からは、DbContext で追跡されていない新しいインスタンスが返されることに注意してください。 返されたオブジェクトには、他のエンティティへのリレーションシップも設定されていません。

複製されたオブジェクトは、データベースに対する同時更新に関連する問題、特に、特定の型のオブジェクトへのデータ バインドがある場合の問題の解決に役立ちます。 詳細については、オプティミスティック同時実行制御に関するページを参照してください。

エンティティのすべてのナビゲーションの操作

EntityEntry.Navigations は、エンティティのすべてのナビゲーションについて、NavigationEntryIEnumerable<T> を返します。 EntityEntry.ReferencesEntityEntry.Collections は同じことを行いますが、それぞれ、参照またはコレクションのナビゲーションに限定されます。 これは、エンティティのすべてのナビゲーションに対してアクションを実行するために使用できます。 たとえば、すべての関連エンティティを強制的に読み込むには、次のようにします。

foreach (var navigationEntry in context.Entry(blog).Navigations)
{
    navigationEntry.Load();
}

エンティティのすべてのメンバーの操作

通常のプロパティとナビゲーション プロパティでは、状態と動作が異なっています。 そのため上記のセクションで示したように、ナビゲーションとナビゲーションでないことは別個に処理するのが一般的です。 ただし、それが通常のプロパティであるかナビゲーションであるかに関係なく、エンティティの任意のメンバーを使用して何かを行うと便利な場合があります。 EntityEntry.MemberEntityEntry.Members は、この目的のために用意されています。 次に例を示します。

foreach (var memberEntry in context.Entry(blog).Members)
{
    Console.WriteLine(
        $"Member {memberEntry.Metadata.Name} is of type {memberEntry.Metadata.ClrType.ShortDisplayName()} and has value {memberEntry.CurrentValue}");
}

サンプルのブログに対してこのコードを実行すると、次の出力が生成されます。

Member Id is of type int and has value 1
Member Name is of type string and has value .NET Blog
Member Posts is of type IList<Post> and has value System.Collections.Generic.List`1[Post]

ヒント

変更トラッカーのデバッグ ビューには、このような情報が表示されます。 変更トラッカー全体のデバッグ ビューは、各追跡対象エンティティの個々の EntityEntry.DebugView から生成されます。

Find と FindAsync

DbContext.FindDbContext.FindAsyncDbSet<TEntity>.FindDbSet<TEntity>.FindAsync は、主キーがわかっているときに 1 つのエンティティを効率的に検索するために設計されています。 Find では、まず、エンティティが既に追跡されているかどうかがチェックされ、その場合はすぐにエンティティが返されます。 データベース クエリは、エンティティがローカルで追跡されていない場合にのみ行われます。 たとえば、同じエンティティに対して Find を 2 回呼び出す次のコードについて考えます。

using var context = new BlogsContext();

Console.WriteLine("First call to Find...");
var blog1 = context.Blogs.Find(1);

Console.WriteLine($"...found blog {blog1.Name}");

Console.WriteLine();
Console.WriteLine("Second call to Find...");
var blog2 = context.Blogs.Find(1);
Debug.Assert(blog1 == blog2);

Console.WriteLine("...returned the same instance without executing a query.");

SQLite を使用する場合のこのコードの出力 (EF Core のログ記録を含む) は、次のようになります。

First call to Find...
info: 12/29/2020 07:45:53.682 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@__p_0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
      SELECT "b"."Id", "b"."Name"
      FROM "Blogs" AS "b"
      WHERE "b"."Id" = @__p_0
      LIMIT 1
...found blog .NET Blog

Second call to Find...
...returned the same instance without executing a query.

最初の呼び出しではローカルでエンティティが見つからないので、データベース クエリが実行されることに注目してください。 逆に、2 番目の呼び出しでは、既に追跡中であるため、データベース クエリを実行せずに同じインスタンスが返されます。

指定したキーを持つエンティティが、ローカルで追跡されておらず、データベース内には存在しない場合、Find から null が返されます。

複合キー

Find は、複合キーと一緒に使用することもできます。 たとえば、注文 ID と製品 ID から成る複合キーを持つ OrderLine エンティティについて考えます。

public class OrderLine
{
    public int OrderId { get; set; }
    public int ProductId { get; set; }

    //...
}

複合キーを DbContext.OnModelCreating で構成して、キー パーツと "それらの順序" を定義する必要があります。 次に例を示します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<OrderLine>()
        .HasKey(e => new { e.OrderId, e.ProductId });
}

OrderId が最初のキー パーツで、ProductId が 2 つ目のキー パーツであることに注目してください。 キー値を Find に渡すときには、この順序を使用する必要があります。 次に例を示します。

var orderline = context.OrderLines.Find(orderId, productId);

ChangeTracker.Entries を使用したすべての追跡対象エンティティへのアクセス

ここまでは、一度に 1 つの EntityEntry のみにアクセスしました。 ChangeTracker.Entries() からは、DbContext によって現在追跡されているすべてのエンティティの EntityEntry が返されます。 次に例を示します。

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

foreach (var entityEntry in context.ChangeTracker.Entries())
{
    Console.WriteLine($"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property("Id").CurrentValue}");
}

このコードは、次の出力を生成します。

Found Blog entity with ID 1
Found Post entity with ID 1
Found Post entity with ID 2

ブログと投稿の両方のエントリが返されることに注意してください。 代わりに、ChangeTracker.Entries<TEntity>() ジェネリック オーバーロードを使って、結果を特定のエンティティ型にフィルター処理できます。

foreach (var entityEntry in context.ChangeTracker.Entries<Post>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

このコードからの出力は、投稿だけが返されたことを示しています。

Found Post entity with ID 1
Found Post entity with ID 2

また、ジェネリック オーバーロードを使うと、ジェネリックの EntityEntry<TEntity> インスタンスが返されます。 これが、この例で Id プロパティへの、あの fluent のようなアクセスを実現している機能です。

フィルター処理に使用されるジェネリック型は、マップされたエンティティ型である必要はありません。代わりに、マップされていない基本データ型やインターフェイスを使用できます。 たとえば、モデル内のすべてのエンティティ型で、キー プロパティを定義するインターフェイスを実装している場合は、次のようになります。

public interface IEntityWithKey
{
    int Id { get; set; }
}

その後、このインターフェイスを使用して、厳密に型指定された方法で、任意の追跡対象エンティティのキーを操作できます。 次に例を示します。

foreach (var entityEntry in context.ChangeTracker.Entries<IEntityWithKey>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

DbSet.Local を使用した追跡対象エンティティのクエリ

EF Core のクエリは、常にデータベースに対して実行され、データベースに保存されているエンティティのみが返されます。 DbSet<TEntity>.Local を使うと、ローカル環境の追跡対象エンティティに対する DbContext のクエリを実行できます。

DbSet.Local は追跡対象エンティティのクエリを実行するために使用されるため、エンティティを DbContext に読み込んでから、読み込まれたそれらのエンティティを操作するのが一般的です。 これは特にデータ バインディングに当てはまりますが、その他の状況でも便利な場合があります。 たとえば次のコードでは、すべてのブログと投稿について、最初にデータベースのクエリが行われます。 Load 拡張メソッドは、コンテキストによって追跡されている結果を、アプリケーションに直接返すことなく使用して、このクエリを実行するために使われます。 (ToList または類似のものを使用しても同じ効果がありますが、それらには返されるリストを作成するオーバーヘッドがあり、ここでは必要ありません。)例では、次に DbSet.Local を使用して、ローカルで追跡されているエンティティにアクセスしています。

using var context = new BlogsContext();

context.Blogs.Include(e => e.Posts).Load();

foreach (var blog in context.Blogs.Local)
{
    Console.WriteLine($"Blog: {blog.Name}");
}

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"Post: {post.Title}");
}

ChangeTracker.Entries() とは異なり、DbSet.Local はエンティティ インスタンスを直接返すことに注意してください。 もちろん、DbContext.Entry を呼び出すことで、返されたエンティティに対する EntityEntry をいつでも取得できます。

ローカル ビュー

DbSet<TEntity>.Local からは、ローカルで追跡されているエンティティの現在の EntityState が反映された、それらのエンティティのビューが返されます。 具体的には、これは以下のことを意味します。

  • Added エンティティが含められます。 Added エンティティは、まだデータベース内に存在しておらず、したがってデータベース クエリによって返されることはないため、これは通常の EF Core クエリに当てはまらないことに注意してください。
  • Deleted エンティティは除外されます。 Deleted エンティティは、まだデータベース内に存在しており、それゆえデータベース クエリによって "返される" ので、この場合も、これは通常の EF Core クエリに当てはまらないことに注意してください。

これらはすべて、DbSet.Local は、Added エンティティが含められ、Deleted エンティティが除外された、エンティティ グラフの現在の概念的状態を反映したデータに関するビューであることを意味しています。 これは、SaveChanges が呼び出された後になっていると期待されるデータベースの状態と一致しています。

データ バインディングでは、アプリケーションによって行われた変更に基づいて、ユーザーがデータを理解しているとおりにユーザーにデータが提示されるため、これは通常、データ バインディングに最適なビューです。

次のコードでは、これを具体的に示すため、1 つの投稿を Deleted とマークしてから新しい投稿を追加し、それを Added とマークしています。

using var context = new BlogsContext();

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

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Remove(posts[1]);

context.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

このコードの出力は次のようになります。

Local view after loading posts:
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing F# 5
  Post: Announcing .NET 5.0
Local view after adding and deleting posts:
  Post: What’s next for System.Text.Json?
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing .NET 5.0

削除した投稿がローカル ビューから削除されて、追加した投稿が含められていることに注意してください。

Local を使用したエンティティの追加と削除

DbSet<TEntity>.LocalLocalView<TEntity> のインスタンスを返します。 これは、コレクションのエンティティが追加および削除されたときの通知の生成と応答を行う ICollection<T> の実装です。 (これは ObservableCollection<T> と同じ概念ですが、独立したコレクションとしてではなく、既存の EF Core 変更追跡エントリに対するプロジェクションとして実装されています。)

ローカル ビューの通知は、DbContext の変更の追跡にフックされます。これにより、ローカル ビューは DbContext と同期した状態に留まります。 具体的には次のとおりです。

  • 新しいエンティティを DbSet.Local に追加すると、通常は Added 状態で、DbContext によって追跡されるようになります。 (エンティティに既に生成されたキー値がある場合は、代わりに Unchanged として追跡されます。)
  • DbSet.Local からエンティティを削除すると、それは Deleted とマークされます。
  • DbContext の追跡対象になったエンティティは、自動的に DbSet.Local コレクション内に出現します。 たとえば、より多くのエンティティを取り込むためにクエリを実行すると、ローカル ビューが自動的に更新されます。
  • Deleted としてマークされているエンティティは、ローカル コレクションから自動的に削除されます。

これは、コレクションに対して追加や削除を行うだけで、追跡対象エンティティを操作するためにローカル ビューを利用できることを意味します。 たとえば、前のコード例を変更して、ローカル コレクションに対する投稿の追加と削除をしてみましょう。

using var context = new BlogsContext();

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

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Posts.Local.Remove(posts[1]);

context.Posts.Local.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

ローカル ビューに加えられた変更は DbContext と同期されるため、出力は前の例から変化していません。

Windows フォームまたは WPF データ バインディングのためのローカル ビューの使用

DbSet<TEntity>.Local は、EF Core エンティティへのデータ バインディングの基礎となります。 ただし、Windows フォームと WPF はどちらも、想定された特定の型の通知コレクションと共に使用される場合に最適に動作します。 ローカル ビューでは、以下の特定のコレクション型の作成がサポートされています。

次に例を示します。

ObservableCollection<Post> observableCollection = context.Posts.Local.ToObservableCollection();
BindingList<Post> bindingList = context.Posts.Local.ToBindingList();

EF Core での WPF のデータ バインディングについて詳しくは、「WPF の概要」をご覧ください。EF Core での Windows フォームのデータ バインディングについて詳しくは、「Windows フォームについて」をご覧ください。

ヒント

特定の DbSet インスタンスのローカル ビューは、最初にアクセスされ、その後キャッシュされるときに、遅れて作成されます。 LocalView の作成自体は高速であり、大量のメモリは使用されません。 ただし、それによって DetectChanges が呼び出されます。これは、エンティティが大量だと低速な場合があります。 ToObservableCollectionToBindingList によって作成されるコレクションも、遅れて作成されてからキャッシュされます。 これらのどちらの方法でも、新しいコレクションが作成されます。これは、何千ものエンティティが関係している場合は低速で、大量のメモリが使用される場合があります。