NOTA: questa risposta parla di Entity Framework DbContext
, ma è applicabile a qualsiasi tipo di implementazione di Unit of Work, come LINQ to SQL DataContext
e NHibernate ISession
.
Cominciamo facendo eco a Ian: avere un singolo DbContext
per l'intera applicazione è una cattiva idea. L'unica situazione in cui ciò ha senso è quando si dispone di un'applicazione a thread singolo e di un database utilizzato esclusivamente da quella singola istanza dell'applicazione. Non DbContext
è thread-safe e, poiché i DbContext
dati della cache, diventano obsoleti molto presto. Questo ti causerà tutti i tipi di problemi quando più utenti / applicazioni lavorano contemporaneamente su quel database (il che è molto comune ovviamente). Ma mi aspetto che tu lo sappia già e voglio solo sapere perché non iniettare una nuova istanza (cioè con uno stile di vita transitorio) di DbContext
chiunque ne abbia bisogno. (per ulteriori informazioni sul perché un singolo, DbContext
o anche sul contesto per thread, è negativo, leggi questa risposta ).
Vorrei iniziare dicendo che la registrazione di un DbContext
transitorio potrebbe funzionare, ma in genere si desidera avere un'unica istanza di tale unità di lavoro in un determinato ambito. In un'applicazione Web, può essere pratico definire un tale ambito sui limiti di una richiesta Web; quindi uno stile di vita per richiesta Web. Ciò consente di far funzionare un intero set di oggetti nello stesso contesto. In altre parole, operano all'interno della stessa transazione commerciale.
Se non hai l'obiettivo di far operare una serie di operazioni all'interno dello stesso contesto, in quel caso lo stile di vita transitorio va bene, ma ci sono alcune cose da guardare:
- Poiché ogni oggetto ottiene la propria istanza, ogni classe che modifica lo stato del sistema deve chiamare
_context.SaveChanges()
(altrimenti le modifiche andrebbero perse). Ciò può complicare il tuo codice e aggiunge una seconda responsabilità al codice (la responsabilità di controllare il contesto), ed è una violazione del Principio della responsabilità singola .
- È necessario assicurarsi che le entità [caricate e salvate da a
DbContext
] non escano mai dall'ambito di tale classe, poiché non possono essere utilizzate nell'istanza di contesto di un'altra classe. Questo può complicare enormemente il tuo codice, perché quando hai bisogno di quelle entità, devi caricarle di nuovo per id, il che potrebbe anche causare problemi di prestazioni.
- Poiché
DbContext
implementa IDisposable
, probabilmente vorrai comunque eliminare tutte le istanze create. Se vuoi farlo, in pratica hai due opzioni. È necessario disporli nello stesso metodo subito dopo la chiamata context.SaveChanges()
, ma in tal caso la logica aziendale diventa proprietaria di un oggetto che viene trasferito dall'esterno. La seconda opzione è quella di disporre tutte le istanze create al limite della richiesta HTTP, ma in tal caso è ancora necessario un tipo di ambito per far sapere al contenitore quando è necessario eliminare tali istanze.
Un'altra opzione è quella di non iniettare DbContext
affatto a. Invece, si inietta un oggetto in DbContextFactory
grado di creare una nuova istanza (in passato utilizzavo questo approccio). In questo modo la logica aziendale controlla esplicitamente il contesto. Se potrebbe apparire così:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
Il lato positivo di questo è che gestisci la vita DbContext
esplicitamente ed è facile configurarlo. Consente inoltre di utilizzare un singolo contesto in un determinato ambito, che presenta chiari vantaggi, come l'esecuzione di codice in una singola transazione aziendale e la possibilità di passare da un'entità all'altra, poiché provengono dallo stesso DbContext
.
Il rovescio della medaglia è che dovrai passare DbContext
dal metodo al metodo (che è chiamato metodo di iniezione). Si noti che in un certo senso questa soluzione è la stessa dell'approccio "con ambito", ma ora l'ambito è controllato nel codice dell'applicazione stesso (e può essere ripetuto più volte). È l'applicazione che è responsabile della creazione e dello smaltimento dell'unità di lavoro. Poiché il simbolo DbContext
viene creato dopo la creazione del grafico delle dipendenze, Iniezione costruttore non è visibile e devi rimandare a Iniezione metodo quando devi passare il contesto da una classe all'altra.
Method Injection non è poi così male, ma quando la logica aziendale diventa più complessa e vengono coinvolte più classi, dovrai passare da metodo a metodo e da classe a classe, il che può complicare molto il codice (ho visto questo in passato). Per un'applicazione semplice, questo approccio funzionerà benissimo.
A causa dei lati negativi, questo approccio di fabbrica ha per i sistemi più grandi, un altro approccio può essere utile e quello è quello in cui si lascia che il contenitore o il codice dell'infrastruttura / Root di composizione gestiscano l'unità di lavoro. Questo è lo stile di cui parla la tua domanda.
Consentendo al contenitore e / o all'infrastruttura di gestirlo, il codice dell'applicazione non viene inquinato dal fatto di dover creare, (facoltativamente) eseguire il commit e lo smaltimento di un'istanza UoW, che mantiene la logica aziendale semplice e pulita (solo una singola responsabilità). Ci sono alcune difficoltà con questo approccio. Ad esempio, hai eseguito il commit e lo smaltimento dell'istanza?
Lo smaltimento di un'unità di lavoro può essere eseguito al termine della richiesta Web. Molte persone, tuttavia, ritengono erroneamente che questo sia anche il luogo in cui impegnare l'unità di lavoro. Tuttavia, a quel punto dell'applicazione, semplicemente non è possibile determinare con certezza che l'unità di lavoro debba essere effettivamente impegnata. ad esempio, se il codice del livello aziendale ha generato un'eccezione rilevata più in alto nel callstack, sicuramente non vuoi impegnarti.
La vera soluzione è di nuovo quella di gestire esplicitamente una sorta di ambito, ma questa volta fallo all'interno del Root di Composizione. Estrarre tutta la logica aziendale dietro il modello di comando / gestore , si sarà in grado di scrivere un decoratore che può essere avvolto attorno a ciascun gestore di comando che consente di eseguire ciò. Esempio:
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();
}
}
Ciò garantisce che è necessario scrivere questo codice di infrastruttura una sola volta. Qualsiasi contenitore DI solido consente di configurare un tale decoratore da avvolgere ICommandHandler<T>
in modo coerente tutte le implementazioni.