Aggregate root with Entity Framework using Domain Driven Design

aggregateroot c# domain-driven-design entity-framework

Question

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
8
7/24/2013 7:45:19 PM

Accepted Answer

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.

14
7/24/2013 9:17:37 PM

Popular Answer

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.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow