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 DataContext
et NHibernate ISession
.
Commençons par faire écho à Ian: Avoir un single DbContext
pour 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 DbContext
n'est pas thread-safe et et puisque les DbContext
donné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 DbContext
personne 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 DbContext
transitoire 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
DbContext
implé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 DbContext
du tout. Au lieu de cela, vous injectez un DbContextFactory
qui 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 DbContext
explicite 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 DbContext
mé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 DbContext
est 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.