web-dev-qa-db-fra.com

Un DbContext par requête Web ... pourquoi?

J'ai lu de nombreux articles expliquant comment configurer DbContext de Entity Framework de sorte qu'un seul soit créé et utilisé par requête Web HTTP à l'aide de divers cadres DI.

Pourquoi est-ce une bonne idée en premier lieu? Quels avantages gagnez-vous en utilisant cette approche? Y a-t-il des situations où ce serait une bonne idée? Y a-t-il des choses que vous pouvez faire avec cette technique que vous ne pouvez pas faire lors de l'instanciation de l'appel de méthode DbContexts par référentiel?

374
Andrew

NOTE: Cette réponse parle de DbContext de Entity Framework _, mais elle est applicable à toute sorte de mise en œuvre d'Unity of Work, telle que LINQ to SQL's DataContext, et NHibernate ISession.

Commençons par faire écho à Ian: avoir une seule DbContext pour toute l'application est une mauvaise idée. La seule situation où cela a du sens est lorsque vous avez une application à un seul thread et une base de données qui est uniquement utilisée par cette instance d'application unique. Le DbContext n'est pas thread-safe et, et comme le DbContext met en cache les données, il devient vite obsolète. Cela vous causera toutes sortes de problèmes lorsque plusieurs utilisateurs/applications travaillent sur cette base de données simultanément (ce qui est très courant bien sûr). Mais je suppose que vous savez déjà cela et que vous voulez juste savoir pourquoi ne pas simplement injecter un nouvel exemple (à savoir un mode de vie transitoire) du DbContext à quiconque en a besoin. (pour plus d'informations sur les raisons pour lesquelles un seul DbContext -ou même sur le contexte par thread- est mauvais, lisez cette réponse ).

Permettez-moi de commencer par dire que l'enregistrement d'une DbContext en tant que transitoire peut fonctionner, mais que vous souhaitez généralement avoir une seule instance d'une telle unité de travail dans un certain périmètre. Dans une application Web, il peut être pratique de définir un tel périmètre sur les limites d’une requête Web; donc un style de vie par requête Web. Cela vous permet de laisser tout un ensemble d'objets fonctionner dans le même contexte. En d'autres termes, ils opèrent dans la même transaction commerciale.

Si vous n'avez pas pour objectif de faire fonctionner un ensemble d'opérations dans le même contexte, dans ce cas, le mode de vie transitoire convient, mais il y a quelques points à surveiller:

  • Étant donné que chaque objet a sa propre instance, chaque classe qui modifie l'état du système doit appeler _context.SaveChanges() (sinon les modifications seraient perdues). Cela peut compliquer votre code et ajoute une deuxième responsabilité au code (la responsabilité de contrôler le contexte) et constitue une violation du Principe de responsabilité unique .
  • Vous devez vous assurer que les entités [chargées et enregistrées par un DbContext] ne quittent jamais la portée d'une telle classe, car elles ne peuvent pas être utilisées dans l'instance de contexte d'une autre classe. Cela peut énormément compliquer votre code, car lorsque vous avez besoin de ces entités, vous devez les charger à nouveau par identifiant, ce qui peut également entraîner des problèmes de performances.
  • Puisque DbContext implémente IDisposable, vous souhaiterez probablement toujours supprimer toutes les instances créées. Si vous voulez faire cela, vous avez essentiellement deux options. Vous devez les disposer de la même manière juste après avoir appelé context.SaveChanges(), mais dans ce cas, la logique applicative devient propriétaire d'un objet qui le transmet de l'extérieur. La deuxième option consiste à supprimer toutes les instances créées à la limite de la requête HTTP, mais dans ce cas, vous avez encore besoin d'une sorte de portée pour permettre au conteneur de savoir quand ces instances doivent être éliminées.

Une autre option consiste à not injecter un DbContext du tout. Au lieu de cela, vous injectez un DbContextFactory capable de créer une nouvelle instance (j'avais l'habitude d'utiliser cette approche dans le passé). De cette façon, la logique applicative contrôle le contexte de manière explicite. Si pourrait ressembler à ceci:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

L'avantage de ceci est que vous gérez la vie de la DbContext de manière explicite et qu'il est facile de la configurer. Cela vous permet également d'utiliser un seul contexte dans une certaine étendue, ce qui présente des avantages évidents, tels que l'exécution de code dans une transaction commerciale unique et le fait de pouvoir contourner des entités, puisqu'elles proviennent du même DbContext.

L'inconvénient est que vous devrez faire passer la méthode DbContext de méthode en méthode (appelée méthode d'injection). Notez que dans un sens, cette solution est la même que l'approche "portée", mais maintenant, la portée est contrôlée dans le code d'application lui-même (et peut-être répétée plusieurs fois). C'est l'application qui est responsable de la création et de la suppression de l'unité de travail. Etant donné que DbContext est créé après la construction du graphe de dépendances, l'injection de constructeur est absente et vous devez vous reporter à la méthode d'injection lorsque vous devez transmettre le contexte d'une classe à une autre.

L’injection de méthode n’est pas si mauvaise, mais lorsque la logique métier devient plus complexe et que plus de classes sont impliquées, vous devrez la passer de méthode à méthode et de classe à classe, ce qui peut beaucoup compliquer le code (j’ai vu ceci dans le passé). Pour une application simple, cette approche suffira cependant.

En raison des inconvénients de cette approche d’usine pour les systèmes plus volumineux, une autre approche peut être utile et consiste à laisser le conteneur ou le code d’infrastructure/ Composition Root gérer l’unité de travail. C'est le style de votre question.

En laissant le conteneur et/ou l'infrastructure gérer cela, votre code d'application n'est pas pollué en raison de la nécessité de créer, de valider (éventuellement) et de supprimer une instance UoW, ce qui maintient la logique métier simple et propre (responsabilité unique). Il y a quelques difficultés avec cette approche. Par exemple, avez-vous commis et supprimé l'instance?

L'élimination d'une unité de travail peut être effectuée à la fin de la demande Web. Cependant, beaucoup de gens incorrectement supposent que c’est aussi le lieu de commettre l’unité de travail. Cependant, à ce stade de l'application, vous ne pouvez tout simplement pas déterminer avec certitude que l'unité de travail doit réellement être validée. par exemple. Si le code de la couche de gestion a généré une exception qui a été détectée plus haut dans la pile d'appels, vous devez absolument ne pas vous engager.

La vraie solution est encore une fois de gérer explicitement une sorte d’étendue, mais cette fois-ci dans la racine de la composition. En résumant toute la logique métier derrière le modèle de commande/gestionnaire , vous pourrez écrire un décorateur pouvant être encapsulé autour de chaque gestionnaire de commandes permettant de le faire. Exemple:

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();
    } 
}

Cela garantit que vous ne devez écrire ce code d'infrastructure qu'une seule fois. Tout conteneur DI solide vous permet de configurer un tel décorateur de manière à ce qu'il soit mis en place de manière cohérente dans toutes les implémentations ICommandHandler<T>.

537
Steven

Pas une seule réponse ici ne répond réellement à la question. Le PO n'a pas posé de question sur une conception DbContext unique/par application, il a posé des questions sur une conception par requête Web et sur les avantages potentiels.

Je ferai référence à http://mehdi.me/ambient-dbcontext-in-ef6/ car Mehdi est une ressource fantastique:

Gains de performance possibles.

Chaque instance de DbContext maintient un cache de premier niveau de toutes les entités qu'il charge de la base de données. Chaque fois que vous interrogez une entité à l'aide de sa clé primaire, DbContext tentera d'abord de la récupérer à partir de son cache de premier niveau avant de l'interroger par défaut à partir de la base de données. En fonction de votre modèle de requête de données, la réutilisation du même DbContext dans plusieurs transactions commerciales séquentielles peut entraîner moins de requêtes de base de données grâce au cache de premier niveau DbContext.

Il permet le chargement paresseux.

Si vos services renvoient des entités persistantes (par opposition à des modèles d'affichage ou à d'autres types de DTO) et que vous souhaitez tirer parti du chargement différé sur ces entités, la durée de vie de l'instance DbContext à partir de laquelle ces entités ont été extraites doit s'étendre au-delà. la portée de la transaction commerciale. Si la méthode service supprimait l'instance DbContext qu'elle utilisait avant le renvoi, toute tentative de chargement paresseux des propriétés sur les entités renvoyées échouerait (que le chargement paresseux soit une bonne idée ou non, c'est un débat totalement différent dans lequel nous n'entrerons pas dans les détails. ici). Dans notre exemple d'application Web, le chargement différé serait généralement utilisé dans les méthodes d'action du contrôleur sur les entités renvoyées par une couche de service distincte. Dans ce cas, l'instance DbContext utilisée par la méthode de service pour charger ces entités doit rester active pendant la durée de la demande Web (ou à tout le moins jusqu'à la fin de la méthode d'action).

Gardez à l'esprit qu'il y a aussi des inconvénients. Ce lien contient de nombreuses autres ressources à lire sur le sujet.

Il suffit de poster ceci au cas où quelqu'un d'autre trébuche sur cette question et ne soit pas absorbé par des réponses qui ne répondent pas réellement à la question.

31
user4893106

Il y a deux contradictoires recommandations de Microsoft et beaucoup de gens utilisent DbContexts de manière complètement divergente.

  1. Une recommandation est de "Dispose DbContexts dès que possible" car avoir un DbContext Alive occupe des ressources précieuses telles que des connexions à une base de données, etc.
  2. L’autre indique que n DbContext par requête est vivement recommandé

Celles-ci se contredisent parce que si votre requête fait beaucoup de choses sans lien avec le contenu de la base de données, votre DbContext est conservé sans raison. Il est donc inutile de garder votre DbContext en vie pendant que votre requête attend que des tâches aléatoires soient effectuées ...

Tant de gens qui suivent règle 1 ont leurs DbContexts dans leur "modèle de référentiel" et créent ne nouvelle instance par requête de base de données so X * DbContext par demande

Ils récupèrent simplement leurs données et disposent du contexte dès que possible. Ceci est considéré par PLUSIEURS personnes comme une pratique acceptable. Bien que cela présente les avantages d'occuper vos ressources de base de données pendant un minimum de temps, il sacrifie clairement tous les nitOfWork et Caching les bonbons qu'offre EF.

Garder en vie une seule instance polyvalente de DbContext maximise les avantages de mise en cache mais puisque DbContext est non thread-safe, chaque requête Web s'exécute sur son propre thread. , un DbContext par demande est le le plus long vous pouvez le conserver.

La recommandation de l'équipe d'EF concernant l'utilisation d'un contexte 1 Db par requête est clairement basée sur le fait que, dans une application Web, UnitOfWork sera probablement dans une requête et que cette requête comporte un thread. Ainsi, un seul DbContext par demande correspond à l’avantage idéal de UnitOfWork et de Caching.

Mais dans de nombreux cas, ce n'est pas vrai. Je considère journalisation un UnitOfWork séparé, ce qui donne un nouveau DbContext pour la journalisation post-requête threads asynchrones est tout à fait acceptable

Donc finalement, il s'avère que la durée de vie d'un DbContext est limitée à ces deux paramètres. nitOfWork et Fil de discussion

29
Anestis Kivranoglou

Je suis presque certain que c'est parce que DbContext n'est pas du tout sécurisé par les threads. Donc, partager la chose n'est jamais une bonne idée.

22
Ian

Une chose qui n'est pas vraiment abordée dans la question ou la discussion est le fait que DbContext ne peut pas annuler les modifications. Vous pouvez soumettre des modifications, mais vous ne pouvez pas effacer l’arborescence des modifications. Par conséquent, si vous utilisez un contexte par requête, vous n’avez pas de chance si vous devez jeter les modifications pour une raison quelconque.

Personnellement, je crée des instances de DbContext lorsque cela est nécessaire - généralement attaché à des composants métier capables de recréer le contexte si nécessaire. De cette façon, je contrôle le processus plutôt que de me faire imposer une seule instance. Je n'ai pas non plus besoin de créer le DbContext à chaque démarrage du contrôleur, qu'il soit utilisé ou non. Ensuite, si je souhaite toujours avoir des instances par requête, je peux les créer dans le CTOR (via DI ou manuellement) ou les créer selon les besoins dans chaque méthode de contrôleur. Personnellement, j’adopte généralement cette dernière approche pour éviter de créer des instances de DbContext quand elles ne sont pas réellement nécessaires.

Cela dépend de quel angle vous le regardez aussi. Pour moi, l'instance par requête n'a jamais eu de sens. Est-ce que le DbContext appartient vraiment à la requête HTTP? En termes de comportement, c'est le mauvais endroit. Vos composants métier doivent créer votre contexte et non la requête Http. Vous pouvez ensuite créer ou supprimer vos composants métier en fonction de vos besoins, sans vous soucier de la durée de vie du contexte.

14
Rick Strahl

Je suis d'accord avec les avis précédents. Il est bon de dire que si vous souhaitez partager DbContext dans une application à thread unique, vous aurez besoin de plus de mémoire. Par exemple, mon application Web sur Azure (une très petite instance) nécessite 150 Mo de mémoire supplémentaire et environ 30 utilisateurs par heure. Application sharing DBContext in HTTP Request

Voici un exemple d’image: les applications ont été déployées à midi

9
Miroslav Holec

Ce que j’aime, c’est qu’elle aligne l’unité de travail (telle que la voit l’utilisateur - c’est-à-dire une page soumise) avec l’unité de travail au sens ORM.

Par conséquent, vous pouvez effectuer la transaction de soumission de page entière, ce que vous ne pourriez pas faire si vous exposiez des méthodes CRUD à chaque création d'un nouveau contexte.

3
RB.

Une autre raison sous-estimée de ne pas utiliser un singleton DbContext, même dans une application mono-utilisateur mono-threadée, est due au modèle de carte d'identité utilisé. Cela signifie que chaque fois que vous récupérez des données à l'aide de query ou par id, les instances d'entités extraites seront conservées dans le cache. La prochaine fois que vous récupérerez la même entité, cela vous donnera l'instance mise en cache de l'entité, si disponible, avec toutes les modifications que vous avez apportées au cours de la même session. Cela est nécessaire pour que la méthode SaveChanges ne se retrouve pas avec plusieurs instances d'entités différentes du même enregistrement de base de données; sinon, le contexte devrait en quelque sorte fusionner les données de toutes ces instances d'entités.

La raison en est qu’un singleton DbContext peut devenir une bombe à retardement qui pourrait éventuellement mettre en cache toute la base de données + la surcharge d’objets .NET en mémoire.

Il existe des solutions à ce problème en utilisant uniquement les requêtes Linq avec la méthode d'extension .NoTracking(). De plus, ces jours-ci, les PC ont beaucoup de RAM. Mais ce n’est généralement pas le comportement souhaité.

2
Dmitry S.

Un autre problème à surveiller avec Entity Framework concerne plus particulièrement l'utilisation combinée de la création de nouvelles entités, du chargement différé, puis de l'utilisation de ces nouvelles entités (dans le même contexte). Si vous n'utilisez pas IDbSet.Create (vs simplement nouveau), le chargement différé sur cette entité ne fonctionne pas lorsqu'il est extrait du contexte dans lequel il a été créé. Exemple:

 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.
1
Ted Elliott