保存到数据库的对象可以分为三大类:
- 非结构化并保存单个值的对象。 例如、
int、Guidstring、IPAddress。 这些类型(有点松散)称为 基元类型。 - 为保存多个值而构造的对象,对象的标识由键值定义。 例如、
Blog、Post。Customer这些称为 实体类型。 - 结构化为保存多个值的对象,但对象没有定义其标识的键。 例如、
Address、Coordinate。Money这些对象称为 值对象,EF Core 将它们映射为 复杂类型。
复杂类型将多个属性分组为实体类型中包含的单个.NET类型;它没有自己的标识,不能单独跟踪或查询。 这使得复杂类型成为模型 值对象的自然方法。
Tip
可以针对本文GitHub运行和调试完整的示例项目。
注释
复杂类型已在 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每个属性调用 Fluent API 来显式配置它们:注释类型或调用 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引用类型(或classrecord)或值类型(struct或 record structEF 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 (init-only 或只读属性)、a record、a readonly struct或 a readonly record struct表示。 值类型 (struct) 具有复制语义,因此分配它们始终会复制值并避免意外共享问题,即使可变 - 但 可变结构通常不建议在 C# 中使用,因此仍建议使用不可变形式。
Tip
如果多个实体确实应该观察相同的地址并在更改时一起更新,则使用自己的标识将地址建模为 实体类型 ,并通过导航引用它,而不是使用复杂类型。
嵌套的复杂类型
复杂类型可以包含其他复杂类型的属性,使你可以将结构化对象构建到任何深度。 例如, 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 属性必须始终具有值,并且它映射到不可为 null 的列。 从 EF Core 10 开始,可以通过将复杂属性声明为可为 null 来可选:
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!;
}
一个可选的复杂属性,它 null 会导致 NULL 其所有列中的值。
注释
可选复杂类型当前要求在复杂类型上定义至少一个 必需 属性。 这是因为 EF 至少需要一个不可为 null 的列才能将复杂值与所有属性都可能发生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])
);
注释
当前不支持值类型的集合;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");
注释
在 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();
局限性
复杂类型设计用于对值对象进行建模,并且有意不支持实体类型的每个功能。 主要限制包括:
- 没有自己的标识或跟踪。 复杂类型只能作为实体的一部分存在;不能具有
DbSet<T>复杂类型,也不能独立跟踪或查询它。 - 无导航。 复杂类型不能包含实体类型的导航属性。
- 没有单独的表。 在关系数据库上,复杂类型始终存储在其容器的表中(通过表拆分)或 JSON 列中-从不存储在其自己的表中。
- 集合需要 JSON。 在关系提供程序上,复杂集合必须映射到 JSON;
ToJson它们不能通过表拆分进行映射。 - 不支持值类型的集合。 复杂集合元素必须是引用类型。
- 可选复杂类型需要必需的属性。 可选(可为 null)复杂类型必须至少定义一个必需属性。
复杂类型支持在各个版本中继续扩大;查看最新新增功能 的新 页面。