Un DbContext per richiesta web ... perché?


398

Ho letto molti articoli che spiegano come impostare Entity Framework in DbContextmodo che solo uno venga creato e utilizzato per richiesta Web HTTP utilizzando vari framework DI.

Perché questa è una buona idea in primo luogo? Quali vantaggi ottieni utilizzando questo approccio? Ci sono alcune situazioni in cui questa sarebbe una buona idea? Ci sono cose che puoi fare usando questa tecnica che non puoi fare quando si crea un'istanza DbContextper la chiamata al metodo repository?


9
Gueddari in mehdi.me/ambient-dbcontext-in-ef6 chiama l'istanza DbContext per metodo repository che chiama un antipattern. Citazione: "In questo modo, perdi praticamente tutte le funzionalità fornite da Entity Framework tramite DbContext, tra cui la cache di primo livello, la mappa delle identità, l'unità di lavoro, il rilevamento delle modifiche e le capacità di caricamento lento ". Articolo eccellente con ottimi suggerimenti per la gestione del ciclo di vita di DBContexts. Sicuramente vale la pena leggere.
Christoph,

Risposte:


565

NOTA: questa risposta parla di Entity Framework DbContext, ma è applicabile a qualsiasi tipo di implementazione di Unit of Work, come LINQ to SQL DataContexte NHibernate ISession.

Cominciamo facendo eco a Ian: avere un singolo DbContextper 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 DbContextdati 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 DbContextchiunque ne abbia bisogno. (per ulteriori informazioni sul perché un singolo, DbContexto anche sul contesto per thread, è negativo, leggi questa risposta ).

Vorrei iniziare dicendo che la registrazione di un DbContexttransitorio 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é DbContextimplementa 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 DbContextaffatto a. Invece, si inietta un oggetto in DbContextFactorygrado 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 DbContextesplicitamente 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 DbContextdal 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 DbContextviene 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.


2
Caspita, grazie per la risposta esaustiva. Se potessi votare due volte, lo farei. Sopra, dici "... nessuna intenzione di lasciare operare un intero insieme di operazioni all'interno dello stesso contesto, in quel caso lo stile di vita transitorio va bene ...". Cosa intendi con "transitorio", in particolare?
Andrew,

14
@Andrew: 'Transitorio' è un concetto di iniezione di dipendenza, il che significa che se un servizio è configurato per essere temporaneo, una nuova istanza del servizio viene creata ogni volta che viene iniettata in un consumatore.
Steven,

1
@ user981375: per le operazioni CRUD è possibile creare un generico CreateCommand<TEnity>e un generico CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(e fare lo stesso per l'aggiornamento, l'eliminazione e una sola GetByIdQuery<TEntity>query). Tuttavia, dovresti chiederti se questo modello è un'astrazione utile per le operazioni CRUD o se aggiunge solo complessità. Tuttavia, potresti beneficiare della possibilità di aggiungere facilmente preoccupazioni trasversali (attraverso i decoratori) usando questo modello. Dovrai valutare i pro e i contro.
Steven,

3
+1 Credi che ho scritto tutta questa risposta prima di leggerla? BTW IMO Penso che sia importante per te discutere lo smaltimento del DbContext alla fine (anche se è fantastico che tu rimanga indipendente dal contenitore)
Ruben Bartelink

1
Ma non passi il contesto alla classe decorata, come la classe decorata potrebbe lavorare con lo stesso contesto che è passato al TransactionCommandHandlerDecorator? ad esempio se la classe decorata è InsertCommandHandlerclass, come potrebbe registrare l'operazione di inserimento nel contesto (DbContext in EF)?
Masoud,

35

Ci sono due raccomandazioni contraddittorie da parte di microsoft e molte persone usano DbContexts in modo completamente divergente.

  1. Una raccomandazione è "Smaltire DbContexts appena possibile" perché avere un DbContext Alive occupa risorse preziose come connessioni db ecc.
  2. L'altro afferma che un DbContext per richiesta è altamente raccomandato

Quelli si contraddicono a vicenda perché se la tua richiesta sta facendo molte cose non correlate alle cose Db, allora il tuo DbContext viene mantenuto senza motivo. Quindi è inutile mantenere vivo il tuo DbContext mentre la tua richiesta è solo in attesa che vengano fatte cose casuali ...

Così tante persone che seguono la regola 1 hanno i loro DbContexts all'interno del loro "modello di repository" e creano una nuova istanza per query di database, quindi X * DbContext per richiesta

Ricevono solo i loro dati e dispongono il contesto il prima possibile. Questo è considerato da MOLTE persone una pratica accettabile. Mentre questo ha i vantaggi di occupare le tue risorse di database per il tempo minimo, sacrifica chiaramente tutte le caramelle UnitOfWork e Caching che EF ha da offrire.

Mantenere viva una singola istanza multiuso di DbContext massimizza i vantaggi della memorizzazione nella cache, ma poiché DbContext non è sicuro per i thread e ogni richiesta Web viene eseguita sul proprio thread, un DbContext per richiesta è il più lungo che puoi mantenerlo.

Quindi la raccomandazione del team EF sull'uso di 1 Db Context per richiesta si basa chiaramente sul fatto che in un'applicazione Web un UnitOfWork molto probabilmente si troverà all'interno di una richiesta e quella richiesta ha un thread. Quindi un DbContext per richiesta è come il vantaggio ideale di UnitOfWork e Caching.

Ma in molti casi questo non è vero. Considero la registrazione di un'unità UnitOfWork separata, pertanto avere un nuovo DbContext per la registrazione post-richiesta nei thread asincroni è completamente accettabile

Quindi alla fine si scopre che la durata di un DbContext è limitata a questi due parametri. UnitOfWork e Thread


3
In tutta onestà, le tue richieste HTTP dovrebbero terminare piuttosto rapidamente (pochi ms). Se stanno andando più a lungo di quello, allora potresti voler pensare a fare un po 'di elaborazione in background con qualcosa come uno scheduler di lavoro esterno in modo che la richiesta possa tornare immediatamente. Detto questo, la tua architettura non dovrebbe fare affidamento nemmeno su HTTP. Nel complesso, comunque una buona risposta.
schiaccia

34

Nessuna risposta qui in realtà risponde alla domanda. L'OP non ha chiesto informazioni su un progetto DbContext singleton / per applicazione, ha chiesto un progetto su richiesta (web) e quali potenziali benefici potrebbero esistere.

Farò riferimento a http://mehdi.me/ambient-dbcontext-in-ef6/ poiché Mehdi è una risorsa fantastica:

Possibili miglioramenti delle prestazioni.

Ogni istanza DbContext mantiene una cache di primo livello di tutte le entità caricate dal database. Ogni volta che si esegue una query su un'entità tramite la sua chiave primaria, DbContext tenterà innanzitutto di recuperarla dalla sua cache di primo livello prima di passare all'impostazione predefinita per interrogarla dal database. A seconda del modello di query dei dati, il riutilizzo dello stesso DbContext su più transazioni aziendali sequenziali può comportare un minor numero di query sul database grazie alla cache di primo livello DbContext.

Consente il caricamento lento.

Se i tuoi servizi restituiscono entità persistenti (anziché restituire modelli di vista o altri tipi di DTO) e desideri sfruttare il caricamento lento su tali entità, la durata dell'istanza DbContext da cui sono state recuperate tali entità deve estendersi oltre l'ambito della transazione commerciale. Se il metodo di servizio eliminasse l'istanza DbContext utilizzata prima della restituzione, qualsiasi tentativo di caricare in modo pigro le proprietà sulle entità restituite fallirebbe (indipendentemente dal fatto che utilizzare il caricamento in modalità lazy sia una buona idea è un dibattito completamente diverso nel quale non entreremo nel Qui). Nel nostro esempio di applicazione Web, il caricamento lento viene in genere utilizzato nei metodi di azione del controller su entità restituite da un livello di servizio separato. In quel caso,

Tieni presente che ci sono anche dei contro. Quel link contiene molte altre risorse da leggere sull'argomento.

Pubblicando questo nel caso in cui qualcun altro si imbatti in questa domanda e non venga assorbito dalle risposte che in realtà non rispondono alla domanda.


Buon collegamento! La gestione esplicita di DBContext sembra l'approccio più sicuro.
aggsol,

22

Sono abbastanza sicuro che sia perché DbContext non è affatto sicuro per i thread. Quindi condividere la cosa non è mai una buona idea.


Vuoi dire che condividerlo attraverso richieste HTTP non è mai una buona idea?
Andrew,

2
Sì, Andrew è quello che intendeva dire. La condivisione del contesto è solo per le app desktop a thread singolo.
Elisabeth,

10
Che dire della condivisione del contesto per una richiesta. Quindi per una richiesta possiamo avere accesso a repository diversi ed effettuare una transazione attraverso di essi condividendo lo stesso contesto?
Lyubomir Velchev

16

Una cosa che non è stata realmente affrontata nella domanda o nella discussione è il fatto che DbContext non può annullare le modifiche. È possibile inviare le modifiche, ma non è possibile cancellare l'albero delle modifiche, quindi se si utilizza un contesto per richiesta, si è sfortunati se è necessario eliminare le modifiche per qualsiasi motivo.

Personalmente creo istanze di DbContext quando necessario, di solito collegato a componenti aziendali che hanno la possibilità di ricreare il contesto, se necessario. In questo modo ho il controllo del processo, piuttosto che avere una singola istanza forzata su di me. Inoltre, non devo creare DbContext all'avvio di ogni controller, indipendentemente dal fatto che venga effettivamente utilizzato. Quindi, se voglio ancora avere istanze per richiesta, posso crearle nel CTOR (tramite DI o manualmente) o crearle secondo necessità in ciascun metodo del controller. Personalmente di solito seguo quest'ultimo approccio per evitare di creare istanze DbContext quando non sono effettivamente necessarie.

Dipende da quale angolo lo guardi anche tu. Per me l'istanza per richiesta non ha mai avuto senso. DbContext appartiene davvero alla richiesta HTTP? In termini di comportamento questo è il posto sbagliato. I tuoi componenti aziendali dovrebbero creare il tuo contesto, non la richiesta Http. Quindi è possibile creare o eliminare i componenti aziendali in base alle esigenze e non preoccuparsi della durata del contesto.


1
Questa è una risposta interessante e sono parzialmente d'accordo con te. Per me, un DbContext non deve essere associato a una richiesta Web, ma viene sempre digitato in una singola "richiesta" come in: "transazione commerciale". E quando si collega il contesto a una transazione commerciale, la cancellazione delle modifiche diventa davvero strana da fare. Ma non averlo sul limite delle richieste Web non significa che i componenti aziendali (BC) dovrebbero creare il contesto; Penso che non sia una loro responsabilità. Invece, puoi applicare l'ambito usando i decoratori intorno ai tuoi BC. In questo modo è anche possibile modificare l'ambito senza alcuna modifica del codice.
Steven

1
Bene, in quel caso l'iniezione nell'oggetto business dovrebbe occuparsi della gestione della vita. A mio avviso l'oggetto business possiede il contesto e come tale dovrebbe controllare la durata.
Rick Strahl,

In breve, cosa intendi quando dici "la capacità di ricreare il contesto, se necessario"? stai lanciando la tua abilità di rollback? puoi elaborare un po '?
tntwyckoff,

2
Personalmente, penso che sia un po 'problematico forzare un DbContext all'inizio lì. Non vi è alcuna garanzia che sia necessario accedere al database. Forse stai chiamando un servizio di terze parti che cambia stato da quel lato. O forse hai effettivamente 2 o 3 database con cui stai lavorando contemporaneamente. All'inizio non creeresti un mucchio di DbContexts nel caso finissi per usarli. L'azienda conosce i dati con cui lavora, quindi appartiene a quello. Basta mettere un TransactionScope all'inizio se è necessario. Non credo che tutte le chiamate ne abbiano bisogno. Ci vogliono risorse.
Daniel Lorenz,

Questa è la domanda se si consente al contenitore di controllare la durata del dbcontext che quindi controlla la durata dei controlli principali, a volte indebitamente. Dire se desidero iniettare un semplice servizio singleton nei miei controller, quindi non sarò in grado di utilizzare l'iniezione del costatore a causa della semantica della richiesta.
davidcarr,

10

Sono d'accordo con le opinioni precedenti. È bene dire che se hai intenzione di condividere DbContext nell'app a thread singolo, avrai bisogno di più memoria. Ad esempio, la mia applicazione Web su Azure (un'istanza extra piccola) richiede altri 150 MB di memoria e ho circa 30 utenti all'ora. Applicazione che condivide DBContext nella richiesta HTTP

Ecco un'immagine di esempio reale: l'applicazione è stata distribuita in 12PM


Forse l'idea è quella di condividere il contesto per una richiesta. Se accediamo a repository diversi e - classi DBSet e vogliamo che le operazioni con esse siano transazionali, questa dovrebbe essere una buona soluzione. Dai un'occhiata al progetto open source mvcforum.com Penso che sia stato realizzato nella loro implementazione del modello di progettazione di Unit Of Work.
Lyubomir Velchev

3

Quello che mi piace è che allinea l'unità di lavoro (come la vede l'utente, ovvero l'invio di una pagina) con l'unità di lavoro in senso ORM.

Pertanto, è possibile rendere l'intero invio della pagina transazionale, cosa che non si potrebbe fare se si esponessero i metodi CRUD con ognuno di essi creando un nuovo contesto.


3

Un altro motivo per non utilizzare un DbContext singleton, anche in un'applicazione a singolo utente con thread singolo, è dovuto al modello di mappa di identità che utilizza. Ciò significa che ogni volta che recuperi i dati utilizzando query o per ID, manterrà le istanze dell'entità recuperate nella cache. La prossima volta che recupererai la stessa entità, ti fornirà l'istanza memorizzata nella cache dell'entità, se disponibile, con le eventuali modifiche apportate nella stessa sessione. Ciò è necessario in modo che il metodo SaveChanges non finisca con più istanze di entità diverse dello stesso record di database; in caso contrario, il contesto dovrebbe in qualche modo unire i dati di tutte quelle istanze di entità.

La ragione per cui è un problema è che un DbContext singleton può diventare una bomba a tempo che alla fine potrebbe memorizzare nella cache l'intero database + l'overhead degli oggetti .NET in memoria.

Esistono modi per aggirare questo comportamento utilizzando solo query Linq con il .NoTracking()metodo di estensione. Anche in questi giorni i PC hanno molta RAM. Ma di solito non è questo il comportamento desiderato.


Questo è corretto, ma devi presumere che Garbage Collector funzionerà, rendendo questo problema più virtuale che reale.
tocqueville,

3
Il Garbage Collector non raccoglierà alcuna istanza di oggetto trattenuta da un oggetto statico / singleton attivo. Finiranno nella seconda generazione dell'heap.
Dmitry S.

1

Un altro problema a cui prestare attenzione in particolare con Entity Framework è quando si utilizza una combinazione di creazione di nuove entità, caricamento lento e utilizzo di tali nuove entità (dallo stesso contesto). Se non usi IDbSet.Create (rispetto solo a una novità), il caricamento Lazy su quell'entità non funziona quando viene recuperato dal contesto in cui è stato creato. Esempio:

 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.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.