有四個主要 API 可用來存取DbContext追蹤的實體:
- DbContext.Entry 返回一個 EntityEntry<TEntity> 實例,對於給定的實體實例。
- ChangeTracker.Entries 返回所有追蹤實體的實例,或返回指定類型的所有追蹤實體的實例。
- DbContext.Find、 DbContext.FindAsync、 DbSet<TEntity>.Find和 DbSet<TEntity>.FindAsync 會依主鍵尋找單一實體,先尋找追蹤的實體,然後視需要查詢資料庫。
- DbSet<TEntity>.Local 會傳回由 DbSet 表示的實體類型的實際實體(不是 EntityEntry 實例)。
以下各節會更詳細地說明上述各項。
小提示
本文件假設您已瞭解實體狀態以及 EF Core 變更追蹤的基本概念。 如需這些主題的詳細資訊,請參閱 EF Core 中的變更追蹤 。
小提示
您可以從 GitHub 下載範例程式代碼,以執行並偵錯此檔案中的所有程式代碼。
使用 DbContext.Entry 和 EntityEntry 實例
針對每個追蹤的實體,Entity Framework Core (EF Core) 會追蹤:
- 實體的整體狀態。 這是
Unchanged、Modified、Added、或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 不同,設定個別實體的狀態並不會導致所有連接的實體被追蹤。 這使得以這種方式設定狀態較呼叫Add、Attach或Update更為低階,因為這些函式是作用於整個實體圖譜上。
下表摘要說明如何使用 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,例如使用
Attach或Update,則原始值不可用。 在此情況下,傳回的原始值會與目前值相同。 - SaveChanges 只會更新標示為已修改的屬性。 設定 IsModified 為 true 以強制 EF Core 更新指定的屬性值,或將它設定為 false,以防止 EF Core 更新屬性值。
- 暫時值 通常是由 EF Core 值產生器所產生。 設定屬性的目前值會將暫存值取代為指定的值,並將屬性標示為非暫時值。 設定 IsTemporary 為 true,強制值即使在被明確設定之後仍然為臨時。
使用單一導覽進行工作
EntityEntry<TEntity>.Reference、EntityEntry<TEntity>.Collection和EntityEntry.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>NavigationEntry。 EntityEntry.References 和 EntityEntry.Collections 會執行相同動作,但僅限於參考或集合導覽。 這可以被用來執行每次實體導航的動作。 例如,若要強制載入所有相關實體:
foreach (var navigationEntry in context.Entry(blog).Navigations)
{
navigationEntry.Load();
}
與實體的所有成員合作
一般屬性和導覽屬性有不同的狀態和行為。 因此,通常會分別處理導航和非導航,如上面的各節所示。 不過,有時候不考慮它是一般屬性還是導覽屬性,對實體的任何成員執行某些動作會很有用。 EntityEntry.Member和EntityEntry.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.Find、 DbContext.FindAsync、 DbSet<TEntity>.Find和 DbSet<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 存取所有追蹤的實體
到目前為止,我們一次只能存取一個 EntityEntry 。 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 屬性的原因。
用於篩選的泛型型別不一定是對應的實體類型;您可以改用未對應的基底類型或介面。 例如,如果模型中的所有實體類型都實作了一個介面,該介面定義了它們的索引鍵的屬性:
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 在搭配特定類型的通知集合使用時效果最佳。 本地檢視支援建立這些特定的集合類型:
- LocalView<TEntity>.ToObservableCollection() 返回一個 ObservableCollection<T> 給 WPF 資料繫結。
- LocalView<TEntity>.ToBindingList() 傳回適用於 Windows Forms 資料繫結的 BindingList<T>。
例如:
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,這對大量實體而言可能很慢。 由 ToObservableCollection 和 ToBindingList 產生的集合會被延遲建立,然後進行快取。 這兩種方法都會建立新的集合,當涉及數千個實體時,可能會變慢並使用大量的記憶體。