次の方法で共有


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

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

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

ヒント

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

ヒント

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

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

追跡対象エンティティごとに、Entity Framework Core (EF Core) は以下を追跡します。

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

エンティティ インスタンスを DbContext.Entry に渡すと、 EntityEntry<TEntity> が特定のエンティティに対してこの情報にアクセスできます。 例えば次が挙げられます。

using var context = new BlogsContext();

var blog = await context.Blogs.SingleAsync(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としてブログ インスタンスの追跡を開始します。

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 = await context.Blogs.SingleAsync(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 は、変更済みとしてマークされたプロパティのみを更新します。 IsModifiedを true に設定すると、EF Core で特定のプロパティ値が強制的に更新されます。EF Core がプロパティ値を更新できないようにするには false に設定します。
  • 一時値 は通常、EF Core 値ジェネレーターによって生成されます。 プロパティの現在の値を設定すると、一時値が指定された値に置き換えられ、プロパティは一時的なものではなくとしてマークされます。 IsTemporaryを true に設定すると、明示的に設定された後でも値が強制的に一時的になります。

単一のナビゲーションで作業する

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

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

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");

ナビゲーションは、一対多リレーションシップや多対多リレーションシップの「多」側で使用される場合に、関連するエンティティの集合として機能することもあります。 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() EF Core がこのナビゲーションを、さらに構成できる IQueryable として読み込む際に使用するクエリです。 「関連データの明示的な読み込み」を参照してください。

エンティティのすべての特性を取り扱うこと

EntityEntry.Propertiesは、エンティティのすべてのプロパティに対するIEnumerable<T>PropertyEntryを返します。 これを使用して、エンティティのすべてのプロパティに対してアクションを実行できます。 たとえば、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 = await context.Entry(blog).GetDatabaseValuesAsync();

これらの 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 = await context.Entry(blog).GetDatabaseValuesAsync();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);

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

CurrentValues、OriginalValues、または GetDatabaseValues から返される PropertyValues オブジェクトは、 PropertyValues.ToObject()を使用してエンティティの複製を作成するために使用できます。 例えば次が挙げられます。

var clonedBlog = (await context.Entry(blog).GetDatabaseValuesAsync()).ToObject();

ToObjectは、DbContext によって追跡されない新しいインスタンスを返します。 また、返されるオブジェクトには、他のエンティティに設定されたリレーションシップはありません。

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

エンティティのすべてのナビゲーションを管理する

EntityEntry.Navigationsは、エンティティのすべてのナビゲーションに対してIEnumerable<T>NavigationEntryを返します。 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>.Find、および DbSet<TEntity>.FindAsync は、主キーがわかっている場合に、1 つのエンティティを効率的に検索するように設計されています。 エンティティが既に追跡されているかどうかを最初に確認し、その場合はすぐにエンティティを返します。 データベース クエリは、エンティティがローカルで追跡されていない場合にのみ行われます。 たとえば、同じエンティティに対して Find を 2 回呼び出す次のコードを考えてみましょう。

using var context = new BlogsContext();

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

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

Console.WriteLine();
Console.WriteLine("Second call to Find...");
var blog2 = await context.Blogs.FindAsync(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 を返します。

複合キー

検索は複合キーと共に使用することもできます。 たとえば、注文 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 = await context.OrderLines.FindAsync(orderId, productId);

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

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

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

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();

await context.Blogs.Include(e => e.Posts).LoadAsync();

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はエンティティ インスタンスを直接返します。 EntityEntry は、もちろん、 DbContext.Entryを呼び出すことによって、返されたエンティティに対して常に取得できます。

ローカル ビュー

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 = await context.Posts.Include(e => e.Blog).ToListAsync();

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

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

ローカルを使用してエンティティを追加および削除する

DbSet<TEntity>.Local は、 LocalView<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 = await context.Posts.Include(e => e.Blog).ToListAsync();

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によって作成されたコレクションも遅延して作成され、キャッシュされます。 どちらのメソッドも新しいコレクションを作成します。コレクションが遅くなり、何千ものエンティティが関係している場合に大量のメモリを使用する可能性があります。