共用方式為


存取追蹤實體

有四個主要 API 可用來存取DbContext追蹤的實體:

以下各節會更詳細地說明上述各項。

小提示

本文件假設您已瞭解實體狀態以及 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 不同,設定個別實體的狀態並不會導致所有連接的實體被追蹤。 這使得以這種方式設定狀態較呼叫AddAttachUpdate更為低階,因為這些函式是作用於整個實體圖譜上。

下表摘要說明如何使用 EntityEntry 來處理整個實體的方法:

EntityEntry 成員 說明
EntityEntry.State 取得與設定實體的 EntityState
EntityEntry.Entity 取得實體實例。
EntityEntry.Context 追蹤此實體的DbContext
EntityEntry.Metadata IEntityType 實體類型的元數據。
EntityEntry.IsKeySet 實體是否已設定其鍵值。
EntityEntry.Reload() 以從資料庫讀取的值覆寫屬性值。
EntityEntry.DetectChanges() 僅強制偵測此特定實體的變更,請參閱 變更偵測和通知

使用單一屬性

數個 EntityEntry<TEntity>.Property 多載允許存取實體個別屬性的相關信息。 例如,使用一種類似流暢風格的強型別 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";

上述兩個屬性方法都會傳回具有強型別的泛型 PropertyEntry<TEntity,TProperty> 實例。 使用這個泛型類型是慣用的,因為它允許存取屬性值,而不需要 Boxing 實值型別。 不過,如果在編譯階段不知道實體或屬性的類型,則可以改為取得非泛型 PropertyEntry

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

這允許存取任何屬性的屬性資訊,不論其類型為何,都會犧牲Boxing實值型別。 例如:

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 指出這個屬性是否標示為 暫時性,並允許變更此狀態。

注意事項:

  • 屬性的原始值是屬性從資料庫查詢實體時具有的值。 不過,若實體已中斷連線,然後明確附加至另一個 DbContext,例如使用 AttachUpdate,則原始值不可用。 在此情況下,傳回的原始值會與目前值相同。
  • SaveChanges 只會更新標示為已修改的屬性。 設定 IsModified 為 true 以強制 EF Core 更新指定的屬性值,或將它設定為 false,以防止 EF Core 更新屬性值。
  • 暫時值 通常是由 EF Core 值產生器所產生。 設定屬性的目前值會將暫存值取代為指定的值,並將屬性標示為非暫時值。 設定 IsTemporary 為 true,強制值即使在被明確設定之後仍然為臨時。

使用單一導覽進行工作

EntityEntry<TEntity>.ReferenceEntityEntry<TEntity>.CollectionEntityEntry.Navigation的數個重載允許存取個別導覽的相關資訊。

單一相關實體的 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);

這項技術有時在多層式應用程式中更新實體時使用,具體而言是用從服務呼叫或用戶端取得的值進行更新。 請注意,使用的物件不一定與實體的類型相同,只要其名稱符合實體的屬性。 在上述範例中,會使用 DTO BlogDto 的實例來設定追蹤 Blog 實體的目前值。

請注意,只有在值集與目前值不同時,才會將屬性標示為修改。

設定字典中的目前或原始值

先前範例會設定 entity 或 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>NavigationEntryEntityEntry.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生成的。

尋找和 FindAsync

DbContext.FindDbContext.FindAsyncDbSet<TEntity>.FindDbSet<TEntity>.FindAsync 是專為在已知單一實體的主鍵時有效率地查閱所設計。 尋找方法首先檢查實體是否已經被追蹤,如果是,則立即回傳實體。 只有當該實體未在本機受追蹤時,才會進行資料庫查詢。 例如,請考慮針對相同實體呼叫 Find 兩次的程式代碼:

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.

請注意,第一次呼叫在本機找不到實體,因此會執行資料庫查詢。 相反地,第二次操作會傳回相同的實例,而不查詢資料庫,因為它已經被追蹤中。

如果具有指定索引鍵的實體未在本機追蹤且不存在於資料庫中,則尋找 會傳回 null。

複合鍵

Find 也可以搭配複合鍵使用。 例如,考慮具有由訂單標識碼和產品標識碼組成的複合鍵的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 是索引鍵的第二個部分。 將索引鍵值傳遞至 Find 時,必須使用這個順序。 例如:

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

使用 ChangeTracker.Entries 存取所有追蹤的實體

到目前為止,我們一次只能存取一個 EntityEntryChangeTracker.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 屬性的原因。

用於篩選的泛型型別不一定是對應的實體類型;您可以改用未對應的基底類型或介面。 例如,如果模型中的所有實體類型都實作了一個介面,該介面定義了它們的索引鍵的屬性:

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 直接傳回實體實例。 當然,您可以呼叫 DbContext.Entry來取得傳回實體的 EntityEntry。

當地觀點

DbSet<TEntity>.Local 將傳回本機追蹤實體的檢視,這些實體的檢視反映了當前的 EntityState。 具體來說,這表示:

  • Added 被包含的實體。 請注意,這並非一般 EF Core 查詢的情況,因為 Added 資料庫中尚未存在實體,因此不會由資料庫查詢傳回。
  • Deleted 實體已被排除。 請注意,這種情況在一般 EF Core 查詢中再次不成立,因為 Deleted 實體仍存在於資料庫中,因此資料庫查詢 傳回。

DbSet.Local 表示,這是反映實體圖形目前概念狀態的數據檢視,其中包含 Added 實體和 Deleted 排除實體。 這會比對呼叫 SaveChanges 之後預期的資料庫狀態。

這通常是數據系結的理想檢視,因為它會根據應用程式所做的變更,向使用者呈現數據。

下列程式代碼藉由將一則貼文標記為 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

請注意,已刪除的貼文會從本機檢視中移除,並包含新增的貼文。

使用Local新增和移除實體

DbSet<TEntity>.Local 會傳回 LocalView<TEntity> 的執行個體。 這是 ICollection<T> 的實作,會在實體從集合中被新增和移除時生成並回應通知。 (這個概念與 ObservableCollection<T>相同,但實作為對現有 EF Core 變更追蹤條目的映射,而不是獨立集合。)

本機檢視的通知會連結至 DbContext 變更追蹤,讓本機檢視與 DbContext 保持同步。 具體說來:

  • 將新實體新增到 DbSet.Local 中會開始被 DbContext 追蹤,通常處於 Added 狀態。 (如果實體已經有產生的索引鍵值,則會被作為 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 Forms 或 WPF 資料繫結的本地視圖

DbSet<TEntity>.Local 構成資料繫結至 EF Core 實體的基礎。 不過,Windows Forms 和 WPF 在搭配特定類型的通知集合使用時效果最佳。 本地檢視支援建立這些特定的集合類型:

例如:

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

如需使用 EF Core 進行 WPF 數據系結的詳細資訊,請參閱 開始使用 WPF 數據系結和 開始使用 Windows Forms 以取得使用 EF Core 進行 Windows Forms 數據系結的詳細資訊。

小提示

第一次存取並快取時,會延遲建立指定 DbSet 實例的本機檢視。 LocalView 建立本身的速度很快,而且不會使用大量的記憶體。 不過,它會呼叫 DetectChanges,這對大量實體而言可能很慢。 由 ToObservableCollectionToBindingList 產生的集合會被延遲建立,然後進行快取。 這兩種方法都會建立新的集合,當涉及數千個實體時,可能會變慢並使用大量的記憶體。