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