实现值对象
As discussed in earlier sections about entities and aggregates, identity is fundamental for entities. However, there are many objects and data items in a system that do not require an identity and identity tracking, such as value objects.
正如在之前关于实体和聚合的章节中所讨论的,身份(Identity)对于实体来说是至关重要的。然而,系统中还有许多对象和数据项并不需要身份标识和身份追踪,比如值对象(Value Object)。
A value object can reference other entities. For example, in an application that generates a route that describes how to get from one point to another, that route would be a value object. It would be a snapshot of points on a specific route, but this suggested route would not have an identity, even though internally it might refer to entities like City, Road, etc.
值对象可以引用其他的实体。例如,在一个生成路线描述(告诉你如何从一个地点到达另一个地点)的应用程序中,这条路线就是一个值对象。它只是特定路线上各个点的快照,但这条推荐的路线本身并不需要有一个身份ID,即使它的内部可能引用了像城市(City)、道路(Road)等实体。
Figure 7-13 shows the Address value object within the Order aggregate.
图 7-13 展示了订单(Order)聚合中的地址(Address)值对象。

Figure 7-13. Address value object within the Order aggregate
图 7-13. 订单(Order)聚合中的地址(Address)值对象
As shown in Figure 7-13, an entity is usually composed of multiple attributes. For example, the Order entity can be modeled as an entity with an identity and composed internally of a set of attributes such as OrderId, OrderDate, OrderItems, etc. But the address, which is simply a complex-value composed of country/region, street, city, etc., and has no identity in this domain, must be modeled and treated as a value object.
如图 7-13 所示,一个实体通常由多个属性组成。例如,订单(Order)实体可以被建模为一个拥有身份的实体,其内部包含一组属性,如订单ID(OrderId)、订单日期(OrderDate)、订单项(OrderItems)等。但是,地址(Address)仅仅是由国家/地区、街道、城市等组成的复合值,在这个领域中并没有独立的身份标识,因此它必须被建模并当作一个值对象来处理。
Important characteristics of value objects 值对象的重要特征
There are two main characteristics for value objects:
值对象主要有两个特征:
-
They have no identity.
它们没有身份标识。
-
They are immutable.
它们是不可变的(immutable)。
The first characteristic was already discussed. Immutability is an important requirement. The values of a value object must be immutable once the object is created. Therefore, when the object is constructed, you must provide the required values, but you must not allow them to change during the object's lifetime.
第一个特征前面已经讨论过了。不可变性是一个非常重要的要求。值对象的值一旦在对象被创建后,就必须保持不变。因此,当你构造对象时,必须提供所需的值,但在对象的整个生命周期内,你绝不允许修改这些值。
Value objects allow you to perform certain tricks for performance, thanks to their immutable nature. This is especially true in systems where there may be thousands of value object instances, many of which have the same values. Their immutable nature allows them to be reused; they can be interchangeable objects, since their values are the same and they have no identity. This type of optimization can sometimes make a difference between software that runs slowly and software with good performance. Of course, all these cases depend on the application environment and deployment context.
得益于其不可变的特性,值对象允许你使用一些提升性能的“小技巧”。这在那些可能包含成千上万个值对象实例、且其中许多实例具有相同数值的系统中尤其有效。它们的不可变性使得它们可以被复用;它们可以成为可互换的对象,因为它们的值相同且没有身份标识。这种类型的优化有时能决定软件是运行缓慢还是性能良好。当然,所有这些情况都取决于具体的应用环境和部署上下文。
Value object implementation in C# C# 中值对象的实现
In terms of implementation, you can have a value object base class that has basic utility methods like equality based on the comparison between all the attributes (since a value object must not be based on identity) and other fundamental characteristics. The following example shows a value object base class used in the ordering microservice from eShopOnContainers.
在实现层面,你可以拥有一个值对象基类,它包含一些基本的实用方法,比如基于所有属性进行比较的相等性判断(因为值对象绝不能基于身份标识来判断相等),以及其他一些基本特征。下面的示例展示了 eShopOnContainers 的订购微服务中所使用的值对象基类。
/// <summary>
/// 值对象抽象基类。在领域驱动设计 (DDD) 中,值对象没有唯一标识符,其相等性完全由其所包含的属性值决定。
/// 此类封装了通用的相等性比较逻辑和哈希码生成算法,子类只需实现属性提取方法即可。
/// </summary>
public abstract class ValueObject
{
/// <summary>
/// 判断两个值对象是否相等的静态运算符辅助方法。
/// </summary>
/// <param name="left">左侧的值对象。</param>
/// <param name="right">右侧的值对象。</param>
/// <returns>如果两者都为 null、引用相同或值相等,则返回 true;否则返回 false。</returns>
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
// 使用异或运算检查:如果其中一个为 null 而另一个不为 null,则它们肯定不相等
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
// 如果两者都是 null 或指向同一内存地址,或者通过 Equals 判定值相等,则返回 true
return ReferenceEquals(left, right) || left.Equals(right);
}
/// <summary>
/// 判断两个值对象是否不相等的静态运算符辅助方法。
/// </summary>
/// <param name="left">左侧的值对象。</param>
/// <param name="right">右侧的值对象。</param>
/// <returns>如果不相等则返回 true,否则返回 false。</returns>
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
// 直接取反 EqualOperator 的结果
return !(EqualOperator(left, right));
}
/// <summary>
/// 获取参与相等性比较的所有属性组件集合。
/// 派生类必须重写此方法,以提供用于比较的具体属性列表。
/// </summary>
/// <returns>包含所有需要参与相等性比较的属性值的枚举集合。</returns>
protected abstract IEnumerable<object> GetEqualityComponents();
/// <summary>
/// 重写 Equals 方法,基于值对象的各个属性值来判断两个对象是否相等。
/// </summary>
/// <param name="obj">要与当前对象进行比较的对象。</param>
/// <returns>如果类型相同且所有参与比较的属性值均相等,则返回 true;否则返回 false。</returns>
public override bool Equals(object obj)
{
// 如果传入对象为 null 或运行时类型与当前对象不一致,直接返回 false
if (obj == null || obj.GetType() != GetType())
{
return false;
}
// 将传入对象安全转换为 ValueObject 类型
var other = (ValueObject)obj;
// 依次比较双方提供的属性组件序列,确保每个元素的值和顺序都完全一致
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
/// <summary>
/// 重写 GetHashCode 方法,根据参与相等性比较的属性值生成哈希码。
/// 保证相等的值对象会生成相同的哈希码,以便正确放入字典或哈希集中。
/// </summary>
/// <returns>基于所有属性值计算出的组合哈希码。</returns>
public override int GetHashCode()
{
// 遍历所有参与比较的属性组件,处理可能存在的 null 值(null 视为 0),
// 最后通过异或运算 (^) 将所有属性的哈希码聚合为一个最终的哈希码
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// ...其他实用的工具方法...
}
The ValueObject is an abstract class type, but in this example, it doesn't overload the == and != operators. You could choose to do so, making comparisons delegate to the Equals override. For example, consider the following operator overloads to the ValueObject type:
ValueObject 是一个abstract class类型,不过在这个示例中,它并没有重载 == 和 != 运算符。你当然也可以选择这样做,让这两个运算符的比较逻辑直接交给(委托给)重写的 Equals 方法去处理。比如,可以考虑为 ValueObject 类型加上下面这样的运算符重载:
/// <summary>
/// 重载相等运算符 (==),用于比较两个值对象实例是否等价。
/// </summary>
/// <param name="one">左侧的值对象实例。</param>
/// <param name="two">右侧的值对象实例。</param>
/// <returns>如果两个值对象的属性值完全相同则返回 true;否则返回 false。</returns>
public static bool operator ==(ValueObject one, ValueObject two)
{
// 委托给内部辅助方法执行具体的相等性逻辑判断
return EqualOperator(one, two);
}
/// <summary>
/// 重载不等运算符 (!=),用于比较两个值对象实例是否不等价。
/// </summary>
/// <param name="one">左侧的值对象实例。</param>
/// <param name="two">右侧的值对象实例。</param>
/// <returns>如果两个值对象的属性值存在差异则返回 true;否则返回 false。</returns>
public static bool operator !=(ValueObject one, ValueObject two)
{
// 委托给内部辅助方法执行具体的不相等逻辑判断
return NotEqualOperator(one, two);
}
You can use this class when implementing your actual value object, as with the Address value object shown in the following example:
在实现你实际的值对象时,可以直接使用这个类,就像下面示例中展示的 Address(地址)值对象一样:
/// <summary>
/// 地址值对象(Value Object),用于封装实体的位置信息特征。
/// 作为值对象,它没有唯一标识符,且创建后不允许修改;当需要变更时,只能通过整体替换来实现。
/// </summary>
public class Address : ValueObject
{
/// <summary>
/// 获取街道信息。
/// </summary>
public String Street { get; private set; }
/// <summary>
/// 获取所在城市。
/// </summary>
public String City { get; private set; }
/// <summary>
/// 获取所在省份/州。
/// </summary>
public String State { get; private set; }
/// <summary>
/// 获取所在国家/地区。
/// </summary>
public String Country { get; private set; }
/// <summary>
/// 获取邮政编码。
/// </summary>
public String ZipCode { get; private set; }
/// <summary>
/// 无参构造函数,主要供 EF Core 等 ORM 框架在反序列化或映射时内部调用。
/// </summary>
public Address() { }
/// <summary>
/// 初始化一个新的地址实例,确保在创建时完成所有属性的完整赋值。
/// </summary>
/// <param name="street">街道信息。</param>
/// <param name="city">所在城市。</param>
/// <param name="state">所在省份/州。</param>
/// <param name="country">所在国家/地区。</param>
/// <param name="zipcode">邮政编码。</param>
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street; // 赋值街道
City = city; // 赋值城市
State = state; // 赋值省份/州
Country = country; // 赋值国家/地区
ZipCode = zipcode; // 赋值邮政编码
}
/// <summary>
/// 重写获取相等性组件的方法,用于定义值对象的相等性比较逻辑。
/// 只要以下所有属性都相同,两个 Address 实例即被视为相等。
/// </summary>
/// <returns>包含所有参与相等性比较的属性的枚举集合。</returns>
protected override IEnumerable<object> GetEqualityComponents()
{
// 使用 yield return 语句逐个返回参与比较的属性元素,以实现延迟加载和内存优化
yield return Street; // 加入街道进行比较
yield return City; // 加入城市进行比较
yield return State; // 加入省份/州进行比较
yield return Country; // 加入国家/地区进行比较
yield return ZipCode; // 加入邮政编码进行比较
}
}
This value object implementation of Address has no identity, and therefore no ID field is defined for it, either in the Address class definition or the ValueObject class definition.
这个 Address 值对象的实现是没有“身份标识”的,因此无论是在 Address 类的定义中,还是在 ValueObject 类的定义中,都没有定义 ID 字段。
Having no ID field in a class to be used by Entity Framework (EF) was not possible until EF Core 2.0, which greatly helps to implement better value objects with no ID. That is precisely the explanation of the next section.
在 Entity Framework (EF) 使用的类中不定义 ID 字段,这一点直到 EF Core 2.0 才得以实现,它极大地帮助了我们更好地实现这种不带 ID 的值对象。这正是下一节要详细解释的内容。
It could be argued that value objects, being immutable, should be read-only (that is, have get-only properties), and that's indeed true. However, value objects are usually serialized and deserialized to go through message queues, and being read-only stops the deserializer from assigning values, so you just leave them as private set, which is read-only enough to be practical.
有人可能会提出,值对象既然是不可变的,那就应该是只读的(也就是说,属性应该只有 get 访问器),这确实没错。然而,值对象通常需要经过序列化和反序列化,以便通过消息队列进行传输。如果属性是完全只读的,反序列化器就无法给它们赋值。所以,你只需要将属性设置为 private set(私有设置器),这在实践中已经足够“只读”了。
Value object comparison semantics 值对象的比较语义
Two instances of the Address type can be compared using all the following methods:
Address 类型的两个实例,可以使用以下所有方法进行比较:
// 创建第一个地址对象(值对象),传入街道、城市、州、国家和邮编
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
// 创建第二个地址对象,虽然是一个全新的实例,但所有属性值与 one 完全相同
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
// 使用默认的相等比较器进行判断:因为 Address 是值对象,基于内容而非引用进行比较,结果为 True
Console.WriteLine(EqualityComparer.Default.Equals(one, two)); // True
// 调用静态的 object.Equals 方法:同样触发值对象的内容比较逻辑,结果为 True
Console.WriteLine(object.Equals(one, two)); // True
// 调用实例级别的 Equals 方法:重写后比较内部属性是否一致,结果为 True
Console.WriteLine(one.Equals(two)); // True
// 使用 == 运算符进行比较:值对象重载了该运算符以支持按值相等,结果为 True
Console.WriteLine(one == two); // True
When all the values are the same, the comparisons are correctly evaluated as true. If you didn't choose to overload the == and != operators, then the last comparison of one == two would evaluate as false. For more information, see Overload ValueObject equality operators.
当所有属性的值都相同时,比较结果会被正确地判定为 true。如果你没有选择重载 == 和 != 运算符,那么最后的 one == two 比较结果就会判定为 false。想了解更多信息,请参见“重载 ValueObject 相等运算符”。
How to persist value objects in the database with EF Core 2.0 and later 如何使用 EF Core 2.0 及更高版本在数据库中持久化值对象
You just saw how to define a value object in your domain model. But how can you actually persist it into the database using Entity Framework Core since it usually targets entities with identity?
你刚刚了解了如何在领域模型中定义一个值对象。但是,既然 Entity Framework Core 通常是针对带有身份标识(ID)的实体进行设计的,那你要如何真正地将值对象持久化到数据库中呢?
Background and older approaches using EF Core 1.1 背景知识与 EF Core 1.1 及更早版本的旧方法
As background, a limitation when using EF Core 1.0 and 1.1 was that you could not use complex types as defined in EF 6.x in the traditional .NET Framework. Therefore, if using EF Core 1.0 or 1.1, you needed to store your value object as an EF entity with an ID field. Then, so it looked more like a value object with no identity, you could hide its ID so you make clear that the identity of a value object is not important in the domain model. You could hide that ID by using the ID as a shadow property. Since that configuration for hiding the ID in the model is set up in the EF infrastructure level, it would be kind of transparent for your domain model.
作为背景补充,在使用 EF Core 1.0 和 1.1 时存在一个限制:你无法像在传统的 .NET Framework 版 EF 6.x 中那样使用复杂类型(complex types)。因此,如果使用的是 EF Core 1.0 或 1.1,你就必须把你的值对象当作一个带有 ID 字段的 EF 实体来存储。为了让它看起来更像一个没有身份标识的值对象,你可以把这个 ID 隐藏起来,以此明确表明在领域模型中,值对象的“身份”并不重要。你可以通过将 ID 设置为影子属性(shadow property)来隐藏它。由于这种在模型中隐藏 ID 的配置是在 EF 基础结构层设置的,所以对你的领域模型来说,这几乎是完全透明的。
In the initial version of eShopOnContainers (.NET Core 1.1), the hidden ID needed by EF Core infrastructure was implemented in the following way in the DbContext level, using Fluent API at the infrastructure project. Therefore, the ID was hidden from the domain model point of view, but still present in the infrastructure.
在 eShopOnContainers 的初始版本(.NET Core 1.1)中,EF Core 基础结构所需的这个隐藏 ID,是在 DbContext 层级,通过在基础结构项目中使用 Fluent API 来实现的。因此,从领域模型的角度来看,这个 ID 被隐藏了,但它依然存在于基础结构层中。
// 旧版实现方式(基于 EF Core 1.1)
// 在基础设施层(Infrastructure project)的 OrderingContext:DbContext 中使用 Fluent API 进行配置
/// <summary>
/// 配置 Address(地址)实体的数据库映射关系。
/// </summary>
/// <param name="addressConfiguration">用于配置 Address 实体类型的 EntityFramework 构建器。</param>
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
// 将实体映射到数据库中指定默认架构(DEFAULT_SCHEMA)下的 "address" 表
addressConfiguration.ToTable("address", DEFAULT_SCHEMA);
// 将 "Id" 配置为影子属性(Shadow Property),即该属性仅存在于 EF Core 的数据模型中,而不在 C# 实体类中显式定义
// IsRequired() 表示该字段在数据库中不允许为空
addressConfiguration.Property<int>("Id")
.IsRequired();
// 将影子属性 "Id" 设置为该实体的主键
addressConfiguration.HasKey("Id");
}
However, the persistence of that value object into the database was performed like a regular entity in a different table.
不过,当时那个值对象在数据库中的持久化方式,就像是存储在一个独立表中的普通实体一样。
With EF Core 2.0 and later, there are new and better ways to persist value objects.
而从 EF Core 2.0 及更高版本开始,出现了更新、更好的持久化值对象的方法。
Persist value objects as owned entity types in EF Core 2.0 and later 在 EF Core 2.0 及更高版本中,将值对象作为“从属的实体类型”进行持久化
Even with some gaps between the canonical value object pattern in DDD and the owned entity type in EF Core, it's currently the best way to persist value objects with EF Core 2.0 and later. You can see limitations at the end of this section.
尽管 DDD 中经典的值对象模式与 EF Core 中的“从属实体类型”之间仍存在一些差异,但这目前仍然是使用 EF Core 2.0 及更高版本持久化值对象的最佳方式。你可以在本节的末尾看到相关的局限性说明。
The owned entity type feature was added to EF Core since version 2.0.
“从属实体类型”这一特性是从 EF Core 2.0 版本开始加入的。
An owned entity type allows you to map types that do not have their own identity explicitly defined in the domain model and are used as properties, such as a value object, within any of your entities. An owned entity type shares the same CLR type with another entity type (that is, it's just a regular class). The entity containing the defining navigation is the owner entity. When querying the owner, the owned types are included by default.
从属实体类型允许你映射那些在领域模型中没有明确定义自身身份标识(ID)的类型。它们通常作为属性被用在你的各个实体中(比如值对象)。从属实体类型与其他的实体类型共享同一个 CLR 类型(也就是说,它本质上就是一个普通的类)。而包含该导航属性的实体,就是“所有者实体”。当查询所有者实体时,默认情况下会把从属类型也一并查出来。
Just by looking at the domain model, an owned type looks like it doesn't have any identity. However, under the covers, owned types do have the identity, but the owner navigation property is part of this identity.
单从领域模型来看,从属类型看起来好像没有任何身份标识。然而,在底层,从属类型其实是有身份标识的,只是“所有者的导航属性”构成了这个身份标识的一部分。
The identity of instances of owned types is not completely their own. It consists of three components:
从属类型实例的身份标识并不完全属于它们自己。它由以下三个部分组成:
-
The identity of the owner
所有者的身份标识
-
The navigation property pointing to them
指向它们的导航属性
-
In the case of collections of owned types, an independent component (supported in EF Core 2.2 and later).
如果是从属类型集合,则还需要一个独立的组件(EF Core 2.2 及更高版本支持)。
For example, in the Ordering domain model at eShopOnContainers, as part of the Order entity, the Address value object is implemented as an owned entity type within the owner entity, which is the Order entity. Address is a type with no identity property defined in the domain model. It is used as a property of the Order type to specify the shipping address for a particular order.
举个例子,在 eShopOnContainers 的订单(Ordering)领域模型中,作为订单(Order)实体的一部分,Address 值对象就是作为所有者实体(即 Order 实体)内部的一个“从属实体类型”来实现的。Address 是一个在领域模型中没有定义任何身份标识属性的类型。它被用作 Order 类型的一个属性,用来指定某个特定订单的收货地址。
By convention, a shadow primary key is created for the owned type and it will be mapped to the same table as the owner by using table splitting. This allows to use owned types similarly to how complex types are used in EF6 in the traditional .NET Framework.
按照惯例,EF Core 会为从属类型创建一个影子主键,并且会通过“表拆分”的方式,将其映射到与所有者实体相同的数据库表中。这使得从属类型在使用方式上,非常类似于传统 .NET Framework 版 EF6 中的复杂类型。
It is important to note that owned types are never discovered by convention in EF Core, so you have to declare them explicitly.
需要特别注意的是,在 EF Core 中,从属类型永远不会通过约定自动发现,所以你必须显式地去声明它们。
In eShopOnContainers, in the OrderingContext.cs file, within the OnModelCreating() method, multiple infrastructure configurations are applied. One of them is related to the Order entity.
在 eShopOnContainers 的 OrderingContext.cs 文件中,OnModelCreating() 方法里应用了多项基础结构配置,其中就有一项是关于 Order 实体的。
// 属于 Ordering.Infrastructure 项目中的 OrderingContext.cs 类
// 该方法是 EF Core 模型构建的核心入口点,用于将 DDD 领域的实体映射到数据库结构
/// <summary>
/// 重写 EF Core 的模型创建方法,用于应用所有聚合根及实体的类型配置(Fluent API)。
/// </summary>
/// <param name="modelBuilder">EF Core 提供的模型构建器实例。</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 应用客户端请求(防重放机制)的实体配置
modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
// 应用支付方式实体的类型配置
modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
// 应用订单聚合根实体的类型配置
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
// 应用订单项子实体的类型配置
modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
// ...其他相关的实体类型配置
}
In the following code, the persistence infrastructure is defined for the Order entity:
在下面的代码中,定义了 Order 实体的持久化基础结构:
// 属于 OrderEntityTypeConfiguration.cs 类的一部分
// 该类用于集中配置 Order 聚合根在数据库中的持久化映射规则
/// <summary>
/// 配置 Order 聚合根的 Entity Framework Core 实体映射关系。
/// </summary>
/// <param name="orderConfiguration">Order 实体的类型构建器。</param>
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
// 指定该实体映射到数据库中的 "orders" 表,并归属于默认的 Ordering 架构 (Schema)
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
// 设置主键为 Id 属性
orderConfiguration.HasKey(o => o.Id);
// 忽略领域事件集合,避免其被映射到数据库中(DDD 最佳实践:领域事件仅在内存中流转或由基础设施层单独处理)
orderConfiguration.Ignore(b => b.DomainEvents);
// 配置主键生成策略:使用 SQL Server 的 HiLo 序列算法来生成 ID,以提高高并发下的插入性能
orderConfiguration.Property(o => o.Id)
.ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);
// 将 Address 值对象作为“所属实体”(Owned Entity)进行持久化,这是 EF Core 2.0+ 处理 DDD 值对象的推荐方式
orderConfiguration.OwnsOne(o => o.Address);
// 显式配置隐式属性 "OrderDate" 的映射,并将其设置为必填字段
orderConfiguration.Property<DateTime>("OrderDate").IsRequired();
// ...其他额外的验证、约束以及配置代码...
// ...
}
In the previous code, the orderConfiguration.OwnsOne(o => o.Address) method specifies that the Address property is an owned entity of the Order type.
在前面的代码中,orderConfiguration.OwnsOne(o => o.Address) 方法指明了 Address 属性是 Order 类型的一个“从属实体”。
By default, EF Core conventions name the database columns for the properties of the owned entity type as EntityProperty_OwnedEntityProperty. Therefore, the internal properties of Address will appear in the Orders table with the names Address_Street, Address_City (and so on for State, Country, and ZipCode).
按照 EF Core 的默认命名约定,从属实体类型的属性在数据库中的列名会被命名为 实体属性_从属实体属性(EntityProperty_OwnedEntityProperty)。因此,Address 的内部属性在 Orders 表中会显示为 Address_Street、Address_City(以此类推,还有 State、Country 和 ZipCode)。
You can append the Property().HasColumnName() fluent method to rename those columns. In the case where Address is a public property, the mappings would be like the following:
你可以通过链式调用 Property().HasColumnName() 这个 Fluent API 方法来重命名这些列。假设 Address 是一个公共属性,那么具体的映射配置就会像下面这样:
// 配置订单实体中 Address 值对象的属性与数据库列的映射关系
// 使用 OwnsOne 将 Address 作为订单的所属类型(Owned Entity / 值对象)进行映射
orderConfiguration.OwnsOne(p => p.Address)
// 将 Address 中的 Street 属性映射到数据库表的 "ShippingStreet" 列
.Property(p => p.Street).HasColumnName("ShippingStreet");
// 继续配置 Address 值对象的下一个属性映射
orderConfiguration.OwnsOne(p => p.Address)
// 将 Address 中的 City 属性映射到数据库表的 "ShippingCity" 列
.Property(p => p.City).HasColumnName("ShippingCity");
It's possible to chain the OwnsOne method in a fluent mapping. In the following hypothetical example, OrderDetails owns BillingAddress and ShippingAddress, which are both Address types. Then OrderDetails is owned by the Order type.
在 Fluent API 映射中,是可以对 OwnsOne 方法进行链式调用的。在下面这个假设的示例中,OrderDetails 拥有 BillingAddress(账单地址)和 ShippingAddress(收货地址),而这两个属性都是 Address 类型。与此同时,OrderDetails 本身又是被 Order 类型所拥有。
// 配置 Order 聚合根与 OrderDetails 之间的“拥有”关系(OwnsOne)
// 这表示 OrderDetails 是 Order 的值对象,其生命周期完全依附于 Order,没有独立的 ID
orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
{
// 在 OrderDetails 内部继续嵌套配置 BillingAddress(账单地址)为值对象
cb.OwnsOne(c => c.BillingAddress);
// 在 OrderDetails 内部继续嵌套配置 ShippingAddress(收货地址)为值对象
cb.OwnsOne(c => c.ShippingAddress);
});
// ...其他实体或配置代码...
// ...
/// <summary>
/// 订单聚合根实体,作为订单上下文的核心入口。
/// </summary>
public class Order
{
/// <summary>
/// 订单的唯一标识符(主键)。
/// </summary>
public int Id { get; set; }
/// <summary>
/// 订单详情(值对象),包含与订单相关的附加信息如地址等。
/// </summary>
public OrderDetails OrderDetails { get; set; }
}
/// <summary>
/// 订单详情值对象,封装了订单的账单地址和收货地址。
/// 在 DDD 中,它没有独立的生命周期,仅通过父级 Order 进行访问和管理。
/// </summary>
public class OrderDetails
{
/// <summary>
/// 账单地址(值对象),用于记录财务结算所需的地址信息。
/// </summary>
public Address BillingAddress { get; set; }
/// <summary>
/// 收货地址(值对象),用于记录商品物流配送的目标地址。
/// </summary>
public Address ShippingAddress { get; set; }
}
/// <summary>
/// 地址值对象,封装了基础的地理位置信息。
/// 由于被多处复用(如账单地址、收货地址),将其提取为独立的值对象以保证领域模型的纯粹性。
/// </summary>
public class Address
{
/// <summary>
/// 街道名称及门牌号。
/// </summary>
public string Street { get; set; }
/// <summary>
/// 所在城市名称。
/// </summary>
public string City { get; set; }
}
Additional details on owned entity types 关于从属实体类型的更多细节
-
Owned types are defined when you configure a navigation property to a particular type using the OwnsOne fluent API.
当你使用 OwnsOne Fluent API 将导航属性配置为某个特定类型时,从属类型就定义好了。
-
The definition of an owned type in our metadata model is a composite of: the owner type, the navigation property, and the CLR type of the owned type.
在我们的元数据模型中,从属类型的定义由以下三部分复合而成:所有者类型、导航属性以及从属类型的 CLR 类型。
-
The identity (key) of an owned type instance in our stack is a composite of the identity of the owner type and the definition of the owned type.
在我们这套技术栈中,从属类型实例的身份标识(主键)也是一个复合体,它由所有者类型的身份标识和从属类型的定义共同组成。
Owned entities capabilities 从属实体的功能特性
-
Owned types can reference other entities, either owned (nested owned types) or non-owned (regular reference navigation properties to other entities).
从属类型可以引用其他实体,既可以是从属(嵌套的从属类型),也可以是非从属(指向其他实体的常规引用导航属性)。
-
You can map the same CLR type as different owned types in the same owner entity through separate navigation properties.
你可以通过不同的导航属性,在同一个所有者实体中,将相同的 CLR 类型映射为不同的从属类型。
-
Table splitting is set up by convention, but you can opt out by mapping the owned type to a different table using ToTable.
表拆分(Table splitting)是按默认约定自动设置的,但你可以通过使用 ToTable 将从属类型映射到不同的表来退出这种默认行为。
-
Eager loading is performed automatically on owned types, that is, there's no need to call
.Include()on the query.从属类型会自动执行贪婪加载(Eager loading),也就是说,在查询时根本不需要调用
.Include()。 -
Can be configured with attribute
[Owned], using EF Core 2.1 and later.在使用 EF Core 2.1 及更高版本时,可以通过
[Owned]特性来进行配置。 -
Can handle collections of owned types (using version 2.2 and later).
支持处理从属类型集合(需使用 EF Core 2.2 及更高版本)。
Owned entities limitations 从属实体的限制条件
-
You can't create a
DbSet<T>of an owned type (by design).不能为从属类型创建
DbSet<T>(这是设计如此)。 -
You can't call
ModelBuilder.Entity<T>()on owned types (currently by design).不能在从属类型上调用
ModelBuilder.Entity<T>()(目前也是设计如此)。 -
No support for optional (that is, nullable) owned types that are mapped with the owner in the same table (that is, using table splitting). This is because mapping is done for each property, there is no separate sentinel for the null complex value as a whole.
不支持映射在与所有者同一张表中的可选(即可为空)从属类型(也就是使用表拆分的情况)。这是因为映射是针对每个属性单独进行的,系统并没有为整个复杂的空值(null complex value)设置单独的标记。
-
No inheritance-mapping support for owned types, but you should be able to map two leaf types of the same inheritance hierarchies as different owned types. EF Core will not reason about the fact that they are part of the same hierarchy.
不支持对从属类型进行继承映射,但你应该能够将同一继承层次结构中的两个叶子类型,映射为不同的从属类型。不过,EF Core 并不会去推导它们属于同一个继承层次结构这一事实。
Main differences with EF6's complex types 与 EF6 复杂类型的主要区别
-
Table splitting is optional, that is, they can optionally be mapped to a separate table and still be owned types.
表拆分是可选的,也就是说,它们可以选择被映射到一张单独的表中,但依然保持作为从属类型。

浙公网安备 33010602011771号