Aggregate root with Entity Framework using Domain Driven Design
Asked Answered
E

3

12

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

Ellinger answered 24/7, 2013 at 19:35 Comment(1)
You need to mark it as virtual for it to auto populate.Lebeau
F
17

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.

Freda answered 24/7, 2013 at 20:17 Comment(4)
Thank you for the advice, I ended up switching to NHibernate as it gives me a lot more flexibility in the design of my classes.Ellinger
I agree with you. The REPOSITORY pattern is an interface between the data access layer and the domain layer. EF is more like a Data Mapper than it is a REPOSITORY, since EF models tend to be nothing more than simple data container objects (which really reinforces the notion that it's more of a Data Mapper). You would be best to implement a domain layer that uses a repository for translating objects from the data-mapped layer (the data-mapped objects being provided by EF and which are the objects represented by the EF data model). Furthermore, this would be in keeping with DDD.Jamin
Be careful with AutoMapper. One of the core notions of DDD is that our domain model is free to change. With AutoMapper, changing our domain model could lead to errors that will not appear until the corresponding code is run. A discrepancy between the domain model and the data model is quite a severe issue to discover so late.Bloodstock
@Timo: it's not that bad. If you rely on explicit mappings (CreateMap<T,U>) and create your mappings early (in the Composition Root) and call the Mapper.AssertConfigurationIsValid, AutoMapper reports any incompatibilities between models as soon as you start your app. With this technique we had zero problems with mapping incompatibilities for few years (as these are eliminated quickly; in fact, you can't even run the app as the AssertConfigurationIsValid just throws an exception). However, your remark is of course correct and important. Thanks.Freda
L
4

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

Larkspur answered 25/7, 2013 at 1:44 Comment(0)
R
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.

Refinery answered 25/7, 2013 at 11:16 Comment(2)
You can map private properties with Code First with an extra code, as shown here: romiller.com/2012/10/01/…Acrodont
Thanks for the hack. I'll give it a try next time. :)Nonfulfillment

© 2022 - 2024 — McMap. All rights reserved.