Um DbContext por solicitação da Web ... por quê?

398

Eu tenho lido muitos artigos explicando como configurar o Entity Framework DbContextpara que apenas um seja criado e usado por solicitação da Web HTTP usando várias estruturas de DI.

Por que essa é uma boa idéia em primeiro lugar? Que vantagens você ganha ao usar essa abordagem? Existem certas situações em que isso seria uma boa idéia? Existem coisas que você pode fazer usando essa técnica que não pode instanciar ao instanciar DbContexts por chamada de método de repositório?

Andrew
fonte
9
Gueddari em mehdi.me/ambient-dbcontext-in-ef6 chama a instância DbContext por método de repositório, chamando um antipadrão. Citação: "Ao fazer isso, você está perdendo praticamente todos os recursos que o Entity Framework fornece via DbContext, incluindo seu cache de primeiro nível, seu mapa de identidade, sua unidade de trabalho e suas habilidades de rastreamento e carregamento lento . " Excelente artigo com ótimas sugestões para lidar com o ciclo de vida do DBContexts. Definitivamente vale a pena ler.
Christoph

Respostas:

565

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 DataContexte NHibernate ISession.

Vamos começar ecoando para Ian: ter um single DbContextpara 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 DbContextnão é seguro para threads e, como os DbContextdados 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 DbContextpessoa 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 DbContextcomo 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 DbContextimplementa 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 DbContextFactoryque é 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 DbContextexplicitamente 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 DbContextmé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.

Steven
fonte
2
Uau - obrigado pela resposta completa. Se eu pudesse votar duas vezes, eu faria. Acima, você diz "... nenhuma intenção de deixar todo um conjunto de operações operar dentro do mesmo contexto; nesse caso, o estilo de vida transitório é bom ...". O que você quer dizer com "transitório", especificamente?
18730 Andrew
14
@ Andrew: 'Transiente' é um conceito de Injeção de Dependência, o que significa que, se um serviço estiver configurado para ser transitório, uma nova instância do serviço será criada sempre que for injetada no consumidor.
1616 Steven
11
@ user981375: Para operações CRUD, você pode criar um genérico CreateCommand<TEnity>e um genérico CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(e fazer o mesmo para Atualizar e Excluir e teve uma única GetByIdQuery<TEntity>consulta). Ainda assim, você deve se perguntar se esse modelo é uma abstração útil para operações CRUD ou se apenas adiciona complexidade. Ainda assim, você pode se beneficiar da possibilidade de adicionar facilmente preocupações transversais (por meio de decoradores) usando esse modelo. Você terá que pesar os prós e contras.
Steven
3
+1 Você acreditaria que escrevi toda essa resposta antes de realmente ler isso? BTW IMO Eu acho que é importante para você discutir a Eliminação da DbContext no final (embora a sua grande que você é agnóstico recipiente de permanência)
Ruben Bartelink
11
Mas você não passa o contexto para a classe decorada, como a classe decorada poderia trabalhar com o mesmo contexto que passou para a TransactionCommandHandlerDecorator? por exemplo, se a classe decorada for InsertCommandHandlerclass, como ela poderá registrar a operação de inserção no contexto (DbContext no EF)?
Masoud
35

Existem duas recomendações contraditórias da microsoft e muitas pessoas usam o DbContexts de uma maneira completamente divergente.

  1. Uma recomendação é "Descarte o DbContexts assim que possível", pois ter um DbContext Alive ocupa recursos valiosos, como conexões db, etc ...
  2. O outro afirma que Um DbContext por solicitação é altamente recomendado

Isso se contradiz porque, se a sua solicitação não estiver relacionada com o material do banco de dados, o seu DbContext será mantido sem motivo. Portanto, é um desperdício manter seu DbContext ativo enquanto sua solicitação está apenas aguardando a conclusão de coisas aleatórias ...

Muitas pessoas que seguem a regra 1 têm seus DbContexts dentro de seu "Padrão de Repositório" e criam uma nova Instância por Consulta ao Banco de Dados, de modo que X * DbContext por Solicitação

Eles apenas obtêm seus dados e descartam o contexto o mais rápido possível. Isso é considerado por MUITAS pessoas uma prática aceitável. Embora isso tenha os benefícios de ocupar seus recursos de banco de dados pelo tempo mínimo, ele sacrifica claramente todo o doce UnitOfWork e Caching Candy que a EF tem a oferecer.

Manter viva uma única instância multiuso do DbContext maximiza os benefícios do Caching, mas como o DbContext não é seguro para threads e cada solicitação da Web é executada em seu próprio thread, um DbContext por solicitação é o maior tempo possível.

Portanto, a recomendação da equipe da EF sobre o uso de 1 Db Context por solicitação é claramente baseada no fato de que em um aplicativo Web o UnitOfWork provavelmente estará dentro de uma solicitação e essa solicitação possui um encadeamento. Portanto, um DbContext por solicitação é como o benefício ideal do UnitOfWork e Caching.

Mas em muitos casos isso não é verdade. Considero que o log de um UnitOfWork separado, portanto, ter um novo DbContext para log de pós-solicitação em threads assíncronos é completamente aceitável

Então, finalmente, verifica-se que a vida útil de um DbContext está restrita a esses dois parâmetros. UnitOfWork e Thread

Anestis Kivranoglou
fonte
3
Com toda a justiça, suas solicitações HTTP devem ser concluídas rapidamente (alguns ms). Se eles estiverem demorando mais do que isso, convém fazer um processamento em segundo plano com algo como um agendador de tarefas externo, para que a solicitação possa retornar imediatamente. Dito isto, sua arquitetura também não deve contar com HTTP. No geral, uma boa resposta embora.
esmagar
34

Nenhuma resposta aqui realmente responde à pergunta. O OP não perguntou sobre um design DbContext de singleton / por aplicativo, ele perguntou sobre um design de solicitação por Web () e quais benefícios potenciais poderiam existir.

Vou fazer referência a http://mehdi.me/ambient-dbcontext-in-ef6/, pois o Mehdi é um recurso fantástico:

Possíveis ganhos de desempenho.

Cada instância DbContext mantém um cache de primeiro nível de todas as entidades carregadas no banco de dados. Sempre que você consulta uma entidade por sua chave primária, o DbContext tenta primeiro recuperá-la do cache de primeiro nível antes de padronizar a consulta no banco de dados. Dependendo do seu padrão de consulta de dados, a reutilização do mesmo DbContext em várias transações comerciais sequenciais pode resultar em menos consultas ao banco de dados, graças ao cache de primeiro nível do DbContext.

Permite carregamento lento.

Se seus serviços retornarem entidades persistentes (em vez de retornar modelos de exibição ou outros tipos de DTOs) e você desejar tirar vantagem do carregamento lento nessas entidades, o tempo de vida da instância DbContext da qual essas entidades foram recuperadas deve se estender além o escopo da transação comercial. Se o método de serviço descartou a instância DbContext usada antes de retornar, qualquer tentativa de carregar preguiçosamente as propriedades nas entidades retornadas falharia (se usar o carregamento lento é uma boa ideia, é um debate diferente no qual não entraremos em detalhes aqui). Em nosso exemplo de aplicativo da web, o carregamento lento normalmente seria usado nos métodos de ação do controlador em entidades retornadas por uma camada de serviço separada. Nesse caso,

Lembre-se de que há contras também. Esse link contém muitos outros recursos para ler sobre o assunto.

Basta postar isso no caso de alguém tropeçar nessa pergunta e não ser absorvido pelas respostas que não abordam a questão.

user4893106
fonte
Bom link! O gerenciamento explícito do DBContext parece a abordagem mais segura.
aggsol
22

Tenho certeza de que é porque o DbContext não é totalmente seguro para threads. Portanto, compartilhar a coisa nunca é uma boa ideia.

Ian
fonte
Você quer dizer que compartilhá-lo entre solicitações HTTP nunca é uma boa ideia?
22412 Andrew
2
Sim, Andrew é isso que ele quis dizer. Compartilhar o contexto é apenas para aplicativos de desktop de thread único.
Elisabeth
10
Que tal compartilhar o contexto para uma solicitação. Portanto, para uma solicitação, podemos ter acesso a repositórios diferentes e fazer uma transação através deles compartilhando o mesmo contexto?
Lyubomir Velchev
16

Uma coisa que não é realmente abordada na pergunta ou na discussão é o fato de o DbContext não poder cancelar as alterações. Você pode enviar alterações, mas não pode limpar a árvore de alterações. Portanto, se você usar um contexto por solicitação, não terá sorte se precisar jogar fora as alterações por qualquer motivo.

Pessoalmente, crio instâncias do DbContext quando necessário - geralmente anexadas a componentes de negócios que têm a capacidade de recriar o contexto, se necessário. Dessa forma, eu tenho controle sobre o processo, em vez de ter uma única instância forçada para mim. Também não preciso criar o DbContext em cada inicialização do controlador, independentemente de ele realmente ser usado. Então, se ainda quero ter instâncias por solicitação, posso criá-las no CTOR (via DI ou manualmente) ou criá-las conforme necessário em cada método do controlador. Pessoalmente, costumo usar a última abordagem para evitar a criação de instâncias DbContext quando elas não são realmente necessárias.

Depende de qual ângulo você olha para ele também. Para mim, a instância por solicitação nunca fez sentido. O DbContext realmente pertence à solicitação de HTTP? Em termos de comportamento, esse é o lugar errado. Seus componentes de negócios devem criar seu contexto, não a solicitação de HTTP. Em seguida, você pode criar ou jogar fora seus componentes de negócios conforme necessário e nunca se preocupar com a vida útil do contexto.

Rick Strahl
fonte
11
Esta é uma resposta interessante e concordo parcialmente com você. Para mim, um DbContext não precisa estar vinculado a uma solicitação da Web, mas sempre é digitado em uma única 'solicitação', como em: 'transação comercial'. E quando você vincula o contexto a uma transação comercial, o cancelamento de alterações se torna realmente estranho. Mas não tê-lo no limite de solicitações da web não significa que os componentes de negócios (BCs) devam estar criando o contexto; Eu acho que não é responsabilidade deles. Em vez disso, você pode aplicar o escopo usando decoradores nos seus BCs. Dessa maneira, você pode alterar o escopo sem nenhuma alteração no código.
Steven
11
Bem, nesse caso, a injeção no objeto de negócios deve lidar com o gerenciamento da vida útil. Na minha opinião, o objeto de negócios possui o contexto e, como tal, deve controlar a vida útil.
21816 Rick Strahl # 01:
Em resumo, o que você quer dizer quando diz "a capacidade de recriar o contexto, se necessário"? você está rolando sua própria capacidade de reversão? você pode elaborar um pouco?
tntwyckoff
2
Pessoalmente, acho um pouco problemático forçar um DbContext no início por lá. Não há garantia de que você precise acessar o banco de dados. Talvez você esteja ligando para um serviço de terceiros que muda de estado nesse lado. Ou talvez você tenha 2 ou 3 bancos de dados com os quais está trabalhando ao mesmo tempo. Você não criaria um monte de DbContexts no início, caso acabe usando-os. A empresa conhece os dados com os quais está trabalhando, portanto pertence a isso. Basta colocar um TransactionScope no início, se necessário. Não acho que todas as ligações precisem de uma. É preciso recursos.
Daniel Lorenz
Essa é a questão de permitir que o contêiner controle a vida útil do dbcontext, que controla a vida útil dos controles pai, às vezes indevidamente. Digamos que se eu quiser um singleton de serviço simples injetado nos meus controladores, não poderei usar a injeção de constuctor devido à semântica por solicitação.
Davidcarr 10/10/19
10

Eu concordo com opiniões anteriores. É bom dizer que, se você deseja compartilhar o DbContext no aplicativo de thread único, precisará de mais memória. Por exemplo, meu aplicativo Web no Azure (uma instância extra pequena) precisa de mais 150 MB de memória e eu tenho cerca de 30 usuários por hora. DBContext de compartilhamento de aplicativo na solicitação HTTP

Aqui está um exemplo real da imagem: o aplicativo foi implantado às 12h

Miroslav Holec
fonte
Possivelmente, a idéia é compartilhar o contexto de uma solicitação. Se acessarmos repositórios diferentes e classes - DBSet e desejarmos que as operações com eles sejam transacionais, essa deve ser uma boa solução. Dê uma olhada no projeto de código aberto mvcforum.com. Acho que isso é feito na implementação do padrão de design da Unidade de Trabalho.
Lyubomir Velchev
3

O que eu mais gosto é que alinha a unidade de trabalho (como o usuário a vê - ou seja, um envio de página) com a unidade de trabalho no sentido ORM.

Portanto, você pode tornar o envio da página inteiro transacional, o que não seria possível se estivesse expondo os métodos CRUD, cada um criando um novo contexto.

RB.
fonte
3

Outro motivo discreto para não usar um DbContext singleton, mesmo em um único aplicativo de usuário único encadeado, é devido ao padrão de mapa de identidade que ele usa. Isso significa que toda vez que você recuperar dados usando consulta ou por ID, ele manterá as instâncias da entidade recuperada em cache. Na próxima vez que você recuperar a mesma entidade, ela fornecerá a instância em cache da entidade, se disponível, com as modificações que você fez na mesma sessão. Isso é necessário para que o método SaveChanges não termine com várias instâncias de entidade diferentes dos mesmos registros do banco de dados; caso contrário, o contexto teria que, de alguma forma, mesclar os dados de todas essas instâncias de entidade.

O motivo é que um DbContext único pode se tornar uma bomba-relógio que pode eventualmente armazenar em cache todo o banco de dados + a sobrecarga de objetos .NET na memória.

Existem maneiras de contornar esse comportamento usando apenas consultas Linq com o .NoTracking()método de extensão. Hoje em dia, os PCs têm muita RAM. Mas geralmente esse não é o comportamento desejado.

Dmitry S.
fonte
Isso está correto, mas você deve assumir que o Garbage Collector funcionará, tornando esse problema mais virtual do que real.
tocqueville
3
O coletor de lixo não coletará nenhuma instância de objeto mantida por um objeto estático / singleton ativo. Eles vão acabar na geração 2 da pilha.
Dmitry S.
1

Outro problema a ser observado especificamente no Entity Framework é o uso de uma combinação de criação de novas entidades, carregamento lento e, em seguida, o uso dessas novas entidades (do mesmo contexto). Se você não usar IDbSet.Create (apenas um novo), o carregamento lento nessa entidade não funcionará quando for recuperado do contexto em que foi criado. Exemplo:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
Ted Elliott
fonte