如何处理Entity Framework中的值对象?

How to deal with value objects in Entity Framework?

如何在 Entity Framework 中持久化值对象而不污染我的域模型? EF(好吧,通常是关系数据库)要求我定义一个键——我的值对象没有现成的键,例如

public class Tag : ValueObject<Tag>
{
   private readonly string name;

   public Tag(string name)
   {
      this.name = name;
   }

   public string Name { get { return this.name; }}
}

另一方面,我不应该解决模型中的持久性问题。我真的应该创建另一个 class 来包含值对象中的所有字段加上一个键 属性 然后将它们相互映射吗?我宁愿不要。

是否有更优雅的解决方案?

Vaughn Vernon 在他的优秀著作 Implementing Domain-Driven Design.

中介绍了持久值对象(第 248 页)

ORM 和单值对象

The basic idea is to store each of the attributes of the Value in separate columns of the row where its parent Entity is stored. Said another way, a single Value Object is denormalized into its parent Entity's row. There are advantages to employing convention for column naming to clearly identity and standardize the way serialized objects are named.

ORM 和数据库实体支持的许多值

A very straightforward approach to persisting a collection of Value instances using an ORM and a relational database is to treat the Value type as an entity in the data model. (...) To accomplish this we can employ a Layer Supertype.

可在此处找到 C# 中的示例限界上下文:https://github.com/VaughnVernon/IDDD_Samples_NET

我目前正在应对其中一些相同的挑战。我真的不喜欢将 Id 添加到您的基础 ValueObject<T> class 因为这会为所有值对象提供一个 Id,无论它们是否需要加上定义的值对象没有 Id,任何继承该基类型的东西都将不再是纯粹意义上的值对象。

在我进一步讨论之前,我要指出编码 DDD 的一个关键概念是您不必在任何地方都是纯 DDD,只要您知道您做出的让步和它们的权衡即可。话虽这么说,你的方法当然可以被认为是好的,但我相信它增加了一个可能不是真正必要的让步。首先,这会影响您的值对象的相等性。加上Id,两个同名的Tag不再相等

以下是我对这种情况的处理方法: 首先是简单的,并不真正适用于我认为你的问题,但它很重要。这是Martin的回答第一部分中的单值对象。

  • 使值对象成为实体的 属性。

只要您的值对象只包含简单的类型属性,Entity Framework 就可以很好地映射它。

例如:

    public class BlogEntry : Entity<Guid>
    {
         public String Text { get; private set; }
         public Tag Tag { get; private set; }

         // Constructors, Factories, Methods, etc
    }

Entity Framework 会处理得很好,您最终会得到一个 table BlogEntry,它仅包含以下内容:

  • 编号
  • 文字
  • Tag_Name

现在我认为在这种情况下这并不是您真正想要的,但对于许多值对象来说效果很好。我经常使用的一个是 DateRange 值对象,它由几个属性组成。然后在我的域对象上,我只有一个 属性 类型的 DateRange。 EF 将这些映射到域对象本身的 table。

我提出这个问题是因为回到我们将 Id 添加到 ValueObject<T> 基类型所做的让步,即使它 Id 可能没有在您的域对象具体实现中列出,它仍然存在并且仍然会被 Entity Framework 选中,为此,可能是最常见的值对象用例不再那么好用了。

好的,最后,关于你的具体案例(我也有几次 运行)。以下是我选择如何处理实体包含值对象列表的需要。基本上它归结为扩展我们对领域的理解。假设 Tag 值对象用于记录博客中的 Tag post,我的看法是 BlogPost 包含具有 Tag 值的 PostTag 列表。是的,它还有一个 class,但你不需要为每个值对象添加它,只有当你有一个值对象列表时才需要它,我认为更好地表达了正在发生的事情。

下面是一个将值对象列表添加到实体的示例(使用上面的 Tag 值对象):

    public class BlogEntry : Entity<Guid>
    {
         public String Text { get; private set; }
         public ICollection<PostTag> PostTags { get; private set; }

         // Constructors:
         private BlogEntry(Guid id) : base(id) { }
         protected BlogEntry() : this(Guid.NewGuid()) { }

         // Factories:
         public static BlogEntry Create (String text, ICollection<PostTag> tags = null)
         {
             if(tags == null) { tags = new List<PostTag>(); }
             return new BlogEntry(){ Text = text, Tags = tags };
         }        

         // Methods:
         public void AddTag(String name)
         {
             PostTags.Add(PostTag.Create(name));
         }
    }

    public class PostTag : Entity<Guid>
    {
        // Properties:
        public Tag Tag { get; private set; }
        public DateTime DateAdded { get; private set; } // Properties that aren't relevant to the value of Tag.

        // Constructors:
        private PostTag(Guid id) : base(id) { }
        protected PostTag() : this(Guid.NewGuid()) { }

        // Factories:
        public static PostTag Create(Tag tag) 
        { 
            return new PostTag(){ Tag = tag, DateAdded = DateTime.Now };
        }

        public static PostTag Create(Tag tag, DateTime dateAdded) 
        { 
            return new PostTag(){ Tag = tag, DateAdded = dateAdded };
        }
    }

这将允许您的 BlogEntry 包含多个标签而不影响值对象,并且 Entity Framework 将很好地映射它而无需执行任何特殊操作。

我认为对于那些使用 Entity Framework Core 2.0 的人来说,了解

可能会有用

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.

以下是 Microsoft 提供的有关此功能的详细信息:

Persist value objects as owned entity types in EF Core 2.0 and later 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.

The owned entity type feature was added to EF Core since version 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.

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 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).

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.

It is important to note that owned types are never discovered by convention in EF Core, so you have to declare them explicitly.

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.

-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.

-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.

-Table splitting is set up by convention, but you can opt out by mapping the owned type to a different table using ToTable.

-Eager loading is performed automatically on owned types, that is, there's no need to call .Include() on the query.

-Can be configured with attribute [Owned], using EF Core 2.1 and later.

-Can handle collections of owned types (using version 2.2 and later).

Owned entities limitations

-You can't create a DbSet of an owned type (by design).

-You can't call ModelBuilder.Entity() on owned types (currently by design).

-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, we don't have a separate sentinel for the null complex value as a whole.

-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.

Main differences with EF6's complex types

-Table splitting is optional, that is, they can optionally be mapped to a separate table and still be owned types.

-They can reference other entities (that is, they can act as the dependent side on relationships to other non-owned types).

https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects