使用领域驱动设计的实体框架聚合根

10

我正在使用领域驱动设计构建一个应用程序,该应用程序使用实体框架。

我的目标是允许我的领域模型(使用EF持久化)包含一些逻辑。

默认情况下,实体框架对于如何将实体添加到图形中并进行持久化相当宽松。

以我的领域为例,它是一个没有逻辑的POCO:

public class Organization
{
    private ICollection<Person> _people = new List<Person>(); 

    public int ID { get; set; }

    public string CompanyName { get; set; }

    public virtual ICollection<Person> People { get { return _people; } protected set { _people = value; } }
}

public class Person
{
    public int ID { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual Organization Organization { get; protected set; }
}

public class OrganizationConfiguration : EntityTypeConfiguration<Organization>
{
    public OrganizationConfiguration()
    {
        HasMany(o => o.People).WithRequired(p => p.Organization); //.Map(m => m.MapKey("OrganizationID"));
    }
}

public class PersonConfiguration : EntityTypeConfiguration<Person>
{
    public PersonConfiguration()
    {
        HasRequired(p => p.Organization).WithMany(o => o.People); //.Map(m => m.MapKey("OrganizationID"));
    }
}

public class MyDbContext : DbContext
{
    public MyDbContext()
        : base(@"Data Source=(localdb)\v11.0;Initial Catalog=stackoverflow;Integrated Security=true")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new PersonConfiguration());
        modelBuilder.Configurations.Add(new OrganizationConfiguration());
    }

    public IDbSet<Organization> Organizations { get; set; }
    public IDbSet<Person> People { get; set; } 
}

我的例子是一个组织可以拥有许多人。一个人只能属于一个组织。

创建一个组织并向其中添加人员非常简单:

using (var context = new MyDbContext())
{
    var organization = new Organization
    {
        CompanyName = "Matthew's Widget Factory"
    };

    organization.People.Add(new Person {FirstName = "Steve", LastName = "McQueen"});
    organization.People.Add(new Person {FirstName = "Bob", LastName = "Marley"});
    organization.People.Add(new Person {FirstName = "Bob", LastName = "Dylan" });
    organization.People.Add(new Person {FirstName = "Jennifer", LastName = "Lawrence" });

    context.Organizations.Add(organization);

    context.SaveChanges();
}

我的测试查询是。

var organizationsWithSteve = context.Organizations.Where(o => o.People.Any(p => p.FirstName == "Steve"));

上述班级布局不符合域的工作方式。例如,所有人都隶属于一个组织,而组织是聚合根。这样做 context.People.Add(...) 没有意义,因为这不符合域的工作方式。

如果我们想要向 Organization 模型添加一些逻辑来限制该组织中可以有多少人,我们可以实现一个方法。

public Person AddPerson(string firstName, string lastName)
{
    if (People.Count() >= 5)
    {
        throw new InvalidOperationException("Your organization already at max capacity");
    }

    var person = new Person(firstName, lastName);
    this.People.Add(person);
    return person;
}
然而,由于当前类的布局,我可以通过调用organization.Persons.Add(...)或完全忽略聚合根而执行context.Persons.Add(...)来规避AddPerson逻辑,这两种方式我都不想采取。
我的解决方案建议(它并不起作用,这也是我在此发帖的原因)是:
public class Organization
{
    private List<Person> _people = new List<Person>(); 

    // ...

    protected virtual List<Person> WritablePeople
    {
        get { return _people; }
        set { _people = value; }
    }

    public virtual IReadOnlyCollection<Person> People { get { return People.AsReadOnly(); } }

    public void AddPerson(string firstName, string lastName)
    {
                    // do domain logic / validation

        WriteablePeople.Add(...);
    }
}

由于映射代码 HasMany(o => o.People).WithRequired(p => p.Organization); 无法编译,因为 HasMany 需要一个 ICollection<TEntity> 而不是 IReadOnlyCollection。我可以暴露一个 ICollection 本身,但我想避免有 Add / Remove 方法。

我可以“忽略” People 属性,但我仍然想能够针对它编写 Linq 查询。

我的第二个问题是我不想让我的上下文直接公开添加/删除人员的可能性。

在上下文中,我希望:

public IQueryable<Person> People { get; set; }

然而,即使IDbSet实现了IQueryable,EF也不会填充我的上下文中的People属性。我能想到的唯一解决方案是编写一个门面模式覆盖MyDbContext,它公开了我想要的功能。这似乎对于只读数据集来说过于繁琐且需要大量维护。

在使用Entity Framework的同时如何实现干净的DDD模型?

编辑
我正在使用Entity Framework v5


你需要将其标记为虚拟的,才能自动填充。 - user1496062
3个回答

15

正如您所注意到的,持久性基础设施 (EF) 对类结构提出了一些要求,因此使其不像您期望的那样"干净"。我担心与它纠缠会导致无尽的挣扎和头疼。

我建议采用另一种方法,即完全干净的域模型和一个单独的持久化模型在更低的层次上。您可能需要这两者之间的转换机制,AutoMapper 可以很好地完成这个工作。

这将完全解除您的顾虑。没有办法"削减",仅仅因为 EF 使事情变得必要,而且上下文不能从域层获得,因为它只是来自"另一个世界",它不属于域。

我见过人们制作局部模型 (也称为"有限上下文") 或者只创建一个普通的 EF POCO 结构,并假装这是 DDD,但它可能不是,您的顾虑正好击中了要害。


2
谢谢您的建议,最终我决定转换到NHibernate,因为它在类设计方面给了我更多的灵活性。 - Matthew
我同意你的观点。REPOSITORY模式是数据访问层和领域层之间的接口。EF更像一个数据映射器(Data Mapper)而不是一个REPOSITORY,因为EF模型往往只是简单的数据容器对象(这实际上加强了它更像是一个Data Mapper的概念)。最好实现一个领域层,使用仓储(repository)将对象从数据映射层翻译过来(由EF提供的数据映射对象是EF数据模型所代表的对象)。此外,这与DDD相一致。 - fourpastmidnight
1
小心AutoMapper。DDD的核心概念之一是我们的领域模型是自由变化的。使用AutoMapper,改变我们的领域模型可能会导致错误,这些错误直到相应的代码运行时才会出现。领域模型和数据模型之间的差异是一个相当严重的问题,如果发现得太晚就为时已晚了。 - Timo
2
@Timo:其实情况并不糟糕。如果你依赖于显式映射(CreateMap<T,U>)并且在组合根中早期创建映射,然后调用Mapper.AssertConfigurationIsValid,AutoMapper会在您启动应用程序时立即报告模型之间的任何不兼容性。使用这种技术,我们在几年内没有遇到过任何映射不兼容的问题(因为这些问题很快就被消除了;事实上,您甚至无法运行应用程序,因为AssertConfigurationIsValid会抛出异常)。但是,您的评论当然是正确和重要的。谢谢。 - Wiktor Zychla

4
Wiktor的建议值得认真考虑。我坚持使用CORE Data模型,并学会了如何应对EF的一些缺点。我已经花费了数小时来解决这些问题。现在,我已经接受了这些限制,并避免了额外的映射层。这是我的首要任务。
但是,如果你不认为映射层是一个问题,想要一个没有限制的DDD模型,那么Wiktor的建议就是正确的选择。
EF存在一些问题: - 仅支持类型的子集 - 属性公开get/set - 导航公开get/set - 不支持多态类型变化。 例如,基类中的Id对象,在子类型S1中为Int,在子类型S2中为Guid。 - 在1:1关系中构建键的限制等等。
我有一个全新的场景,并且只想维护一个层次结构,所以我坚持使用了有限制的DDD。即使有了这样的经验,我个人仍然会再次使用有限制的DDD。但我完全理解为什么有人会建议使用映射层和纯DDD模型。
祝你好运。

2

大多数问题来自于流畅的映射需要实体属性是公共的,因此您无法正确地封装持久性细节。

考虑使用基于XML的映射(.edmx文件)而不是流畅的映射。它允许您映射私有属性。

另一件需要注意的事情 - 您的应用程序不应直接使用DbContext。为其创建一个接口,仅公开那些您确定为聚合根的实体的DbSet。


1
您可以使用额外的代码来使用 Code First 映射私有属性,如此处所示:http://romiller.com/2012/10/01/mapping-to-private-properties-with-code-first/ - Yorro
谢谢你的技巧。我下次会试一试。 :) - Bartłomiej Szypelow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接