如何在不污染我的域模型的情况下在实体框架中保留值对象? EF(通常是关系数据库)要求我定义一个键 - 我的值对象没有开箱即用,例如
public class Tag : ValueObject<Tag>
{
private readonly string name;
public Tag(string name)
{
this.name = name;
}
public string Name { get { return this.name; }}
}
另一方面,我不应该解决模型中的持久性问题。我真的应该创建另一个类,其中包含值对象的所有字段加上一个键属性,然后将它们相互映射?我不愿意。
是否有更优雅的解决方案?
答案 0 :(得分:13)
Vaughn Vernon在他出色的着作Implementing Domain-Driven Design中写到了坚持价值对象(第248页)。
ORM和单值对象
基本思想是将Value的每个属性存储在存储其父Entity的行的不同列中。换句话说,将单个值对象非规范化为其父实体的行。使用列命名约定来清楚地标识和标准化序列化对象的命名方式是有好处的。
由数据库实体支持的ORM和许多值
使用ORM和关系数据库持久化Value实例集合的一种非常简单的方法是将Value类型视为数据模型中的实体。 (...)为实现这一目标,我们可以使用Layer Supertype。
可以在此处找到C#中的有界上下文示例:https://github.com/VaughnVernon/IDDD_Samples_NET
答案 1 :(得分:6)
我目前正在解决其中一些相同的挑战。我真的不喜欢向你的基础ValueObject<T>
类添加一个Id,因为它为所有值对象提供了一个Id,无论它们是否需要加上作为一个值对象,根据定义没有Id,它&# 39;继承该基类型的任何东西都不再是纯粹意义上的价值对象。
在我走得更远之前,我会注意到编码DDD的一个关键概念是你不必在任何地方都是纯DDD,只要你知道你做出的让步和他们的权衡。话虽这么说,你的方法当然可以被认为是好的,但我相信它会增加一个可能并非真正必要的让步。首先,这会影响值对象的相等性。通过添加Id,两个标签即使具有相同的名称也不再相等。
以下是我处理这种情况的方法: 首先是简单的,不适用于我认为你的问题,但它很重要。这是Martin答案第一部分中的单一价值对象。
只要您的值对象只包含简单的类型属性,Entity Framework就会很好地映射它。
例如:
public class BlogEntry : Entity<Guid>
{
public String Text { get; private set; }
public Tag Tag { get; private set; }
// Constructors, Factories, Methods, etc
}
实体框架将处理这一点很好,你最终会得到的是一个只包含以下内容的单表BlogEntry:
现在我认为在这种情况下并不是你真正想要的东西,但是对于许多有价值的物品来说,它的效果非常好。我经常使用的是DateRange值对象,它由几个属性组成。然后在我的域对象上,我只有一个DateRange类型的属性。 EF将这些映射到域对象本身的表中。
我提出这个问题是因为回到我们向ValueObject<T>
基类型添加Id所做出的让步,即使它可能没有在您的域对象具体实现中列出,它仍然存在并仍然存在得到实体框架,为此,可能最常见的价值对象用例不再适用。
好的,最后,你的特定情况(我也遇到过几次)。这是我如何选择处理实体包含值对象列表的需要。基本上它归结为扩展我们对域的理解。假设Tag值对象用于在博客文章中记录Tag,我看待它的方式是BlogPost包含一个PostTag列表,其值为Tag。是的,它是另一个类,但你不需要为每个值对象添加它,只有当你有一个值对象列表时才需要它,我认为更好地表达了正在发生的事情。
所以这里是一个向实体添加值对象列表的示例(使用上面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包含多个标签而不会损害价值对象,实体框架会将其映射得很好而无需做任何特殊的事情。
答案 2 :(得分:0)
对于那些使用Entity Framework Core 2.0的人来说,我认为了解它可能会有用
在Entity Framework(EF)使用的类中没有ID字段是 在EF Core 2.0之前是不可能的,这大大有助于实现 没有ID的价值更高的对象。
以下是Microsoft有关此功能的详细信息:
将值对象持久保存为EF Core 2.0和更高版本中的拥有实体类型 即使DDD中的规范值对象模式之间存在一些间隙 以及EF Core中的拥有实体类型,这是目前的最佳方法 使用EF Core 2.0和更高版本持久保存值对象。
自2.0版以来,拥有实体类型功能已添加到EF Core。
拥有实体类型可让您映射没有实体类型的类型 在域模型中明确定义的自己的身份,并用作 您任何实体中的属性(例如值对象)。一个 拥有的实体类型与另一个实体类型共享相同的CLR类型 (也就是说,这只是一个普通的类)。包含 定义导航为所有者实体。查询所有者时, 默认包含所有类型。
仅通过查看域模型,便可以看到一个拥有的类型 没有任何身份但是,在幕后,拥有类型确实可以 有身份,但所有者导航属性是其中的一部分 身份。
拥有类型实例的标识并不完全是它们自己的。 它由三个部分组成:
-所有者的身份
-指向它们的导航属性
-对于拥有类型的集合,是一个独立的组件(在EF Core 2.2和更高版本中受支持)。
按照惯例,将为拥有的类型创建一个影子主键, 它将通过使用表映射到与所有者相同的表 分裂。这允许使用拥有类型类似于复杂程度 类型在传统.NET Framework的EF6中使用。
重要的是要注意,拥有者永远不会发现拥有的类型 EF Core中的约定,因此必须显式声明它们。
有关拥有实体类型的其他详细信息
-当您使用OwnsOne fluent API将导航属性配置为特定类型时,将定义拥有类型。
-我们的元数据模型中所有权类型的定义是以下各项的组合:所有者类型,导航属性和CLR类型的CLR类型。 拥有的类型。
-我们的堆栈中拥有类型实例的标识(密钥)是所有者类型的标识和 拥有的类型。
拥有实体的功能
-拥有的类型可以引用其他实体,这些实体既可以是拥有的(嵌套拥有的类型),也可以是非拥有的(其他实体的常规参考导航属性) 实体)。
-您可以通过单独的导航属性将同一CLR类型映射为同一所有者实体中的不同拥有类型。
-表拆分是按照约定设置的,但是您可以通过使用ToTable将拥有的类型映射到其他表来退出。
-预先加载会自动对拥有的类型执行加载,也就是说,无需在查询中调用.Include()。
-可以使用EF Core 2.1及更高版本,通过[拥有]属性进行配置。
-可以处理拥有类型的集合(使用2.2版及更高版本)。
拥有实体的限制
-您不能创建拥有类型的DbSet(通过设计)。
-您不能在拥有的类型上调用ModelBuilder.Entity()(当前是设计使然)。
-不支持与所有者在同一表(即使用表)中映射的可选(即可为空)拥有的类型 分裂)。这是因为映射是针对每个属性完成的,我们 整个null值没有单独的标记。
-不支持对拥有类型的继承映射,但是您应该能够映射具有相同继承层次结构的两个叶子类型 不同的拥有类型。 EF Core不会对以下事实进行推理 它们属于同一层次结构。
与EF6复杂类型的主要区别
-表拆分是可选的,也就是说,可以选择性地将它们映射到单独的表,并且仍然是拥有的类型。
-他们可以引用其他实体(也就是说,它们可以充当与其他非拥有类型的关系的依赖方)。