儲存至資料庫的物件可以分割成三大類別:
- 非結構化物件並保留單一值。 例如,、
int、Guidstring、IPAddress。 這些(較寬鬆地)被稱為 原始類型。 - 用於保存多個值且其身份由索引鍵值定義的物件。 例如,
Blog、Post、Customer。 這些稱為 實體類型。 - 物件結構化可承載多個值,但物件沒有定義其身份的金鑰。 例如,
Address、Coordinate、Money。 這些稱為 值物件,EF Core 將它們映射為 複型態。
複雜型態將多個屬性合併成一個包含於實體型別中的單一 .NET 類型;它沒有自己的身份,無法獨立追蹤或查詢。 這使得複雜型態成為建模 價值對象的自然方式。
提示
你可以在 GitHub 上執行並除錯完整範例專案。
Note
複雜型態於 EF Core 8 中引入,並在後續版本中大幅擴充。 以下註解為引入這些特徵的版本。
複雜類型與擁有實體類型
在複雜型態出現之前, 擁有實體型 別是無關鍵屬性物件建模的推薦方式。 然而,擁有型態在幕後仍是 實體型別 :它們擁有隱藏的金鑰與身份,因此以參考語意運作。 這會產生許多複雜型態設計用來解決的摩擦點。
主要差異包括:
| 層面 | 擁有的物件類型 | 複雜類型 |
|---|---|---|
| 身分識別 | 擁有隱藏的鑰匙和身份 | 沒有身份認同;以價值比較 |
| 實例分享 | 同一個實例不能被引用兩次 | 同一個實例可以被指派給多個屬性 |
| 指派語意 | 參考語意 | 價值語意學(屬性被複製) |
| .NET 類型 | 僅限參考型別 | 參考 或 值類型 |
| 表格映射 | 自有資料表、資料表拆分或 JSON | 容器表格(表格分割)或 JSON |
| Navigations | 可包含前往其他實體的導航 | 無法包含導航 |
批量更新(ExecuteUpdate) |
不支援 | 支援 |
例如,將客戶的帳單地址與其出貨地址相同,對於擁有實體類型來說是失敗的,因為同一實體實例不能被多次引用:
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types
由於複雜型態具有值語意,相同的賦值只是複製屬性,並如預期般運作。 同樣地,在 LINQ 查詢中比較兩個複數值會比較它們的內容,而比較兩個擁有實體則比較它們的身份。
基於這些原因,複雜型態通常是用資料表分割或 JSON 映射來建模值物件的較佳選擇。 目前使用擁有實體類型來處理這些情境的使用者,建議考慮轉換為複雜型態。
一個簡單的例子
考慮一個 Address 型別,它包含多個相關值,但自身沒有身份:
public record Address
{
public required string Line1 { get; init; }
public string? Line2 { get; init; }
public required string City { get; init; }
public required string Country { get; init; }
public required string PostCode { get; init; }
}
Address 接著可以在客戶/訂單模式的多個場合使用:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
// A required (non-nullable) complex property.
public required Address Address { get; set; }
// An optional (nullable) complex property.
public Address? SecondaryAddress { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
創建與拯救客戶的流程與往常相同:
var customer = new Customer
{
Name = "Willow",
Address = new Address
{
Line1 = "Barking Gate",
City = "Walpole St Peter",
Country = "UK",
PostCode = "PE14 7AV"
}
};
context.Add(customer);
await context.SaveChangesAsync();
在關聯式資料庫中,複雜型別不會有自己的資料表。 取而代之的是,其屬性會以附加欄位形式儲存在包含實體的表格中(這稱為 表格拆分):
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
由於複雜型態具有值語意,同一 Address 實例可在多個屬性間共享而不會有問題:
// The same Address instance can be assigned to multiple complex properties.
customer.Orders.Add(
new Order
{
Contents = "Tesco Tasty Treats",
BillingAddress = customer.Address,
ShippingAddress = customer.Address
});
await context.SaveChangesAsync();
複雜型態配置
與大多數實體類型不同,複雜類型 並非依慣例被發現。 你必須明確配置它們,要麼用 標註 ComplexTypeAttribute,要麼 ComplexProperty 呼叫 Fluent API 來 OnModelCreating 對應每個應該映射為複雜型別的屬性:
[ComplexType]
public record Address
{
public required string Line1 { get; init; }
public string? Line2 { get; init; }
public required string City { get; init; }
public required string Country { get; init; }
public required string PostCode { get; init; }
}
配置複雜型態屬性的面向
巢狀 Property 建構器可用來配置複雜型態的純量屬性,就像設定實體類型的屬性一樣——例如,設定欄位名稱或最大長度:
modelBuilder.Entity<Order>()
.ComplexProperty(
o => o.ShippingAddress,
b =>
{
b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
b.Property(a => a.City).HasColumnName("ShipsToCity");
});
從 EF Core 11 開始,你可以直接在複雜型別中設定巢狀屬性,方法是在 lambda 中串接成員存取,而無需先取得複雜型態建構器:
// EF Core 11 allows configuring a complex-type property directly by chaining
// member access, without first obtaining the complex-type builder.
modelBuilder.Entity<Customer>()
.Property(c => c.Address.Line1)
.HasMaxLength(200);
參考與值類型
複雜型態可以是 .NET 參考型別(a class 或 record),或是值型別(a struct 或 record struct,於 EF Core 10 引入)。
可變動性
由於一個參考型實例可以被多個屬性共享,變異其屬性會改變所有使用該屬性的值。 這通常不是你想要的。 避免這種情況的好方法——也是值物件的自然擬合——是讓複數型別 是不可變的,因此改變值時必須建立一個新的實例。
Address本文所使用的型別是不可變record型;因此更改位址時會用一個with表達式完成:
// Address is an immutable record, so create a new instance to change a value.
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
即使被指派了 Address 全新的實例,EF 仍會在個別屬性層級追蹤變更,因此只有實際改變值的欄位會被更新:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
不可變性可以用不可變 class (僅初始化或唯讀)屬性表示,例如 a record、a readonly struct、 或 readonly record struct。 值型別struct()具有複製語意,因此指派它們總是會複製值,避免意外共享問題,即使是可變的,但在 C# 中通常不鼓勵使用可變結構,因此仍建議使用不可變形式。
提示
如果多個實體真的應該觀察到相同的位址並在變更時一起更新,那麼就將該位址建模為具有自身身份的 實體類型 ,並透過導覽來引用,而非使用複雜型別。
巢狀複雜類型
一個複雜型態可以包含其他複雜型態的屬性,讓你能建立任何深度的結構化物件。 例如,一個 Contact 複雜型態可能同時包含一個 Address 及一個或多個 PhoneNumber 複型態:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
public record PhoneNumber(int CountryCode, long Number);
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
}
當透過表格分割映射時,巢狀複雜型的欄位前會加上該屬性的完整路徑(例如, Contact_HomePhone_Number)。
可選的複雜類型
預設 情況下,需要一個複雜的性質:CLR 性質必須始終有值,且該值會映射到不可空欄位。 從 EF Core 10 開始,複數屬性可透過宣告為可空(nullable)來選擇:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
// A required (non-nullable) complex property.
public required Address Address { get; set; }
// An optional (nullable) complex property.
public Address? SecondaryAddress { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
一個可選的複數性質,結果nullNULL是所有欄位的值。
Note
目前,一個可選的複雜型態至少需要在該複雜型別上定義一個 必要的 屬性。 這是因為 EF 至少需要一個不可空欄位,才能區分 null 複數值與性質皆為 null的複數值。
如果複合型別本身沒有必須的屬性,你可以改為配置判別子屬性。 雖然 EF Core 尚未支援複型的繼承,但判別器預設作為 必須的影子屬性 建立,符合上述要求:
// GeoLocation has no required property, so configure a discriminator. EF creates
// it as a required shadow property, which satisfies the requirement that an
// optional complex type have at least one required property.
modelBuilder.Entity<Place>()
.ComplexProperty(p => p.Location, b => b.HasDiscriminator());
複型集合
從 EF Core 10 開始,一個屬性可以包含一 組 複雜型態。 在關聯式資料庫中,複雜的集合必須用以下 ToJson 方式映射到單一 JSON 欄位,不能映射到其他資料表:
public class Distributor
{
public int Id { get; set; }
public required string Name { get; set; }
// A collection of complex types, mapped to a single JSON column.
public List<Address> ShippingCenters { get; set; } = new();
}
// Collections of complex types must be mapped to JSON on relational providers.
modelBuilder.Entity<Distributor>()
.ComplexCollection(d => d.ShippingCenters, b => b.ToJson());
集合中的每個元素都以 JSON 物件形式儲存在陣列內,整個集合對應到同一欄:
CREATE TABLE [Distributors] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[ShippingCenters] json NOT NULL,
CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);
Note
值型別()struct的集合目前不支援;複雜集合元素請使用參考型別(class 或 record)。
複雜型態映射到 JSON
除了資料表拆分外,EF Core 10 還允許將(非集合)複雜屬性映射到單一 JSON 欄位,且為:ToJson
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.Address, c => c.ToJson());
b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});
每個複數值會序列化成單一 JSON 欄位,而非分散於多個欄位:
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Address] json NOT NULL,
[SecondaryAddress] json NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
在 SQL Server 2025 和 Azure SQL 上,EF 預設使用原生json資料型別;在其他資料庫及較舊版本的 SQL Server 中,JSON 則以文字欄位儲存。 如果需要,你可以用 來覆寫欄位類型 HasColumnType 。
與資料表分割不同,JSON 映射允許在映射型別 內 設置集合,並且讓你能像查詢其他屬性一樣,在文件中查詢和更新個別屬性。 JSON 欄位內的值也可有效批量更新。ExecuteUpdateAsync
複數型態屬性上的鍵與索引
從 EF Core 11 開始,鍵與索引可針對嵌套於非集合複雜型態中的純量屬性。 這可以用 lambda 來完成:
// EF Core 11 allows keys and indexes to target scalar properties nested
// inside non-collection complex types.
modelBuilder.Entity<Customer>()
.HasIndex(c => c.Address.PostCode);
相同的路徑也可以依名稱設定,並用來 . 導航到一個複雜的屬性:
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
對於關聯式提供者,索引也能針對映射到 JSON 欄位的複雜型別路徑。 複雜集合路徑過去 [] 指稱所有元素,或指特定元素的數字索引器:
// Index a scalar inside every element of a JSON-mapped complex collection.
// Requires a provider/database with JSON index support, such as SQL Server 2025.
modelBuilder.Entity<Distributor>()
.HasIndex("ShippingCenters[].City");
Note
索引到 JSON 映射的複雜集合需要支援 JSON 索引的資料庫,例如 SQL Server 2025。
具有實體繼承的複型
從 EF Core 11 開始,複雜型態與 JSON 欄位可用於使用 TPT(逐表型別)或 TPC(每個具體型別表)的實體型別。 這讓你能結合這些繼承策略的彈性與複型態的建模能力。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
變更追蹤
EF Core 會追蹤複雜型別的個別屬性變更,因此當你呼叫 SaveChanges時,只有受影響的欄位會被更新。 你可以透過變更追蹤器檢查並操作這個追蹤狀態。
用來 EntityEntry.ComplexProperty 達到一個複雜性質,然後鑽入其純量性質:
var addressEntry = context.Entry(customer).ComplexProperty(c => c.Address);
Console.WriteLine($"City is currently: {addressEntry.Property(a => a.City).CurrentValue}");
Console.WriteLine($"Address was modified: {addressEntry.Property(a => a.City).IsModified}");
API ComplexPropertyEntry 會鏡像實體 EntityEntry API:你可以讀取並設定 CurrentValue、檢查並設定 IsModified,並瀏覽到更深入的巢狀複雜屬性或複雜集合。 複雜的屬性也會透過實體的 屬性值 API(CurrentValues/OriginalValues)揭露。
查詢複雜型態
複雜型態成員可以像實體本身的屬性一樣用於 LINQ 查詢——你可以對它們進行篩選、投影,並依序排序:
// Filter and project members of a complex property.
var ukCities = await context.Customers
.Where(c => c.Address.Country == "UK")
.Select(c => c.Address.City)
.ToListAsync();
由於複雜型態具有值語意,你也可以在查詢中比較整個複數值,EF 會比較其所有屬性:
var ordersToHomeAddress = await context.Orders
.Where(o => o.ShippingAddress == o.BillingAddress)
.ToListAsync();
對於映射到 JSON 的複雜型別,EF Core 11 新增 EF.Functions.JsonPathExists了 ,用以檢查文件中是否存在給定的 JSON 路徑:
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitations
複雜型別設計用於建模價值物件,並刻意不支援實體類型的所有能力。 主要限制包括:
- 沒有自己的身份或追蹤。 複型態只能作為實體的一部分存在;你不能擁有複雜型態的 ,
DbSet<T>也不能獨立追蹤或查詢它。 - 沒有導航。 複雜型別不能包含實體型別的導航屬性。
- 沒有獨立的桌子。 在關聯型資料庫中,複雜型別總是儲存在其容器的表格(透過表格拆分)或 JSON 欄位中,絕不會存在自己的表格中。
- 集合需要 JSON。 在關聯式提供者中,複雜的集合必須映射到 JSON
ToJson;它們無法透過資料表分割來映射。 - 不支援值類型的集合。 複雜的集合元素必須是參考型別。
- 可選複合類型則需具備必修屬性。 一個可選(可空)的複雜型態必須定義至少一個必需屬性。
複雜型別支援持續在各版本間擴展;請參閱 「最新內容」 頁面。