複雜類型

儲存至資料庫的物件可以分割成三大類別:

  • 非結構化物件並保留單一值。 例如,、intGuidstringIPAddress。 這些(較寬鬆地)被稱為 原始類型
  • 用於保存多個值且其身份由索引鍵值定義的物件。 例如,BlogPostCustomer。 這些稱為 實體類型
  • 物件結構化可承載多個值,但物件沒有定義其身份的金鑰。 例如,AddressCoordinateMoney。 這些稱為 值物件,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 classrecord),或是值型別(a structrecord 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的集合目前不支援;複雜集合元素請使用參考型別(classrecord)。

複雜型態映射到 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;它們無法透過資料表分割來映射。
  • 不支援值類型的集合。 複雜的集合元素必須是參考型別。
  • 可選複合類型則需具備必修屬性。 一個可選(可空)的複雜型態必須定義至少一個必需屬性。

複雜型別支援持續在各版本間擴展;請參閱 「最新內容」 頁面。