Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aggregate root with Entity Framework using Domain Driven Design

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

like image 275
Matthew Avatar asked Jul 24 '13 19:07

Matthew


People also ask

What is aggregate root in domain driven design?

Aggregate Root is the mothership entity inside the aggregate (in our case Computer ), it is a common practice to have your repository only work with the entities that are Aggregate Roots, and this entity is responsible for initializing the other entities. Consider Aggregate Root as an Entry-Point to an Aggregate.

Is aggregate a root entity?

An aggregate is a collection of one or more related entities (and possibly value objects). Each aggregate has a single root entity, referred to as the aggregate root. The aggregate root is responsible for controlling access to all of the members of its aggregate.

Which EF approach we can use for domain driven?

The DDD approach to writing entity classes in EF Core makes every property read-only. The only way to create or update entity data is constructors (ctors), factories or methods in the entity class.

What is aggregate root CQRS?

Concretely, an aggregate will handle commands, apply events, and have a state model encapsulated within it that allows it to implement the required command validation, thus upholding the invariants (business rules) of the aggregate.


2 Answers

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.

like image 152
Wiktor Zychla Avatar answered Sep 29 '22 10:09

Wiktor Zychla


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

like image 30
phil soady Avatar answered Sep 29 '22 11:09

phil soady