Este artigo foi traduzido por máquina.

Pontos de dados

Reduza os modelos EF com Contextos Limitados DDD

Julie Lerman

Baixar o código de exemplo

Ao definir modelos para uso com Entity Framework (EF), os desenvolvedores geralmente incluem todas as classes para ser usado em todo o aplicativo. Isso pode ser um resultado de criação de um novo modelo de banco de dados primeiro no Designer do EF e selecionar todas as tabelas disponíveis e exibições no banco de dados. Para aqueles de vocês usando código primeiro definir seu modelo, ele pode significar criar DbSet Propriedades em um único DbContext para todas as suas classes ou até mesmo inconscientemente, incluindo classes relacionadas àquelas que você já direcionou.

Quando você estiver trabalhando com um modelo grande e um aplicativo grande, existem inúmeros benefícios para projetar modelos menores, mais compactos que são direcionados para tarefas de aplicação específica, ao invés de ter um modelo único para toda a solução. Nesta coluna, vou apresentá-lo a um conceito de domain-driven design (DDD) — contexto limitado — e mostrar-lhe como aplicá-la para construir um modelo alvo com EF, centrando-se sobre como fazer isso com a maior flexibilidade do recurso EF Code First. Se você é novo para DDD, esta uma ótima abordagem para saber mesmo se você não está cometendo totalmente para DDD. E se você já estiver usando DDD, você vai se beneficiar por ver como você pode usar o EF enquanto as seguintes práticas DDD.

Design controlado por domínio e contexto limitado

DDD é um tópico bastante grande que abraça uma visão holística do design de software. Paul Rayner, professora de oficinas DDD para linguagem de domínio (DomainLanguage.com), coloca-lo de forma sucinta:

"DDD defende concepção pragmática, holística e contínua de software: colaborando com especialistas de domínio para incorporar modelos de domínio rico no software — modelos que ajudam a resolver problemas de negócios importantes e complexos. "

DDD inclui numerosas software design patterns, um dos quais — contexto limitado — se presta perfeitamente para trabalhar com EF. Contexto limitado concentra-se no desenvolvimento de pequenos modelos que se destinam a apoiar operações específicas no domínio do negócio. Em seu livro, "Domain-Driven Design" (Addison Wesley, 2003), Eric Evans explica que a contexto limitado "delimita a aplicabilidade de um modelo específico. Delimitadora contextos dá os membros da equipe uma compreensão clara e compartilhada do que tem de ser consistente, e o que pode desenvolver de forma independente."

Modelos menores proporcionam muitos benefícios, permitindo definir limites claros relativos às responsabilidades de concepção e desenvolvimento de equipes. Eles também levam a melhor manutenção — porque um contexto tem uma superfície menor, você tem menos efeitos colaterais que se preocupar ao fazer modificações. Além disso, há um benefício de desempenho quando EF cria metadados na memória para um modelo quando ele é primeiramente carregado na memória.

Porque eu estou construindo contextos vinculados com EF DbContext, já mencionei meu DbContexts como "delimitada DbContexts." No entanto, os dois não são realmente equivalentes: DbContext é uma implementação de classe, Considerando que o contexto limitado engloba o conceito maior dentro do processo de projeto completo. Vou me portanto referir meu DBContexts como "restrito" ou "focada".

Comparar um DbContext EF típico para limitado contexto

Enquanto o DDD é mais comumente aplicado a desenvolvimento de grandes aplicações em domínios de negócio complexo, apps menores também podem beneficiar de muitas das suas lições. Por causa desta explicação, vou-me concentrar em um aplicativo direcionado para um subdomínio específico: acompanhamento de vendas e marketing para uma empresa. Objetos envolvidos nesta aplicação podem variar de clientes, pedidos e itens de linha, de produtos, marketing, vendedores e até mesmo os funcionários. Normalmente um DbContext seria definida para conter DbSet Propriedades para cada classe na solução que precisa ser mantida no banco de dados, como mostrado em Figura 1.

Figura 1 DbContext típica, contendo todas as Classes de domínio da solução

 

public class CompanyContext : DbContext {   public DbSet<Customer> Customers { get; set; }   public DbSet<Employee>  Employees { get; set; }   public DbSet<SalaryHistory> SalaryHistories { get; set; }   public DbSet<Order> Orders { get; set; }   public DbSet<LineItem> LineItems { get; set; }   public DbSet<Product> Products { get; set; }   public DbSet<Shipment> Shipments { get; set; }   public DbSet<Shipper> Shippers { get; set; }   public DbSet<ShippingAddress> ShippingAddresses { get; set; }   public DbSet<Payment> Payments { get; set; }   public DbSet<Category> Categories { get; set; }   public DbSet<Promotion> Promotions { get; set; }   public DbSet<Return> Returns { get; set; }   protected override void OnModelCreating(DbModelBuilder modelBuilder)   {     // Config specifies a 1:0..1 relationship between Customer and ShippingAddress     modelBuilder.Configurations.Add(new ShippingAddressMap());   } }

Imagine se fosse uma aplicação muito mais vasto alcance com centenas de classes. E você também pode ter configurações de API fluente para algumas dessas classes. Que faz para uma enorme quantidade de código para percorrer e gerenciar uma única classe. Com uma grande aplicação, desenvolvimento pode ser dividido entre equipes. Com este único DbContext de toda a empresa, cada equipe precisa um subconjunto do código base que se estende para além de suas responsabilidades. E a este contexto, as alterações de qualquer equipe poderiam afetar outro trabalho equipe.

Existem questões interessantes que você poderia se perguntar sobre este DbContext abrangente, sem foco. Por exemplo, na área do aplicativo voltado para o departamento de marketing, os usuários têm qualquer necessidade de trabalhar com dados de histórico de salário do empregado? O departamento de transporte que precisa acessar o mesmo nível de detalhe sobre um cliente como um agente de serviço ao cliente? Alguém, no departamento de transporte seria necessário editar um registro de cliente? Para cenários mais comuns, a resposta a estas perguntas seria geralmente nenhum, e isso pode ajudá-lo a ver por que ele poderia fazer sentido ter vários DbContexts que gerenciar pequenos conjuntos de objetos de domínio.

Um DbContext focada para o departamento de transporte

Como DDD recomenda trabalhar com modelos menores, mais focada, com limites bem definidos de contexto, vamos restringir o escopo do presente DbContext para funções de departamento de transporte e apenas as classes necessárias para realizar as tarefas pertinentes. Portanto, você pode remover algumas propriedades DbSet de DbContext, deixando somente aqueles que você precisará de suporte a recursos de negócios relacionados ao transporte. Eu tenho tido retornos, promoções, categorias, pagamentos, funcionários e SalaryHistories:

public class ShippingDeptContext : DbContext {   public DbSet<Shipment> Shipments { get; set; }   public DbSet<Shipper> Shippers { get; set; }   public DbSet<Customer> Customers { get; set; }   public DbSet<ShippingAddress> ShippingAddresses { get; set; }   public DbSet<Order> Order { get; set; }   public DbSet<LineItem> LineItems { get; set; }   public DbSet<Product> Products { get; set; } }

EF Code First usa os detalhes do ShippingContext para inferir o modelo. Figura 2 mostra uma visualização do modelo que será criado a partir desta classe, que gerei usando as ferramentas de poder do Entity Framework beta 2. Agora, vamos começar a afinar o modelo.


Figura 2 visualizado modelo de primeira passagem no ShippingContext

Ajuste o DbContext e criação de Classes mais-alvo

Há ainda mais classes envolvidas no modelo que eu especificado para o transporte. Por convenção, o primeiro código inclui todas as classes que são acessíveis por outras classes no modelo. É por isso que categoria e pagamento mostraram-se mesmo que tirei suas propriedades DbSet. Então eu vou dizer o DbContext ignorar categoria e pagamento:

 

protected override void OnModelCreating(DbModelBuilder modelBuilder) {   modelBuilder.Ignore<Category>();   modelBuilder.Ignore<Payment>();   modelBuilder.Configurations.Add(new ShippingAddressMap()); }

Isso garante que categoria e pagamento não ficar puxado para o modelo só porque eles estão relacionados ao produto e a ordem.

É possível refinar essa classe DbContext ainda mais sem afetar o modelo resultante. Com essas propriedades DbSet, é possível a consulta explicitamente para cada um destes sete conjuntos de dados em seu aplicativo. Mas se você pensar sobre as classes e suas relações, você pode concluir que neste contexto nunca será necessário a consulta para o frete­abordar diretamente — ele sempre pode ser recuperado juntamente com os dados do cliente. Mesmo com nenhum ShippingAddresses DbSet, você pode contar com a mesma convenção puxado automaticamente na categoria e pagamento para puxar o ShippingAddress para o modelo por causa de seu relacionamento ao cliente. Então você pode remover a propriedade de ShippingAddresses sem perder os mapeamentos de dados para ShippingAddress. Você pode ser capaz de justificar a remoção de outros, mas vamos nos concentrar em apenas um presente:

public class ShippingContext : DbContext {   public DbSet<Shipment> Shipments { get; set; }   public DbSet<Shipper> Shippers { get; set; }   public DbSet<Customer> Customers { get; set; }   // Public DbSet<ShippingAddress> ShippingAddresses { get; set; }   public DbSet<Order> Order { get; set; }   public DbSet<LineItem> LineItems { get; set; }   public DbSet<Product> Products { get; set; }   protected override void OnModelCreating(DbModelBuilder modelBuilder)   { ...
} }

Dentro do contexto de processamento de envios, eu realmente não preciso um objeto de cliente completo, um objeto de ordem completo ou um objeto LineItem completo. Eu preciso apenas o produto a ser enviado, a quantidade (de LineItem), o nome do cliente e ShippingAddress e quaisquer notas que podem ser anexadas ao cliente ou à ordem. Eu vou ter meu DBA criar uma exibição que irá retornar itens unshipped — aqueles com ShipmentId = 0 ou null. Entretanto, eu pode definir uma classe simplificada que mapeará para esse ponto de vista com as propriedades relevantes que eu antecipo que necessitam:

[Table("ItemsToBeShipped")] public class ItemToBeShipped {   [Key]   public int LineItemId { get; set; }   public int OrderId { get; set; }   public int ProductId { get; set; }   public int OrderQty { get; private set; }   public OrderShippingDetail OrderShippingDetails { get; set; } }

A lógica de processamento de envios necessita consultar o ItemToBeShipped e depois ficar qualquer detalhes do pedido que eu poderia precisar junto com o cliente e ShippingAddress. Eu poderia reduzir a minha definição de DbContext para permitir-me consultar um gráfico começando com este novo tipo e inclusive a ordem, o cliente e o frete­endereço. No entanto, porque sei que a EF seria conseguir isso com uma consulta SQL projetado para mesclar os resultados e ordem repetida, o cliente e o frete de retorno­dados de endereço junto com cada item de linha, vou deixar que o programador para um pedido de consulta e trazer de volta um gráfico com o cliente e ShippingAddress. Mas, novamente, eu não preciso de todas as colunas da tabela de ordem, então vou criar uma classe que é melhor direcionada para o departamento de transporte, incluindo informações que poderiam ser impressas em um manifesto de transporte. A classe é a classe de OrderShippingDetail mostrada na Figura 3.

Figura 3 da classe de OrderShippingDetail

[Table("Orders")] public class OrderShippingDetail {     [Key]   public int OrderId { get; set; }   public DateTime OrderDate { get; set; }   public Nullable<DateTime> DueDate { get; set; }   public string SalesOrderNumber { get; set; }   public string PurchaseOrderNumber { get; set; }   public Customer Customer { get; set; }   public int CustomerId { get; set; }   public string Comment { get; set; }   public ICollection<ItemToBeShipped> OpenLineItems { get; set; } }

Observe que a minha classe ItemToBeShipped tem uma propriedade de navegação para OrderShippingDetail e OrderShippingDetail tem um para o cliente. As propriedades de navegação vão me ajudar com gráficos quando consultando e salvar.

Há uma peça mais esse quebra-cabeça. O departamento de transporte será necessário indicar itens como enviado e a tabela de LineItems tem uma coluna de ShipmentId que é usada para vincular um item de linha para uma expedição. O app será necessário atualizar o campo ShipmentId quando um item é enviado. Vou criar uma classe simples para cuidar dessa tarefa, em vez de depender de LineItem classe que é usada para vendas:

[Table("LineItems")] public class LineItemShipment {   [Key]   public int LineItemId { get; set; }   public int ShipmentId { get; set; } }

Sempre que um item foi enviado, você pode criar uma nova instância dessa classe com os valores adequados e forçar o aplicativo para atualizar o LineItem no banco de dados. É importante projetar seu aplicativo para usar a classe somente para esta finalidade. Se você tentar inserir um LineItem usando essa classe em vez de um que contas para campos de tabela não anulável como OrderId, banco de dados lançará uma exceção.

Após alguns mais Fine-Tuning, meu ShippingContext agora é definido como:

public class ShippingContext : DbContext {   public DbSet<Shipment> Shipments { get; set; }   public DbSet<Shipper> Shippers { get; set; }   public DbSet<OrderShippingDetail> Order { get; set; }   public DbSet<ItemToBeShipped> ItemsToBeShipped { get; set; }   protected override void OnModelCreating(DbModelBuilder modelBuilder)   {     modelBuilder.Ignore<LineItem>();     modelBuilder.Ignore<Order>();     modelBuilder.Configurations.Add(new ShippingAddressMap());   } }

Usando o beta de ferramentas de poder do Entity Framework 2 novamente para criar um EDMX, vejo-me na janela do navegador do modelo (Figura 4) que primeiro código infere que o modelo contém quatro classes especificado pelo DbSets, como cliente e ShippingAddress, que foram descobertos por meio de propriedades de navegação de classe OrderShippingDetail.


Figura 4 vista navegador modelo ShippingContext entidades como inferido pelo código primeiro

DbContext focada e inicialização de banco de dados

Ao usar um DbContext menor que ofereça suporte a contextos específicos de limitado no seu aplicativo, é essencial para manter em mente dois EF Code First padrão comportamentos com relação a inicialização do banco de dados.

O primeiro é que o primeiro código vai olhar para um banco de dados com o nome do contexto. Que não é desejável, quando seu aplicativo tem um ShippingContext, um CustomerContext, um SalesContext e outros. Em vez disso, você quer todos os DbContexts para apontar para o mesmo banco de dados.

O segundo comportamento padrão a considerar é que primeiro código usará o modelo inferido por um DbContext para definir o esquema de banco de dados. Mas agora você tem um DbContext que representa apenas uma fatia do banco de dados. Por esse motivo, você não quer as classes DbContext para acionar a inicialização do banco de dados.

É possível resolver esses dois problemas no construtor da classe de contexto de cada. Por exemplo, aqui na classe ShippingContext você pode ter o construtor Especifica o DPSalesDatabase e desabilite a inicialização de banco de dados:

 

public ShippingContext() : base("DPSalesDatabase") {   Database.SetInitializer<ShippingContext>(null); }

No entanto, se você tem um monte de DbContext classes em seu aplicativo, isso vai se tornar um problema de manutenção. Um melhor padrão é especificar uma classe base que desativa a inicialização do banco de dados e define o banco de dados ao mesmo tempo:

public class BaseContext<TContext>   DbContext where TContext : DbContext {   static BaseContext()   {     Database.SetInitializer<TContext>(null);   }   protected BaseContext() : base("DPSalesDatabase")   {} }

Agora meus várias classes de contexto podem implementar o BaseContext em vez de cada um ter seu próprio construtor:

public class ShippingContext:BaseContext<ShippingContext>

Se você está fazendo o desenvolvimento de novos e você quer deixar o código primeiro criar ou migra seu banco de dados com base em suas classes, você precisará criar um "uber"modelo usando um DbContext que inclui todas as classes e os relacionamentos necessários para construir um modelo completo que representa o banco de dados. No entanto, neste contexto não deve herdar de BaseContext. Quando você fizer alterações em suas estruturas de classe, você pode executar algum código que usa o uber-contexto para executar a inicialização do banco de dados, se você estiver criando ou migrar o banco de dados.

Exercitando o DbContext focalizado

Com tudo isso no lugar, eu criei alguns testes de integração automatizada para executar as seguintes tarefas:

  • Recupere itens de linha abertas.
  • Recupere OrderShippingDetails junto com o cliente e o envio de dados para pedidos com itens de linha unshipped.
  • Recuperar um item de linha unshipped e criar uma nova remessa. Definir o envio para o item de linha e inserir a nova remessa para o banco de dados ao atualizar o item de linha no banco de dados com o valor da chave na nova remessa.

Estas são as funções mais críticas, que você precisa executar para transporte de produtos para os clientes que pedi-los. Os testes todos passarem, verificando que meu DbContext funciona conforme o esperado. Os testes estão incluídos no download deste artigo a amostra.

Conclusão

Não apenas criei um DbContext que enfoca especificamente a tarefas de transporte, mas também pensá-lo ajudou-me a criar objetos de domínio mais eficiente para ser usado com essas tarefas. Usar DbContext alinhar meu contexto limitado com meu subdomínio envio desta forma significa não tenho de percorrer em um atoleiro de código para funcionar o recurso de departamento de transporte da aplicação, e pode fazer o que eu preciso para os objetos de domínio especializado sem afetar outras áreas do esforço de desenvolvimento.

Você pode ver outros exemplos de focagem DbContext usando o conceito DDD de contexto limitado no livro, "programação Entity Framework: DbContext"(o ' Reilly Media, 2011), que eu em co-autoria com Rowan Miller.

Julie Lerman é uma Microsoft MVP, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e ela é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga-a no Twitter, em twitter.com/julielerman.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Paul Rayner