NOTA: Esta resposta fala sobre o Entity Framework DbContext
, mas é aplicável a qualquer tipo de implementação de Unidade de Trabalho, como LINQ to SQL DataContext
e NHibernate ISession
.
Vamos começar ecoando para Ian: ter um single DbContext
para todo o aplicativo é uma má ideia. A única situação em que isso faz sentido é quando você tem um aplicativo de thread único e um banco de dados que é usado exclusivamente por essa instância de aplicativo único. O DbContext
não é seguro para threads e, como os DbContext
dados são armazenados em cache, ficam obsoletos em breve. Isso causará todo tipo de problema quando vários usuários / aplicativos trabalharem nesse banco de dados simultaneamente (o que é muito comum, é claro). Mas espero que você já saiba disso e só queira saber por que não injetar uma nova instância (ou seja, com um estilo de vida transitório) da DbContext
pessoa que precisar. (para obter mais informações sobre por que um único DbContext
- ou mesmo no contexto por segmento - é ruim, leia esta resposta ).
Deixe-me começar dizendo que registrar um DbContext
como transitório pode funcionar, mas geralmente você deseja ter uma única instância dessa unidade de trabalho dentro de um determinado escopo. Em um aplicativo da web, pode ser prático definir esse escopo nos limites de uma solicitação da web; portanto, um estilo de vida Por solicitação da Web. Isso permite que você permita que todo um conjunto de objetos opere no mesmo contexto. Em outras palavras, eles operam dentro da mesma transação comercial.
Se você não tem o objetivo de ter um conjunto de operações operando dentro do mesmo contexto, nesse caso, o estilo de vida transitório é bom, mas há algumas coisas a serem observadas:
- Como todo objeto obtém sua própria instância, toda classe que altera o estado do sistema precisa chamar
_context.SaveChanges()
(caso contrário, as alterações seriam perdidas). Isso pode complicar o seu código e adicionar uma segunda responsabilidade ao código (a responsabilidade de controlar o contexto) e é uma violação do Princípio da Responsabilidade Única .
- Você precisa garantir que as entidades [carregadas e salvas por a
DbContext
] nunca deixem o escopo de uma classe, porque elas não podem ser usadas na instância de contexto de outra classe. Isso pode complicar bastante o seu código, porque quando você precisa dessas entidades, precisa carregá-las novamente por id, o que também pode causar problemas de desempenho.
- Como
DbContext
implementa IDisposable
, você provavelmente ainda deseja Dispor todas as instâncias criadas. Se você quiser fazer isso, basicamente tem duas opções. Você precisa descartá-los no mesmo método logo após a chamada context.SaveChanges()
, mas, nesse caso, a lógica comercial assume a propriedade de um objeto que é passado externamente. A segunda opção é Dispor todas as instâncias criadas no limite da solicitação de HTTP, mas nesse caso você ainda precisa de algum tipo de escopo para informar ao contêiner quando essas instâncias precisam ser descartadas.
Outra opção é não injetar um DbContext
. Em vez disso, você injeta um DbContextFactory
que é capaz de criar uma nova instância (eu costumava usar essa abordagem no passado). Dessa forma, a lógica de negócios controla o contexto explicitamente. Se for assim:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
O lado positivo disso é que você gerencia a vida do DbContext
explicitamente e é fácil configurá-lo. Ele também permite que você use um contexto único em um determinado escopo, o que possui vantagens claras, como executar código em uma única transação comercial e ser capaz de repassar entidades, pois elas se originam da mesma DbContext
.
A desvantagem é que você terá que passar pelo DbContext
método from to method (que é chamado de Injeção de método). Observe que, em certo sentido, essa solução é igual à abordagem 'escopo', mas agora o escopo é controlado no próprio código do aplicativo (e possivelmente é repetido várias vezes). É o aplicativo responsável pela criação e descarte da unidade de trabalho. Como o DbContext
é criado após a construção do gráfico de dependência, a Injeção de construtor fica fora de cena e você precisa adiar para a Injeção de método quando precisar passar o contexto de uma classe para outra.
A injeção de método não é tão ruim, mas quando a lógica de negócios se torna mais complexa e mais classes são envolvidas, você terá que passar de método para método e de classe para classe, o que pode complicar bastante o código (eu já vi isso no passado). Para uma aplicação simples, essa abordagem funciona bem.
Devido às desvantagens, essa abordagem de fábrica tem para sistemas maiores, outra abordagem pode ser útil e é a que permite que o contêiner ou o código de infraestrutura / Raiz da Composição gerenciem a unidade de trabalho. Esse é o estilo de sua pergunta.
Ao permitir que o contêiner e / ou a infraestrutura lidem com isso, o código do aplicativo não é poluído, pois é necessário criar (opcionalmente) confirmar e Dispose uma instância UoW, o que mantém a lógica de negócios simples e limpa (apenas uma responsabilidade única). Existem algumas dificuldades com essa abordagem. Por exemplo, você confirmou e descartou a instância?
A eliminação de uma unidade de trabalho pode ser feita no final da solicitação da web. Muitas pessoas, no entanto, assumem incorretamente que este também é o local para comprometer a unidade de trabalho. No entanto, nesse ponto do aplicativo, você simplesmente não pode determinar com certeza que a unidade de trabalho deve realmente ser confirmada. Por exemplo, se o código da camada de negócios gerou uma exceção que foi capturada mais acima na pilha de chamadas, você definitivamente não deseja Confirmar.
A solução real é novamente gerenciar explicitamente algum tipo de escopo, mas desta vez faça-o dentro da Raiz da Composição. Abstraindo toda a lógica de negócios por trás do padrão de comando / manipulador , você poderá escrever um decorador que possa ser agrupado em torno de cada manipulador de comando que permita fazer isso. Exemplo:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
Isso garante que você só precise escrever esse código de infraestrutura uma vez. Qualquer contêiner DI sólido permite que você configure esse decorador para envolver todas as ICommandHandler<T>
implementações de maneira consistente.