Agregar raiz com o Entity Framework usando o Design Dirigido por Domínio
Eu estou construindo um aplicativo usando o Domain Driven Design que está usando o Entity Framework.
Meu objetivo é permitir que meus modelos de domínio (que persistem com EF) contenham alguma lógica dentro deles.
Fora da caixa, o framework de entidades é bastante não restritivo quanto ao modo como as entidades são adicionadas ao gráfico e depois persistidas.
Tomemos por exemplo, meu domínio como POCO (sem lógica):
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; }
}
Meu domínio de exemplo é que uma organização pode ter muitas pessoas. Uma pessoa só pode pertencer a uma organização.
Isso é muito simples para criar uma organização e adicionar pessoas a ela:
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();
}
Minha consulta de teste é.
var organizationsWithSteve = context.Organizations.Where(o => o.People.Any(p => p.FirstName == "Steve"));
O layout de classes acima não está de acordo com o funcionamento do domínio. Por exemplo, todas as pessoas pertencem a uma organização com organização sendo a raiz agregada. Não faz sentido ser capaz de fazercontext.People.Add(...)
como não é assim que o domínio funciona.
Se quiséssemos adicionar alguma lógica aoOrganization
modelo para restringir quantas pessoas podem estar nessa organização, poderíamos implementar um método.
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;
}
No entanto, com o layout atual das classes, posso contornarAddPerson
lógica por qualquer chamadaorganization.Persons.Add(...)
ou ignorar completamente a raiz agregada, fazendocontext.Persons.Add(...)
, nenhum dos quais eu quero fazer.
Minha solução proposta (que não funciona e é por isso que estou postando aqui) é:
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(...);
}
}
Isso não funciona como o código de mapeamentoHasMany(o => o.People).WithRequired(p => p.Organization);
não compila comoHasMany
espera umICollection<TEntity>
e nãoIReadOnlyCollection
. Posso expor umICollection
em si, mas eu quero evitar terAdd
/ Remove
métodos.
Eu posso "ignorar" oPeople
propriedade, mas eu ainda quero ser capaz de escrever consultas Linq contra ele.
Meu segundo problema é que não quero que meu contexto exponha a possibilidade de adicionar / remover pessoas diretamente.
No contexto, eu gostaria de:
public IQueryable<Person> People { get; set; }
No entanto, a EF não preencherá oPeople
propriedade do meu contexto, emboraIDbSet
implementaIQueryable
. A única solução que posso fazer é escrever uma fachada sobreMyDbContext
que expõe a funcionalidade que eu quero. Parece um exagero e muita manutenção para um conjunto de dados somente leitura.
Como faço para obter um modelo DDD limpo ao usar o Entity Framework?
EDITAR
Estou usando o Entity-Framework v5