Un DbContext par demande Web… pourquoi?

398

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

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

Andrew
la source
9
Gueddari dans mehdi.me/ambient-dbcontext-in-ef6 appelle une instance DbContext par méthode de référentiel appeler un antipattern. Quote: "En faisant cela, vous perdez à peu près toutes les fonctionnalités qu'Entity Framework fournit via le DbContext, y compris son cache de premier niveau, sa carte d'identité, son unité de travail et ses capacités de suivi des modifications et de chargement paresseux . " Excellent article avec d'excellentes suggestions pour gérer le cycle de vie de DBContexts. Vaut vraiment la peine d'être lu.
Christoph

Réponses:

565

REMARQUE: cette réponse parle des Entity Framework DbContext, mais elle s'applique à n'importe quel type d'implémentation d'unité de travail, comme LINQ to SQL DataContextet NHibernate ISession.

Commençons par faire écho à Ian: Avoir un single DbContextpour toute l'application est une mauvaise idée. La seule situation où cela a du sens est lorsque vous avez une application à thread unique et une base de données qui est uniquement utilisée par cette instance d'application unique. Le DbContextn'est pas thread-safe et et puisque les DbContextdonnées en cache, il devient périmé très bientôt. Cela vous mettra dans toutes sortes de problèmes lorsque plusieurs utilisateurs / applications travaillent simultanément sur cette base de données (ce qui est très courant bien sûr). Mais je m'attends à ce que vous le sachiez déjà et que vous vouliez simplement savoir pourquoi ne pas simplement injecter une nouvelle instance (c'est-à-dire avec un style de vie transitoire) de la DbContextpersonne qui en a besoin. (pour plus d'informations sur les raisons pour lesquelles un single 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'un DbContexttransitoire pourrait fonctionner, mais généralement vous voulez avoir une seule instance d'une telle unité de travail dans une certaine portée. Dans une application Web, il peut être pratique de définir une telle portée sur les limites d'une demande Web; ainsi un style de vie par demande Web. Cela vous permet de laisser un ensemble d'objets fonctionner dans le même contexte. En d'autres termes, ils opèrent au sein de la même transaction commerciale.

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

  • Étant donné que chaque objet obtient 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 compliquer énormément votre code, car lorsque vous avez besoin de ces entités, vous devez les charger à nouveau par id, ce qui pourrait également entraîner des problèmes de performances.
  • Depuis les DbContextimplémentations IDisposable, vous souhaiterez probablement toujours supprimer toutes les instances créées. Si vous voulez le faire, vous avez essentiellement deux options. Vous devez les disposer de la même méthode juste après l'appel context.SaveChanges(), mais dans ce cas, la logique métier s'approprie un objet qu'il est transmis de l'extérieur. La deuxième option consiste à supprimer toutes les instances créées à la limite de la demande Http, mais dans ce cas, vous avez toujours besoin d'une sorte de portée pour indiquer au conteneur quand ces instances doivent être supprimées.

Une autre option consiste à ne pas injecter DbContextdu tout. Au lieu de cela, vous injectez un DbContextFactoryqui est capable de créer une nouvelle instance (j'avais l'habitude d'utiliser cette approche dans le passé). De cette façon, la logique métier contrôle explicitement le contexte. Si cela 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();
    }
}

Le côté positif de cela est que vous gérez la vie de manière DbContextexplicite et il est facile de configurer cela. Il vous permet également d'utiliser un contexte unique dans une certaine portée, ce qui présente des avantages évidents, tels que l'exécution de code dans une seule transaction commerciale et la possibilité de contourner des entités, car elles proviennent de la même DbContext.

L'inconvénient est que vous devrez contourner la DbContextméthode from to method (qui est 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 l'élimination de l'unité de travail. Étant donné que le DbContextest créé après la construction du graphique de dépendance, l'injection de constructeur est hors de l'image et vous devez vous reporter à l'injection de méthode lorsque vous devez transmettre le contexte d'une classe à l'autre.

L'injection de méthodes n'est pas si mauvaise, mais lorsque la logique métier devient plus complexe et que davantage de classes sont impliquées, vous devrez la passer de méthode en méthode et de classe en classe, ce qui peut compliquer beaucoup le code (j'ai vu cela dans le passé). Pour une application simple, cette approche fera très bien l'affaire cependant.

En raison des inconvénients, cette approche d'usine s'applique aux systèmes plus gros, une autre approche peut être utile et c'est celle où vous laissez le conteneur ou le code d'infrastructure / Racine de composition gérer l'unité de travail. C'est le style sur lequel porte votre question.

En laissant le conteneur et / ou l'infrastructure gérer cela, votre code d'application n'est pas pollué en devant créer, (éventuellement) valider et supprimer une instance UoW, ce qui maintient la logique métier simple et propre (juste une responsabilité unique). Il y a quelques difficultés avec cette approche. Par exemple, avez-vous validé 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 supposent à tort que c'est aussi le lieu pour engager l'unité de travail. Cependant, à ce stade de la demande, vous ne pouvez tout simplement pas déterminer avec certitude que l'unité d'oeuvre doit réellement être engagée. Par exemple, si le code de la couche métier a levé une exception qui a été interceptée plus haut dans la pile d'appels, vous ne voulez certainement pas vous engager .

La vraie solution est à nouveau de gérer explicitement une sorte de portée, mais cette fois-ci, faites-le à l'intérieur de la racine de composition. En résumant toute la logique métier derrière le modèle de commande / gestionnaire , vous pourrez écrire un décorateur qui peut être enroulé autour de chaque gestionnaire de commandes qui permet 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 n'avez besoin d'écrire ce code d'infrastructure qu'une seule fois. Tout conteneur DI solide vous permet de configurer un tel décorateur pour qu'il soit enroulé autour de toutes les ICommandHandler<T>implémentations de manière cohérente.

Steven
la source
2
Wow - merci pour la réponse complète. Si je pouvais voter deux fois, je le ferais. Ci-dessus, vous dites "... aucune intention de laisser un ensemble d'opérations fonctionner dans le même contexte, dans ce cas, le mode de vie transitoire est bien ...". Que voulez-vous dire par "transitoire", en particulier?
Andrew
14
@Andrew: 'Transient' est un concept d'injection de dépendances, ce qui signifie que si un service est configuré pour être transitoire, une nouvelle instance du service est créée chaque fois qu'il est injecté à un consommateur.
Steven
1
@ user981375: Pour les opérations CRUD, vous pouvez créer un générique CreateCommand<TEnity>et un générique CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(et faire de même pour Mettre à jour et Supprimer et avoir une seule GetByIdQuery<TEntity>requête). Cependant, vous devez vous demander si ce modèle est une abstraction utile pour les opérations CRUD, ou s'il ajoute simplement de la complexité. Néanmoins, vous pourriez bénéficier de la possibilité d'ajouter facilement des préoccupations transversales (par le biais de décorateurs) en utilisant ce modèle. Vous devrez peser le pour et le contre.
Steven
3
+1 Croiriez-vous que j'ai écrit toute cette réponse avant de lire ceci? BTW IMO Je pense qu'il est important pour vous de discuter de l'élimination du DbContext à la fin (bien que ce soit formidable que vous restiez agnostique pour les conteneurs)
Ruben Bartelink
1
Mais vous ne passez pas le contexte à la classe décorée, comment la classe décorée pourrait-elle fonctionner avec le même contexte que celui transmis à la classe TransactionCommandHandlerDecorator? par exemple, si la classe décorée est une InsertCommandHandlerclasse, comment pourrait-elle enregistrer l'opération d'insertion dans le contexte (DbContext dans EF)?
Masoud
35

Il existe deux recommandations contradictoires de Microsoft et de nombreuses personnes 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 comme les connexions db etc ...
  2. L'autre indique que One DbContext par demande est fortement recommandé

Ceux-ci se contredisent parce que si votre demande fait beaucoup de choses sans rapport avec le contenu Db, votre DbContext est conservé sans raison. Il est donc inutile de garder votre DbContext en vie alors que votre demande n'attend que des choses aléatoires pour se faire ...

Tant de personnes qui suivent la règle 1 ont leurs DbContexts à l'intérieur de leur "modèle de référentiel" et créent une nouvelle instance par requête de base de données afin X * DbContext par demande

Ils obtiennent simplement leurs données et éliminent le contexte dès que possible. Ceci est considéré par beaucoup de gens comme une pratique acceptable. Bien que cela ait les avantages d'occuper vos ressources de base de données pour le minimum de temps, cela sacrifie clairement tout ce que EF UnitOfWork et Caching Candy a à offrir.

Garder en vie une seule instance polyvalente de DbContext maximise les avantages de la mise en cache, mais comme DbContext n'est pas sûr pour les threads et que chaque demande Web s'exécute sur son propre thread, un DbContext par demande est le plus long que vous pouvez le conserver.

Ainsi, la recommandation de l'équipe d'EF concernant l'utilisation de 1 Db Context par demande est clairement basée sur le fait que dans une application Web, UnitOfWork va très probablement être dans une demande et que cette demande a un thread. Ainsi, un DbContext par demande est comme l'avantage idéal de UnitOfWork et Caching.

Mais dans de nombreux cas, ce n'est pas vrai. Je considère que la journalisation d' un UnitOfWork distinct ayant ainsi un nouveau DbContext pour la journalisation post-requête dans les threads asynchrones est tout à fait acceptable

Finalement, il s'avère que la durée de vie d'un DbContext est limitée à ces deux paramètres. UnitOfWork et Thread

Anestis Kivranoglou
la source
3
En toute honnêteté, vos requêtes HTTP devraient se terminer assez rapidement (quelques ms). S'ils durent plus longtemps que cela, vous voudrez peut-être envisager de faire un traitement en arrière-plan avec quelque chose comme un planificateur de travaux externe afin que la demande puisse retourner immédiatement. Cela dit, votre architecture ne devrait pas non plus vraiment s'appuyer sur HTTP. Dans l'ensemble, une bonne réponse cependant.
écraser
34

Pas une seule réponse ici ne répond réellement à la question. Le PO n'a pas posé de questions sur une conception DbContext singleton / par application, il a posé des questions sur une conception de demande par (Web) et sur les avantages potentiels qui pourraient exister.

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

Gains de performances possibles.

Chaque instance DbContext maintient un cache de premier niveau de toutes les entités que ses charges de la base de données. Chaque fois que vous interrogez une entité par sa clé primaire, le DbContext essaiera d'abord de la récupérer à partir de son cache de premier niveau avant de demander par défaut à l'interroger à 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 sur plusieurs transactions commerciales séquentielles peut entraîner une réduction du nombre 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 au renvoi de modèles de vue ou d'autres types de DTO) et que vous souhaitez profiter 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 de service supprimait l'instance DbContext qu'elle utilisait avant de retourner, toute tentative de chargement différé des propriétés sur les entités retournées échouerait (que l'utilisation du chargement différé soit une bonne idée soit un débat différent dans lequel nous n'entrerons pas dans ici). Dans notre exemple d'application Web, le chargement différé est 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,

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 tomberait sur cette question et ne serait pas absorbé par des réponses qui ne répondent pas réellement à la question.

user4893106
la source
Bon lien! La gestion explicite du DBContext ressemble à l'approche la plus sûre.
aggsol
22

Je suis presque certain que c'est parce que le DbContext n'est pas du tout sûr pour les threads. Partager la chose n'est donc jamais une bonne idée.

Ian
la source
Voulez-vous dire que le partager entre les requêtes HTTP n'est jamais une bonne idée?
Andrew
2
Oui Andrew, c'est ce qu'il voulait dire. Le partage du contexte est uniquement pour les applications de bureau à thread unique.
Elisabeth
10
Qu'en est-il du partage du contexte pour une demande. Donc, pour une seule demande, nous pouvons avoir accès à différents référentiels et effectuer une transaction entre eux en partageant un même contexte?
Lyubomir Velchev
16

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, donc si vous utilisez un contexte par demande, vous n'avez pas de chance si vous devez annuler 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 qui ont la capacité de recréer le contexte si nécessaire. De cette façon, j'ai le contrôle sur le processus, plutôt que d'avoir une seule instance imposée à moi. Je n'ai pas non plus à créer le DbContext à chaque démarrage du contrôleur, qu'il soit réellement utilisé ou non. Ensuite, si je veux toujours avoir des instances par demande, 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, je prends généralement cette dernière approche pour éviter de créer des instances DbContext lorsqu'elles ne sont pas réellement nécessaires.

Cela dépend de quel angle vous le regardez aussi. Pour moi, l'instance par demande n'a jamais eu de sens. Le DbContext appartient-il vraiment à la demande Http? En termes de comportement, ce n'est pas le bon endroit. Vos composants métier doivent créer votre contexte, pas la demande Http. Ensuite, vous pouvez créer ou jeter vos composants métier selon vos besoins et ne vous souciez jamais de la durée de vie du contexte.

Rick Strahl
la source
1
C'est une réponse intéressante et je suis partiellement d'accord avec vous. Pour moi, un DbContext n'a pas à être lié à une demande Web, mais il EST toujours tapé dans une seule «demande» comme dans: «transaction commerciale». Et lorsque vous liez le contexte à une transaction commerciale, l'annulation des modifications devient vraiment bizarre. Mais ne pas l'avoir sur la limite des requêtes Web ne signifie pas que les composants métier (BC) doivent créer le contexte; Je pense que ce n'est pas leur responsabilité. Au lieu de cela, vous pouvez appliquer la portée à l'aide de décorateurs autour de vos BC. De cette façon, vous pouvez même modifier la portée sans aucun changement de code.
Steven
1
Eh bien, dans ce cas, l'injection dans l'objet métier doit concerner la gestion de la durée de vie. À mon avis, l'objet métier possède le contexte et, en tant que tel, doit contrôler la durée de vie.
Rick Strahl
En bref, que voulez-vous dire lorsque vous dites "la possibilité de recréer le contexte si nécessaire"? utilisez-vous votre propre capacité de restauration? pouvez-vous élaborer un peu?
tntwyckoff
2
Personnellement, je pense que c'est un peu gênant de forcer un DbContext au début là-bas. Il n'y a aucune garantie que vous ayez même besoin d'accéder à la base de données. Peut-être que vous appelez un service tiers qui change d'état de ce côté. Ou peut-être avez-vous en fait 2 ou 3 bases de données avec lesquelles vous travaillez en même temps. Vous ne créeriez pas un tas de DbContexts au début juste au cas où vous finiriez par les utiliser. L'entreprise connaît les données avec lesquelles elle travaille, elle appartient donc à cela. Mettez simplement un TransactionScope au début s'il est nécessaire. Je ne pense pas que tous les appels en aient besoin. Cela prend des ressources.
Daniel Lorenz
C'est la question de savoir si vous autorisez le conteneur à contrôler la durée de vie du dbcontext qui contrôle ensuite la durée de vie des contrôles parents, parfois indûment. Disons que si je veux un simple service singleton injecté dans mes contrôleurs, je ne serai pas en mesure d'utiliser l'injection de constucteur en raison de la sémantique par demande.
davidcarr
10

Je suis d'accord avec les avis précédents. Il est bon de dire que si vous souhaitez partager DbContext dans une application à un seul thread, vous aurez besoin de plus de mémoire. Par exemple, mon application Web sur Azure (une petite instance supplémentaire) a besoin de 150 Mo de mémoire supplémentaires et j'ai environ 30 utilisateurs par heure. Partage d'application DBContext dans la requête HTTP

Voici un exemple réel d'image: l'application a été déployée en 12PM

Miroslav Holec
la source
L'idée est peut-être de partager le contexte d'une demande. Si nous accédons à différents référentiels et - classes DBSet et que nous voulons que les opérations avec eux soient transactionnelles, cela devrait être une bonne solution. Jetez un oeil au projet open source mvcforum.com Je pense que cela se fait dans leur mise en œuvre du modèle de conception de l'unité de travail.
Lyubomir Velchev
3

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

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

RB.
la source
3

Une autre raison sous-estimée de ne pas utiliser un DbContext singleton, même dans une application mono-utilisateur unique, est due au modèle de carte d'identité qu'il utilise. Cela signifie que chaque fois que vous récupérez des données à l'aide d'une requête ou d'un identifiant, il conserve les instances d'entité récupérées dans le cache. La prochaine fois que vous récupérerez la même entité, elle vous donnera l'instance mise en cache de l'entité, si disponible, avec toutes les modifications que vous avez apportées dans la même session. Cela est nécessaire pour que la méthode SaveChanges ne se retrouve pas avec plusieurs instances d'entité différentes des mêmes enregistrements de base de données; sinon, le contexte devrait d'une manière ou d'une autre fusionner les données de toutes ces instances d'entité.

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

Il existe des moyens de contourner ce comportement en utilisant uniquement des requêtes Linq avec la .NoTracking()méthode d'extension. De plus, de nos jours, les PC ont beaucoup de RAM. Mais ce n'est généralement pas le comportement souhaité.

Dmitry S.
la source
C'est correct, mais vous devez supposer que le garbage collector fonctionnera, rendant ce problème plus virtuel que réel.
tocqueville
3
Le garbage collector ne collectera aucune instance d'objet détenue par un objet statique / singleton actif. Ils finiront dans la génération 2 du tas.
Dmitry S.
1

Un autre problème à surveiller avec Entity Framework en particulier est lors de l'utilisation d'une combinaison de création de nouvelles entités, de chargement différé, puis d'utilisation de ces nouvelles entités (à partir du même contexte). Si vous n'utilisez pas IDbSet.Create (vs juste nouveau), le chargement paresseux sur cette entité ne fonctionne pas lorsqu'il est récupéré hors 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.
Ted Elliott
la source