Aggregate root with Entity Framework using Domain Driven Design

倖福魔咒の 提交于 2019-12-04 21:48:07

问题


I am building an application using Domain Driven Design that is using Entity Framework.

My goal is to allow my domain models (that get persisted with EF) contain some logic within them.

Out of the box, entity-framework is pretty nonrestrictive as to how entities get added to the graph and then persisted.

Take for example, my domain as POCO (without logic):

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; } 
}

My Example domain is that an Organization can have many people. A person can only belong to one Organization.

This is very simple to create an organization and add people to it:

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();
}

My test query is.

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

The above layout of classes doesn't conform to how the domain works. For example, all people belong to an Organization with Organization being the aggregate root. It doesn't make sense to be able to do context.People.Add(...) as that's not how the domain works.

If we wanted to add some logic to the Organization model to restrict how many people can be in that organization, we could implement a method.

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;
}

However, with the current layout of classes I can circumvent the AddPerson logic by either calling organization.Persons.Add(...) or completely ignore the aggregate root by doing context.Persons.Add(...), neither of which I want to do.

My proposed solution (which doesn't work and is why I'm posting it here) is:

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(...);
    }
}

This does not work as the mapping code HasMany(o => o.People).WithRequired(p => p.Organization); does not compile as HasMany expects an ICollection<TEntity> and not IReadOnlyCollection. I can expose an ICollection itself, but I want to avoid having Add / Remove methods.

I can "Ignore" the People property, but I still want to be able to write Linq queries against it.

My second problem is that I do not want my context to expose the possibility to Add / Remove people directly.

In the context I would want:

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

However, EF will not populate the People property of my context, even though IDbSet implements IQueryable. The only solution I can come up with to this to write a facade over MyDbContext which exposes the functionality I want. Seems overkill and a lot of maintenance for a read-only dataset.

How do I achieve a clean DDD model while using Entity Framework?

EDIT
I'm using Entity-Framework v5


回答1:


As you noticed, the persistence infrastructure (the EF) imposes some requirements on the class structure thus making it not "as clean" as you'd expect. I am afraid that struggling with it would end up with endless struggle and brain bumps.

I'd suggest another approach, a completely clean domain model and a separate persistence model in a lower layer. You probably would need a translation mechanism between these two, the AutoMapper would do fine.

This would free you from your concerns completely. There are no ways to "take a cut" just because the EF makes things necessary and the context is not available from the domain layer as it is just from "another world", it doesn't belong to the domain.

I've seen people making partial models (aka "bounded contexts") or just creating an ordinary EF poco structure and pretending this IS DDD but it probably isn't and your concerns hit the nail precisely in the head.




回答2:


Most of your problems come from the fluent mapping requiring entitiy properties to be public so you can't properly encapsulate persistence details.

Consider using XML-based mapping (.edmx files) instead of fluent mapping. It allows you to map private properties.

Another thing to note - your application shouldn't use DbContext directly. Create an interface for it that exposes only DbSets of those entities that you identified as aggregate roots.




回答3:


Wiktor advice is certainly worth long consideration. I have persisted with a CORE Data model and learnt to live with some of EF weaknesses. I have spent hours trying to get around them. I now live with the restrictions and have avoided the extra mapping layer. Which was my priority.

However, if you dont see a mapping layer as an issue, use would like a DDD model with NO restrictions. then Wiktors suggestion is the way.

Some issues with EF:

  • Only Supports a subset of types,
  • properties public get/set
  • navigation public get/set
  • no polymorphic type variation support.
    • eg Id Object in base and Int in substype S1 and Guid in Subtype S2.
  • restrictions on how keys are built in 1:1 relationships ...And thats just off the top of my head quickly.

I had a green field scenario and wanted only 1 layer to maintain, so i persisted. I would personally use a DDD with restrictions again, even after the experience. But completely understand why someone might suggest a mapping layer and pure DDD model.

good luck



来源:https://stackoverflow.com/questions/17843310/aggregate-root-with-entity-framework-using-domain-driven-design

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!