Está en la página 1de 35

Mehdi El Gueddari

London-based System Architect and senior .NET / iOS developer with a sweet spot for backend work
and distributed systems. Available for contract work.

home
github
linkedin
contact

Managing DbContext the right way with Entity


Framework 6: an in-depth guide
Posted on 07 Aug 2014 by Mehdi El Gueddari

UPDATE:thesourcecodefor DbContextScope isnowavailableonGitHub:DbContextScope


onGitHub.

A bit of context

Thisisn'tthefirstpostthathasbeenwrittenaboutmanagingthe DbContext lifetimeinEntity


Frameworkbasedapplications.Infact,thereisnoshortageofarticlesdiscussingthistopic.

Formanyapplications,thesolutionspresentedinthosearticles(whichgenerallyrevolve
aroundusingaDIcontainertoinject DbContext instanceswithaPerWebRequestlifetime)will
workjustfine.Theyalsohavethemeritofbeingverysimpleatleastatfirstsight.
Forcertaintypesofapplicationshowever,theinherentlimitationsoftheseapproachespose
problems.Tothepointthatcertainfeaturesbecomeimpossibletoimplementorrequireto
resorttoincreasinglycomplexstructuresorincreasinglyuglyhackstoworkaroundthewaythe
DbContext instancesarecreatedandmanaged.

Hereisforexampleanoverviewoftherealworldapplicationthatpromptedmetorethinkthe
waywemanagedour DbContext instances:

TheapplicationiscomprisedofmultiplewebapplicationsbuiltwithASP.NETMVCand
WebAPI.Italsoincludesmanybackgroundservicesimplementedasconsoleappsand
WindowsServices,includingahomegrowntaskschedulerserviceandmultipleservices
thatprocessmessagesfromMSMQandRabbitMQqueues.MostofthearticlesIlinkedto
abovemaketheassumptionthatallserviceswillexecutewithinthecontextofaweb
request.Thisisnotthecasehere.
Itstoresandreadsdatato/frommultipledatabases,includingamaindatabase,a
secondarydatabase,areportingdatabaseandaloggingdatabase.Itsdomainmodelis
separatedintoseveralindependentgroups,eachwiththeirown DbContext type.Any
approachassumingasingle DbContext typewon'tworkhere.
ItreliesheavilyonthirdpartyremoteAPIs,suchastheFacebook,TwitterorLinkedIn
APIs.Thesearen'ttransactional.Manyuseractionsrequireustomakemultipleremote
APIcallsbeforewecanreturnaresulttotheuser.ManyofthearticlesIlinkedtomake
theassumptionthat"1webrequest=1businesstransaction"thateithergetscommitted
orrolledbackinanatomicmanner(hencetheideaofusingaPerWebRequestscope
DbContext instance).Thisclearlydoesn'tapplyhere.JustbecauseoneremoteAPIcall
faileddoesn'tmeanthatwecanautomagically"rollback"theresultsofanyremoteAPI
callwemaybedonepriortothefailedone(e.g.whenyou'veusedtheFacebookAPIto
postastatusupdateonFacebook,youcan'trollitbackevenifthatoperationwaspartofa
wideruseractionthateventuallyfailedasawhole).Sointhisapplication,auseraction
willoftenrequireustoexecutemultiplebusinesstransactions,whichmustbe
independentlypersisted.(youmayarguethattheremightbewaystoredesignthewhole
systemtoavoidfindingourselvesinthissortofsituation.Andmaybethereare.Butthat's
howtheapplicationwasoriginallydesigned,itworksverywellandthat'swhatwehaveto
workwith).
Manyservicesareheavilyparallelized,eitherbytakingadvantageofasyncI/Oor(more
often)bysimplydistributingtasksacrossmultiplethreadsviatheTPL's Task.Run() or
Parallel.Invoke() methods.Sothewaywemanageour DbContext instancesmustplay
wellwithmultithreadingandparallelprogrammingingeneral.Mostofthecommon
approachessuggestedtomanage DbContext instancesdon'tworkatallinthisscenario.
Inthispost,I'llgoindepthintothevariousmovingpartsthatareinvolvedin DbContext
lifetimemanagement.We'lllookattheprosandconsofseveralstrategiescommonlyusedto
solvethisproblem.Finally,we'lllookindetailsatonestrategy(amongothers)tomanagethe
DbContext lifetimethataddressesallthechallengespresentedaboveandthatshouldworkfor
mostapplicationsregardlessoftheircomplexity.

Thereisofcoursenosuchthingasonesizefitsall.Butbytheendofthispost,youshouldhave
allthetoolsandknowledgeyouneedtomakeaninformeddecisionforyourspecific
application.

Likemostpostsonthisblog,thispostisonthelonganddetailedside.Itmighttakeawhileto
readanddigest.ForanEntityFrameworkbasedapplication,thestrategyyouchoosetouseto
managethelifetimeofthe DbContext willbeoneofthemostimportantdecisionsyoumake.It
willhaveamajorimpactonthecorrectness,maintainabilityandscalabilityofyourapplication.
Soit'swellworthtakingsometimetochooseyourstrategycarefullyandnotrushintoit.

A note on terminology

Inthispost,I'lloftenrefertotheterm"services".WhatImeanbythatisnotremoteservices
(RESTorotherwise).Instead,whatI'mreferringtoiswhatisoftencalledServiceObjects.That
is:theplacewhereyourbusinesslogicisimplementedtheobjectsresponsibleforexecuting
yourbusinessrulesanddefiningyourbusinesstransactionboundaries.

Ofcourse,dependingonthedesignpatternsthatwereusedtocreatethearchitectureofyour
application(anddependingontheimaginationofwhoeverdesigneditsoftwaredevelopers
areanimaginativebunch),yourcodebasemightbeusingdifferentnamesforthis.SowhatI
calla"service"mightverywellbecalleda"workflow",an"orchestrator",an"executor",an
"interactor",a"command",a"handler"oravarietyofothernamesinyourapplication.

Nottomentionthatmanyapplicationdon'thaveawelldefinedplacewherebusinesslogicis
implementedandrelyinsteadonimplementing(andoftenduplicating)businesslogiconan
adhocbasiswhereandwhenneeded,e.g.incontrollersinanMVCapplication.

Butnoneofthismattersforthisdiscussion.WheneverIsay"service",read:"theplacethat
implementsthebusinesslogic",beitarandomcontrollermethodorawelldefinedservice
classinaseparateservicelayer.

Key points to consider


Whencomingupwithorevaluatinga DbContext lifetimemanagementstrategy,it'simportant
tokeepinmindthekeyscenariosandfunctionalitiesthatitmustsupport.

HereareafewpointsthatIwouldconsidertobeessentialformostapplications.

Your services must be in control of the business transaction boundary (but not
necessarily in control of the DbContext instance lifetime)

Perhapsthemainsourceofconfusionwhenitcomestomanaging DbContext instancesis


understandingthedifferencebetweenthelifetimeofa DbContext instanceandthelifetimeofa
businesstransactionandhowtheyrelate.

DbContext implementstheUnitofWorkpattern:

Maintainsalistofobjectsaffectedbyabusinesstransactionandcoordinatesthewriting
outofchangesandtheresolutionofconcurrencyproblems.

Inpractice,asyouusea DbContext instancetoload,update,addanddeletepersistententities,


theinstancekeepstrackofthosechangesinmemory.Itdoesn'thoweverpersistthosechanges
totheunderlyingdatabaseuntilyoucallits SaveChanges() method.

Aservicemethod,asdefinedabove,isresponsiblefordefiningtheboundaryofabusiness
transaction.

Thepracticalconsequenceofthisisthat:

Aservicemethodmustusethesame DbContext instancethroughoutthedurationofa


businesstransaction.Thisissothatallthechangesmadetoyourpersistentmodelare
trackedandeithercommittedtotheunderlyingdatastoreorrolledbackinanatomic
manner.
Yourservicesmustbethesolecomponentsinyourapplicationresponsibleforcallingthe
DbContext.SaveChanges() methodattheendofabusinesstransaction.Shouldotherparts
oftheapplicationcallthe SaveChanges() method(e.g.repositorymethods),youwillend
upwithpartiallycommittedchanges,leavingyourdatainaninconsistentstate.
The SaveChanges() methodmustbecalledexactlyonceattheendofeachbusiness
transaction.Inadvertentlycallingthismethodinthemiddleofabusinesstransactionmay
leavethesystemwithinconsistent,partiallycommittedchanges.

A DbContext instancecanhoweverspanacrossmultiple(sequential)businesstransactions.
Onceabusinesstransactionhascompletedandhascalledthe DbContext.SaveChanges()
methodtopersistallthechangesitmade,it'sentirelypossibletojustreusethesame
DbContext instanceforthenextbusinesstransaction.

I.e.thelifetimeofa DbContext instanceisnotnecessarilyboundtothelifetimeofasingle


businesstransaction.

Pros and cons of managing the DbContext instance lifetime independently of the
business transaction lifetime.

Example

Averycommonscenariowherethelifetimeofthe DbContext instancecanbemaintained


independentlyfromthelifetimeofbusinesstransactionsisinthecaseofwebapplications.It's
quitecommontoauseaconfigurationwherea DbContext instanceiscreatedatthebeginning
ofeachwebrequest,usedbyalltheservicesinvokedduringtheexecutionofthewebrequest
andeventuallydisposedofattheendoftherequest.

Pros

Therearetwomainreasonswhyyouwouldwanttodecouplethelifetimeofthe DbContext
instancefromthebusinesstransactionlifetime.

Possibleperformancegains.Each DbContext instancemaintainsafirstlevelcacheof


alltheentitiesitsloadsfromthedatabase.Wheneveryouqueryanentitybyitsprimary
key,the DbContext willfirstattempttoretrieveitfromitsfirstlevelcachebefore
defaultingtoqueryingitfromthedatabase.Dependingonyourdataquerypattern,re
usingthesame DbContext acrossmultiplesequentialbusinesstransactionsmayresultin
afewerdatabasequeriesbeingmadethankstothe DbContext firstlevelcache.
Itenableslazyloading.Ifyourservicesreturnpersistententities(asopposedto
returningviewmodelsorothersortsofDTOs)andyou'dliketotakeadvantageoflazy
loadingonthoseentities,thelifetimeofthe DbContext instancefromwhichthoseentities
wereretrievedmustextendbeyondthescopeofthebusinesstransaction.Iftheservice
methoddisposedthe DbContext instanceitusedbeforereturning,anyattempttolazy
loadpropertiesonthereturnedentitieswouldfail(whetherornotusinglazyloadingisa
goodideaisadifferentdebatealtogetherwhichwewon'tgetintohere).Inourweb
applicationexample,lazyloadingwouldtypicallybeusedincontrolleractionmethodson
entitiesreturnedbyaseparateservicelayer.Inthatcase,the DbContext instancethatwas
usedbytheservicemethodtoloadtheseentitieswouldneedtoremainaliveforthe
durationofthewebrequest(orattheveryleastuntiltheactionmethodhascompleted).
Issues with keeping the DbContext alive beyond the scope of a business transaction

Whileitcanbefinetoreusea DbContext acrossmultiplebusinesstransactions,itslifetime


shouldstillbekeptshort.Itsfirstlevelcachewillbecomeeventuallybecomestale,whichwill
leadtoconcurrencyissues.Ifyourapplicationusesoptimisticconcurrencythiswillresultin
businesstransactionsfailingwitha DbUpdateConcurrencyException .Usinganinstanceperweb
requestlifetimeforyour DbContext inwebappswillusuallybefineasawebrequestisshort
livedbynature.Butusinganinstanceperformlifetimeinadesktopapplication,whichyou'll
oftenfindsuggested,isalotmorequestionableandrequirescarefulthoughtbeforebeing
adopted.

Notethatyoucan'treusethesame DbContext instanceacrossmultiplebusinesstransactionsif


yourelyonpessimisticconcurrency.Correctlyimplementingpessimisticconcurrencyinvolves
keepingadatabasetransactionwiththecorrectisolationlevelopenforthewholelifetimeofa
DbContext instance,whichwouldpreventcommittingorrollingbackindividualbusiness
transactionsindependently.

Reusingthesame DbContext instanceformorethanonebusinesstransactioncanalsoleadto


disastrousbugswhereaservicemethodaccidentlycommitsthechangesfromapreviously
failedbusinesstransaction.

Finally,managingyour DbContext instancelifetimeoutsideofyourservicestendstotieyour


applicationtoaspecificinfrastructure,makingitalotlessflexibleandmuchmoredifficultto
evolveandmaintaininthelongrun.

Forexample,foranapplicationthatstartsoffasasimplewebapplicationandreliesan
instanceperwebrequeststrategytomanagethelifetimeofits DbContext instances,it'seasyto
fallintothetrapofrelyingonlazyloadingincontrollersorviewsoronpassingpersistent
entitiesacrossservicemethodsontheassumptionthattheywillallusethesame DbContext
instancebehindthescenes.Whentheneedtointroducemultithreadingormoveoperationsto
backgroundWindowsServicesinevitablyarises,thiscarefullyconstructedsandcastleoften
collapsesastherearenomorewebrequeststobind DbContext instancesto.

Asaresult,it'sadvisabletoavoidmanagingthelifetimeof DbContext instancesseparatelyfrom


businesstransactions.Instead,eachservicemethod(i.e.eachbusinesstransaction)should
createitsown DbContext instanceanddisposeitattheendofthebusinesstransaction(i.e.
beforereturning).

Thisprecludesusinglazyloadingoutsideofservices(whichcanbeaddressedbymodelingyour
domainusingDDDorbygettingservicestoreturnDTOsinsteadofpersistententities)and
posesafewotherconstraints(e.g.youshouldn'tpasspersistententitiesintoaservicemethod
astheywon'tbeattachedtothe DbContext instancethattheservicewilluse).Butitbringsalot
oflongtermbenefitsfortheflexibilityandmaintenanceoftheapplication.

Your services must be in control of the database transaction scope and isolation
level

IfyourapplicationworksagainstanRDMSthatprovidesACIDpropertiesforitstransactions
(andifyou'reusingEntityFramework,youalmostcertainlyare),it'sessentialforyourservices
tobeincontrolofthedatabasetransactionscopeandisolationlevel.Youcan'twritecorrect
codeotherwise.

Aswe'llseelater,EntityFrameworkwrapsallwriteoperationswithinanexplicitdatabase
transactionbydefault.CoupledwithaREADCOMMITTEDisolationlevelthedefaultonSQL
Serverthissuitstheneedsofmostbusinesstransactions.Thisisespeciallythecaseifyourely
onoptimisticconcurrencytodetectandavoidconflictingupdates.

Mostapplicationshoweverwillstilloccasionallyneedtouseotherisolationlevelsforspecific
operations.

It'sverycommonforexampletoexecutereportingquerieswhereyouhavedeterminedthat
dirtyreadsaren'tanissueunderaREADUNCOMMITTEDisolationlevelinordertoeliminate
lockcontentionwithotherqueries(althoughifyourenvironmentallowsit,you'llprobablywant
touseREADCOMMITTEDSNAPSHOTinstead).

AndsomebusinessrulesmightrequiretheusetheREPEATABLEREADoreven
SERIALIZABLEisolationlevels(especiallyifyourapplicationusespessimisticconcurrency
control).Inwhichcasetheservicewillneedtohaveexplicitcontroloverthetransactionscope.

The way your DbContext is managed should be independent of the architecture of


the application

Thearchitectureofasoftwaresystemandthedesignpatternsitreliesonalwaysevolveover
timetoadapttonewconstraints,businessrequirementsandincreasingload.

Youdon'twantthestrategyyouchoosetomanagethelifetimeofyour DbContext totieyoutoa


specificarchitectureandpreventyoufrombeingabletoevolveitasandwhenneeded.

The way your DbContext is managed should be independent of the application type
Whilemostapplicationstodaystartoffaswebapplications,thestrategyyouchoosetomanage
thelifetimeofyour DbContext shouldn'tassumethatyourservicemethodwillbecalledfrom
withinthecontextawebrequest.Moregenerally,yourservicelayer(ifyouhaveone)shouldbe
independentofthetypeofapplicationit'susedfrom.

Itwon'tbelonguntilyouneedtocreatecommandlineutilitiesforyoursupportteamto
executeadhocmaintenancetasksorWindowsServicestohandlescheduledtasksandlong
runningbackgroundoperations.Whenthishappens,youwanttobeabletoreferencethe
assemblythatcontainsyourservicesandjustuseanyserviceyouneedfromyourconsoleor
WindowsServiceapplication.Youmostdefinitelydon'twanttohavetocompletelyreengineer
thewayyour DbContext instancesaremanagedjusttobeabletouseyourservicesfroma
differenttypeofapplication.

Your DbContext management strategy should support multiple DbContext-derived


types

Ifyourapplicationneedstoconnecttomultipledatabases(forexampleifitusesseparate
reporting,loggingand/orauditingdatabases)orifyouhavesplityourdomainmodelinto
multipleaggregategroups,youwillhavetomanagemultiple DbContext derivedtypes.

ForthosecomingfromanNHibernatebackground,thisistheequivalentofhavingtomanage
multiple SessionFactory instances.

Whateverstrategyyouchooseshouldbeabletoletservicesusetheappropriate DbContext for


theirneed.

Your DbContext management strategy should work with EF6's async work ow

In.NET4.5,ADO.NETintroduced(atverylonglast)supportforasyncdatabasequeries.Async
supportwasthenincludedinEntityFramework6,allowingyoutouseafullyasyncworkflow
forallreadandwritequeriesmadethroughEF.

Needlesstosaythatwhateversystemyouusetomanageyour DbContext instancemustplay


wellwithEntityFramework'sasyncfeatures.

DbContext 's default behaviour

Ingeneral, DbContext 'sdefaultbehaviourcanbedescribedas:"doestherightthingbydefault".


ThereareseveralkeybehavioursofEntityFrameworkyoushouldalwayskeepinmind
however.ThislistdocumentsEF'sbehaviourwhenworkingagainstSQLServer.Theremightbe
differenceswhenusingotherdatastores.

DbContext is not thread-safe

Youmustneveraccessyour DbContext derivedinstancefrommultiplethreadssimultaneously.


Thismightresultonmultiplequeriesbeingsentconcurrentlyoverthesamedatabase
connection.Itwillalsocorruptthefirstlevelcachethat DbContext maintainstoofferits
IdentityMap,changetrackingandUnitofWorkfunctionalities.

Inamultithreadedapplication,youmustcreateanduseaseparateinstanceofyour
DbContext derivedclassineachthread.

Soif DbContext isn'tthreadsafe,howcanitsupporttheasyncqueryfeaturesintroducedwith


EF6?Simplybypreventingmorethanoneasyncoperationbeingexecutedatanygiventime(as
documentedintheEntityFrameworkspecificationsforitsasyncpatternsupport).Ifyou
attempttoexecutemultipleactionsonthesame DbContext instanceinparallel,forexampleby
kickingoffmultipleSELECTqueriesinparallelviathethe DbSet<T>.ToListAsync() method,
youwillgeta NotSupportedException withthefollowingmessage:

Asecondoperationstartedonthiscontextbeforeapreviousasynchronous
operationcompleted.Use'await'toensurethatanyasynchronousoperationshave
completedbeforecallinganothermethodonthiscontext.Anyinstancemembersare
notguaranteedtobethreadsafe.

EntityFramework'sasyncfeaturesaretheretosupportanasynchronousprogrammingmodel,
nottoenableparallelism.

Changes are only persisted when SaveChanges() is called

Anychangesmadetoyourentities,beitupdates,insertsordeletes,areonlypersistedtothe
databasewhenthe DbContext.SaveChanges() methodiscalled.Ifa DbContext instanceis
disposedbeforeits SaveChanges() methodwascalled,noneoftheinserts,updatesordeletes
donethroughthis DbContext willbepersistedtotheunderlyingdatastore.

ThecanonicalmannertoimplementabusinesstransactionwithEntityFrameworkis
therefore:
using(varcontext=newMyDbContext(ConnectionString))
{
/*
*Businesslogichere.Add,update,deletedata
*throughthe'context'.
*
*Throwincaseofanyerrortorollbackall
*changes.
*
*DonotcallSaveChanges()untilthebusiness
*transactioniscompletei.e.nopartialor
*intermediatesaves.SaveChanges()mustbe
*calledexactlyonceperbusinesstransaction.
*
*IfyoufindyourselfneedingtocallSaveChanges()
*multipletimeswithinabusinesstransaction,itmeans

A side note for NHibernate veterans

Ifyou'recomingfromanNHibernatebackground,thewayEntityFrameworkpersistschanges
tothedatabaseisoneofthemajordifferencesbetweenEFandNHibernate.

InNHibernate,the Session operatesbydefaultinAutoFlushmode.Inthismode,the Session


willautomaticallypersistsallchangesmadetoentitiestothedatabasebeforeexecutingany
'select'query,ensuringconsistencybetweenthepersistedentitiesandtheirinmemorystate
withinthecontextofa Session .EntityFramework'sdefaultbehaviouristheequivalentof
setting Session.FlushMode to Never inNHibernate.

ThisEFbehaviourcanresultinsubtlebugsasitispossibletobeinasituationwherequeries
mayunexpectedlyreturnstaleorincorrectdata.Thiswouldn'tbepossiblewithNHibernate's
defaultbehaviour.Ontheotherside,itdramaticallysimplifiestheissueofdatabasetransaction
lifetimemanagement.

OneofthetrickiestissueinNHibernateistocorrectlymanagethedatabasetransaction
lifetime.SinceNHibernate's Session canpersistsoutstandingchangestothedatabase
automaticallyatanytimethroughoutitslifetimeandmaydosomultipletimeswithinasingle
businesstransaction,thereisnosingle,welldefinedpointormethodwheretostartthe
databasetransactiontoensurethatallchangesareeithercommittedorrolledbackinan
atomicmanner.
Theonlyreliablemethodtocorrectlymanagethedatabasetransactionlifetimewith
NHibernateistowrapallyourservicemethodsinanexplicitdatabasetransaction.Thisiswhat
you'llseedoneinprettymucheveryNHibernatebasedapplication.

Asideeffectofthisapproachisthatitrequireskeepingadatabaseconnectionandtransaction
openforoftenlongerthanstrictlynecessary.Itthereforeincreasesdatabaselockcontention
andtheprobabilityofdatabasedeadlocksoccurring.It'salsoveryeasyforadeveloperto
inadvertentlyexecutealongrunningcomputationoraremoteservicecallwithoutrealizingor
evenknowingthatthey'rewithinthecontextofanopendatabasetransaction.

WiththeEFapproach,onlythe SaveChanges() methodmustbewrappedinanexplicit


databasetransaction(unlessyouneedaREPEATABLEREADorSERIALIZABLEisolation
levelofcourse),ensuringthatthedatabaseconnectionandtransactionarekeptasshortlived
aspossible.

Reads are executed within an AutoCommit transaction

DbContext doesn'tstartexplicitdatabasetransactionsforreadqueries.ItinsteadreliesonSQL
Server'sAutocommitTransactions(orImplicitTransactionsifyou'veenabledthembutthat
wouldbearelativelyunusualsetup).Autocommit(orImplicit)transactionswillusewhatever
defaulttransactionisolationlevelthedatabaseenginehasbeenconfiguredtouse(READ
COMMITTEDbydefaultforSQLServer).

Ifyou'vebeenaroundtheblockforawhile,andparticularlyifyou'veusedNHibernatebefore,
youmayhaveheardthatAutoCommit(orImplicit)transactionsarebad.Andindeed,relying
onAutocommittransactionsforwritescanhaveadisastrousimpactonperformance.

Thestoryisverydifferentforreadshowever.AsyoucanseebyyourselfbyrunningtheSQL
scriptbelow,neitherAutocommitnorImplicittransactionshaveanysignificantperformance
impactfor SELECT statements.
/*
*Execute100,000SELECTqueriesunderautocommit,
*implicitandexplicitdatabasetransactions.
*
*Thesescriptsassumesthatthedatabasetheyare
*runningagainstcontainsaUserstablewithan'Id'
*columnofdatatypeINT.
*
*IfrunningfromSQLServerManagementStudio,
*rightclickinthequerywindow,goto
*QueryOptions>Resultsandtick"Discardresults
*afterexecution".Otherwise,whatyou'llbemeasuring
*willbetheResultGridredrawingperformanceandnot
*thequeryexecutiontime.
*/

Obviously,ifyouneedtouseanisolationlevelhigherthanthedefaultREADCOMMITTED,all
readswillneedtobepartofanexplicitdatabasetransaction.Inthatcase,youwillhavetostart
thetransactionyourselfEFwillnotdothisforyou.Butthiswouldtypicallyonlybedoneon
anadhocbasisforspecificbusinesstransactions.EntityFramework'sdefaultbehaviourshould
suitthevastmajorityofbusinesstransactions.

Writes are executed within an explicit transaction

EntityFrameworkautomaticallywrapsallthequeriesmadebythe DbContext.SaveChanges()
methodinasingleexplicitdatabasetransaction,thereforeensuringthatallthechangesapplied
tothecontextareeithercommittedorrolledbackinfull.

Itwillusewhateverdefaulttransactionisolationlevelthedatabaseenginehasbeenconfigured
touse(READCOMMITTEDbydefaultforSQLServer).

A side note for NHibernate veterans

ThisisanothermajordifferencebetweenEFandNHibernate.WithNHibernate,database
transactionsareentirelyinthehandsofdevelopers.NHibernate's Session willneverstartan
explicitdatabasetransactionautomatically.
You can override EF's default behaviour and control the database transaction scope
and isolation level

WithEntityFramework6,takingexplicitcontrolofthedatabasetransactionscopeand
isolationlevelisassimpleasitshouldbe:

using(varcontext=newMyDbContext(ConnectionString))
{
using(vartransaction=context.BeginTransaction(IsolationLevel.RepeatableRead
{
[...]
context.SaveChanges();
transaction.Commit();
}
}

Anobvioussideeffectofmanuallycontrollingthedatabasetransactionscopeisthatyouare
nowforcingthedatabaseconnectionandtransactiontoremainopenforthedurationofthe
transactionscope.

Youshouldbecarefultokeepthisscopeasshortlivedaspossible.Keepingadatabase
transactionrunningfortoolongcanhaveasignificantimpactonyourapplication's
performanceandscalability.Inparticular,it'sgenerallyagoodideatorefrainfromcalling
otherservicemethodswithinanexplicittransactionscopetheymightbeexecutinglong
runningoperationsunawarethattheyhavebeeninvokedwithinanopendatabasetransaction
scope.

There's no built-in way to override the default isolation level used for AutoCommit
and automatic explicit transactions

Asmentionedearlier,theAutoCommittransactionsEFreliesonforreadqueriesandthe
explicittransactionitautomaticallystartswhen SaveChanges() iscalledusewhateverdefault
isolationlevelthedatabaseenginehasbeenconfiguredwith.

There'sunfortunatelynobuiltinwaytooverridethisisolationlevel.Ifyou'dliketouseanother
isolationlevel,youmuststartandmanagethedatabasetransactionyourself.
The database connection open by DbContext will enroll in an ambient
TransactionScope

Alternatively,youcanalsousethe TransactionScope classtocontrolthetransactionscopeand


isolationlevel.ThedatabaseconnectionthatEntityFrameworkopenswillenrollintheambient
TransactionScope .

PriortoEF6,using TransactionScope wastheonlypracticalwaytocontrolthedatabase


transactionscopeandisolationlevel.

Inpractice,andunlessyouactuallyneedadistributedtransaction,youshouldavoidusing
TransactionScope . TransactionScope ,anddistributedtransactionsingeneral,arenot
necessaryformostapplicationsandtendtointroducemoreproblemsthantheysolve.EF's
documentationhasmoredetailsonworkingwith TransactionScope withEntityFrameworkif
youreallyneeddistributedtransactions.

DbContext instances should be disposed of (but you'll probably be OK if they're not)

DbContext implements IDisposable .Itsinstancesshouldthereforebedisposedofassoonas


they'renotneededanymore.

Inpracticehowever,andunlessyouchoosetoexplicitlymanagethedatabaseconnectionor
transactionthattheDbContextuses,notcalling DbContext.Dispose() won'tcauseanyissuesas
DiegoVega,aEFteammember,explains.

Thisisgoodnewsasalotofthecodeyou'llfindinthewildfailstodisposeof DbContext
instancesproperly.Thisisparticularlythecaseforcodethatattemptstomanage DbContext
instancelifetimesviaaDIcontainer,whichcanbealottrickierthanitsounds.

ADIcontainerlikeStructureMapforexampledoesn'tsupportdecommissioningthe
componentsitcreated.Asaresult,ifyourelyonStructureMaptocreateyour DbContext
instances,theywillneverbedisposedof,regardlessofwhatlifecycleyouchooseforthem.The
onlycorrectwaytomanagedisposablecomponentswithaDIcontainerlikethisisto
significantlycomplicateyourDIconfigurationandusenesteddependencyinjectioncontainers
asJeremyMillerdemonstrates.

Ambient DbContext vs Explicit DbContext vs Injected Dbcontext

Akeydecisionyou'llhavetomakeatthestartofanyEntityFrameworkbasedprojectishow
yourcodewillhandlepassingthe DbContext instancesdowntothemethod/layerthatwill
maketheactualdatabasequeries.

Aswe'veseenabove,theresponsibilityofcreatinganddisposingthe DbContext lieswiththe


toplevelservicemethods.Thedataaccesscode,i.e.thecodethatactuallyusesthe DbContext
instance,willhoweveroftenbemadeinaseparatepartofthecodebeitinaprivatemethod
deepdowntheserviceimplementation,inaqueryobjectorinaseparaterepositorylayer.

The DbContext instancethatthetoplevelservicemethodcreatesmustthereforesomehowfind


itswaydowntothesemethods.

Thereare3schoolofthoughtswhenitcomestomakingthe DbContext instanceavailabletothe


dataaccesscode:ambient,explicitorinjected.Eachapproachhasitsprosandcons,whichwe'll
examinenow.

Explicit DbContext

What it looks like

Withtheexplicit DbContext approach,thetoplevelservicemethodcreatesa DbContext


instanceandsimplypassesitdownthestackasamethodparameteruntilitfinallyreachesthe
methodthatimplementsthedataaccesspart.Inatraditional3tierarchitecturewithbotha
serviceandarepositorylayer,thiswouldlooklikethis:

publicclassUserService:IUserService
{
privatereadonlyIUserRepository_userRepository;

publicUserService(IUserRepositoryuserRepository)
{
if(userRepository==null)thrownewArgumentNullException("userRepository"
_userRepository=userRepository;
}

publicvoidMarkUserAsPremium(GuiduserId)
{
using(varcontext=newMyDbContext())
{
varuser=_userRepository.Get(context,userId);
user.IsPremiumUser=true;
(inthisintentionallycontrivedexample,therepositorylayerisofcoursecompletelypointless.
Inarealworkapplication,youwouldexpecttherepositorylayertobealotricher.Inaddition,
youcouldofcourseabstractyour DbContext behindan"IDbContext"ofsortsandcreateitvia
anabstractfactoryifyoureallydidn'twanttohavetohaveadirectdependencyonEntity
Frameworkinyourservices.Theprinciplewouldremainthesame).

The Good

Thisapproachisbyfarandawaythesimplestapproach.Itresultsincodethat'sveryeasyto
understandandmaintain,evenbydevelopersnewtothecodebase.

There'snomagicanywhere.The DbContext instancedoesn'tmaterializeoutofthinair.There's


aclearandobviousplacewherethecontextiscreated.Andit'sreallyeasytoclimbupthestack
andfinditifyou'rewonderingwhereaparticular DbContext instanceiscomingfrom.

The Bad

Themaindrawbackofthisapproachisthatitrequiresyoutopolluteallyourrepository
methods(ifyouhavearepositorylayer)aswellasmostofyourservicemethodswitha
mandatory DbContext parameter(orsomesortof IDbContext abstractionifyoudon'twantto
betiedtoaconcreteimplementationbutthepointstillstands).Youcouldseethisasbeinga
sortofMethodInjectionpattern.

Thatyourrepositorymethodsrequiretobeprovidedwithanexplicit DbContext parameterisn't


toomuchofanissue.Infact,itcanevenbeseenasagoodthingasitremovesanypotential
ambiguityastowhichcontextthey'llruntheirqueriesagainst.

Thingsarequitedifferentinyourservicelayerhowever.Chancesarethatmostofyourservice
methodswon'tusethe DbContext atall,particularlyifyou'veisolatedyourdataaccesscode
awayinqueryobjectsorinarepositorylayer.Asaresult,thesemethodswillonlyrequiretobe
providedwitha DbContext parametersothattheycanpassitdownthelineuntiliteventually
reacheswhatevermethodactuallyusesit.

Itcangetquiteugly.Particularlyifyourapplicationusesmultiple DbContext ,resultingin


servicemethodspotentiallyrequiringtwoormoremandatory DbContext parameters.Italso
muddiesyourmethodcontractsasyourservicemethodarenowforcedtoaskforaparameter
thattheyneitherneednorusebutrequirepurelytosatisfythedependencyofadownstream
method.
JonSkeetwroteaninterestingarticleonthetopicofexplicitnessvsambientbutcouldn'tcome
upwithagoodsolutioneither.

Nevertheless,thesimplicityandfoolproofnessofthisapproachishardtobeat.

Ambient DbContext

What it looks like

NHibernateuserswillbeveryfamiliarwiththisapproachastheambientcontextpatternisthe
predominantapproachusedintheNHibernateworldtomanageNH's Session (NHibernate's
equivalenttoEF's DbContext ).NHibernateevencomeswithbuiltinsupportforthispattern,
whichitcallscontextualsessions.

In.NETitself,thispatternisusedquiteextensively.You'veprobablyalreadyused
HttpContext.Current orthe TransactionScope class,bothofwhichrelyontheambientcontext
pattern.

Withthisapproach,thetoplevelservicemethodnotonlycreatesthe DbContext touseforthe


currentbusinesstransactionbutitalsoregistersitastheambient DbContext .Thedataaccess
codecanthenjustretrievetheambient DbContext wheneveritneedsit.Noneedtopassthe
DbContext instancearoundanymore.

AndersAbelhaswrittenasimpleimplementationofanambientDbContextthatreliesona
ThreadStatic variabletostoretheambient DbContext .Havealookthere'slesstoitthanit
sounds.

The Good

Theadvantagesofthisapproachareobvious.Yourserviceandrepositorymethodsarenowfree
of DbContext parameters,makingyourinterfacescleanerandyourmethodcontractscleareras
theycannowonlyrequesttheparametersthattheyactuallyneedtodotheirjob.Noneedto
pass DbContext instancesallovertheplaceanymore.

Aswiththeexplicitapproach,thecreationanddisposalofthe DbContext instanceisinaclear,


welldefinedandlogicalplace.

The Bad
Thisapproachdoeshoweverintroduceacertainamountofmagicwhichcancertainlymakethe
codemoredifficulttounderstandandmaintain.Whenlookingatthedataaccesscode,it'snot
necessarilyeasytofigureoutwheretheambient DbContext iscomingfrom.Youjusthaveto
hopethatsomeonesomehowregistereditbeforecallingthedataaccesscode.

Ifyourapplicationusesmultiple DbContext classes,e.g.ifitconnectstomultipledatabasesor


ifyouhavesplityourdomainmodelintoseparatemodelgroups,itcanbedifficultforthetop
levelservicemethodtoknowwhich DbContext object(s)itmustcreateandregister.Withthe
explicitapproach,thedataaccessmethodsrequiretoprovidedwithwhatever DbContext object
theyneedasamethodparameter.Thereisthereforenoambiguitypossible.Butwithan
ambientcontextapproach,thetoplevelservicemethodmustsomehowknowwhat DbContext
typethedownstreamdataaccesscodewillrequire.Therearewaystosolvethisissueinafairly
cleanmannerhoweveraswe'llseelater.

Finally,theambient DbContext exampleIlinkedtoaboveworksfineinasinglethreaded


model.ButifyouintendtouseEntityFramework'sasyncqueryfeature,thiswon'tfly.Afteran
asyncoperation,youwillmostlikelyfindyourselfinanotherthreadthantheonewherethe
DbContext wascreated.Inmanycases(althoughnotinallcasesthisiswhereasyncgets
tricky),itmeansthatyourambient DbContext willbegone.Thisisfixableaswellbutitwill
requiresomeadvancedunderstandingofhowmultithreading,theTPLandasyncworks
behindthescenesin.NET.We'llhavealookatthislaterinthispost.

Injected DbContext

What it looks like

Lastbutnotleast,theinjected DbContext approachisthemostoftenmentionedstrategyin


articlesandblogpostsaddressingtheissueofmanagingthe DbContext lifetime.

Withthisapproach,youletyourDIcontainermanagethelifetimeofyour DbContext andinject


itintowhatevercomponentneedsit(yourrepositoryobjectsforexample).

Thisiswhatitlookslike:
publicclassUserService:IUserService
{
privatereadonlyIUserRepository_userRepository;

publicUserService(IUserRepositoryuserRepository)
{
if(userRepository==null)thrownewArgumentNullException("userRepository"
_userRepository=userRepository;
}

publicvoidMarkUserAsPremium(GuiduserId)
{
varuser=_userRepository.Get(context,userId);
user.IsPremiumUser=true;
}
}

YouthenneedtoconfigureyourDIcontainertocreateaninstanceofthe DbContext withan


appropriatelifetimeonobjectgraphcreation.Acommonadviceyou'llfindistousea
PerWebRequestlifetimeforwebappsandPerFormlifetimefordesktopapps.

The Good

Theadvantagehereissimilartothatoftheambientapproach:thecodeisn'tlitteredwith
DbContext instancesbeingpassedallovertheplace.Thisapproachgoesonestepfurtherstill:
thereisno DbContext tobeseenanywhereintheservicecode.Theserviceiscompletely
obliviousofEntityFramework.Whichmightsoundgoodafirstsightbutquicklyleadstoalot
ofproblems.

The Bad

Despiteitspopularity,thisapproachhassignificantdrawbacksandlimitations.It'simportant
tounderstandthembeforeadoptingthisapproach.

A lot of magic

Thefirstissueisthatthisapproachreliesveryheavilyonmagic.Andwhenitcomesto
managingthecorrectnessandconsistencyofyourdatayourmostpreciousassetmagicisn't
awordyouwanttoheartoooften.
Wheredothese DbContext instancescomefrom?Howandwhereisthebusinesstransaction
boundarydefined?Ifaservicedependsontwodifferentrepositories,willtheybothhaveaccess
tothesame DbContext instanceorwilltheyeachhavetheirowninstance?

Ifyou'reabackenddeveloperworkingonaEFbasedproject,youmustknowtheanswersto
thesequestionsifyouwanttobeabletowritecorrectcode.

Theanswersherearen'tobviousandwillrequireyoutopourthroughyourDIcontainer
configurationcodetofindout.Andaswe'veseenearlier,gettingthisconfigurationrightisn'tas
trivialasitmayseematfirstsightandmayendupbeingfairlycomplexand/orsubtle.

Unclear business transaction boundaries

Perhapsthemostglaringissueinthecodesampleaboveis:whoisresponsibleforcommitting
changestothedatastore?I.e.whoiscallingthe DbContext.SaveChanges() method?It'sunclear.

Youcouldinjectthe DbContext intoyourserviceforthesolepurposeofcallingits


SaveChanges() method.Thatwouldberatherbafflingandveryerrorpronecode.Whywould
theservicemethodcall SaveChanges() onacontextobjectthatitneithercreatednorused?
Whatchangeswouldbesaved?

Alternatively,youcoulddefinea SaveChanges() methodonallyourrepositories,whichwould


justdelegatetotheunderlying DbContext .Theservicemethodwouldthenjustcall
SaveChanges() ontherepositoryitself.Thiswouldbeverymisleadingcode,asitwouldimply
thateachrepositoryimplementtheirownunitofworkandcanpersisttheirchanges
independentlyoftheotherrepositories.Whichwouldofcoursebeincorrectastheywouldin
factallusethesame DbContext instancebehindthescenes.

AnotherapproachsometimesseeninthewildistolettheDIcontainercall SaveChanges()
beforedecommissioningthe DbContext instance.Adisastrousapproachthatwouldmerita
blogpostofitsowntoexamine.

Inshort:theDIcontainerisaninfrastructurelevelcomponentithasnoknowledgeofthe
businesslogicthecomponentsitmanagesimplement.The DbContext.SaveChanges() method
ontheothersidedefinesabusinesstransactionboundaryi.e.it'sabusinesslogicconcern
(andacriticaloneatthat).Mixingthosetwounrelatedconcernstogetherwillquicklycausea
lotofpain.

Allthatbeingsaid,ifyousubscribetotheRepositoryisDeadmovement,theissueofdefining
whoiscalling DbContext.SaveChanges() shouldn'tariseasyourserviceswillusethe DbContext
instancedirectly.Theywillthereforebethenaturalplacefor SaveChanges() tobecalled.
Thereishoweveranumberofotherissuesyouwillrunintowithaninjected DbContext
regardlessofthearchitecturalstyleofyourapplication.

Forces your services to become stateful

Anotableoneisthat DbContext isn'taservice.It'saresource.AndaDisposableonetoboot.By


injectingitintowhateverlayerimplementyourdataaccess,you'remakingthatlayer,andby
extensionallthelayersabovewhichwouldbeprettymuchtheentireapplication,stateful.

It'snottheendoftheworldbutitcertainlycomplicatesDIcontainerconfiguration.Having
statelessservicesprovidestremendousflexibilityandmakestheconfigurationoftheirlifetime
anonissue(anylifetimewoulddoandsingletonisoftenyourbestbet).Assoonasyou
introducestatefulservices,carefulconsiderationhastobegiventoyourservicelifetimes.

Itoftenstartsoffeasy(PerWebRequestorTransientlifetimeforeverythingwhichsuitsa
simplewebappwell)andthendescendsintomorecomplexityasconsoleapps,Windows
Servicesandothersinevitablymaketheirappearance.

Prevents multi-threading

Anotherissue(relatedtothepreviousone)thatwillinevitablybiteyouquitehardisthatan
injected DbContext preventsyoufrombeingabletointroducemultithreadingoranysortof
parallelexecutionflowsinyourservices.

Rememberthat DbContext (justlike Session inNHibernate)isn'tthreadsafe.Ifyouneedto


executemultipletasksinparallelinaservice,youmustmakesurethateachtaskworksagainst
itsown DbContext instanceorthewholethingwillblowupatruntime.Thisisimpossibletodo
withtheinjectedDbContextapproachsincetheserviceisn'tincontrolofthe DbContext
instancecreationanddoesn'thaveanywaytocreatenewones.

Howcanyoufixthis?Noteasily.

YourfirstinstinctisprobablytochangeyourservicestodependonaDbContextfactoryinstead
ofdependingdirectlyonaDbContext.Thatwouldallowthemtocreatetheirown DbContext
instanceswhenneeded.Butthatwouldeffectivelydefeatthewholepointoftheinjected
DbContext approach.IfservicescreatetheirownDbContextinstancesviaafactory,these
instancescan'tbeinjectedanymore.Whichmeansthatserviceswillhavetoexplicitlypass
those DbContext instancesdownthelayerstowhatevercomponentsneedthem(e.g.the
repositories).Soyou'reeffectivelybacktotheexplicitDbContextapproachdiscussedearlier.I
canthinkofafewwaysinwhichthiscouldbesolvedbutallofthemfeelmorelikehacksthan
cleanandelegantsolutions.

Anotherwaytoapproachtheissuewouldbetoaddafewmorelayersofcomplexity,introducea
queuingmiddlewarelikeRabbitMQandletitdistributetheworkloadforyou.Whichmayor
maynotworkdependingonwhyyouneedtointroduceparallelism.Butinanycase,youmay
neitherneednorwanttheadditionaloverheadandcomplexity.

Withaninjected DbContext ,you'resimplybetterofflimitingyourselftosinglethreadedcode


oratleasttoasinglelogicalflowofexecution.Whichisperfectlyfineformanyapplicationsbut
itwillbecomeamajorlimitationincertaincases.

DbContextScope: a simple, correct and exible way to manage


DbContext instances

Timetolookatabetterwaytomanagethose DbContext instances.

Theapproachpresentedbelowrelieson DbContextScope ,acustomcomponentthatimplements


theambientDbContextapproachpresentedearlier.Thefullsourcecodefor DbContextScope
andtheclassesitdependsonisonGitHub.

Ifyou'refamiliarwiththe TransactionScope class,thenyoualreadyknowhowtousea


DbContextScope .They'reverysimilarinessencetheonlydifferenceisthat DbContextScope
createsandmanages DbContext instancesinsteadofdatabasetransactions.Butjustlike
TransactionScope , DbContextScope isambient,canbenested,canhaveitsnestingbehaviour
disabledandworksfinewithasyncexecutionflows.

Thisisthe DbContextScope interface:

publicinterfaceIDbContextScope:IDisposable
{
voidSaveChanges();
TaskSaveChangesAsync();

voidRefreshEntitiesInParentScope(IEnumerableentities);
TaskRefreshEntitiesInParentScopeAsync(IEnumerableentities);

IDbContextCollectionDbContexts{get;}
}
Thepurposeofa DbContextScope istocreateandmanagethe DbContext instancesusedwithin
acodeblock.A DbContextScope thereforeeffectivelydefinestheboundaryofabusiness
transaction.I'llexplainlaterwhyIdidn'tnameit"UnitOfWork"or"UnitOfWorkScope",which
wouldhavebeenamorecommonlyusedterminologyforthis.

Youcaninstantiatea DbContextScope directly.Oryoucantakeadependencyon


IDbContextScopeFactory ,whichprovidesconveniencemethodstocreatea DbContextScope with
themostcommonconfigurations:

publicinterfaceIDbContextScopeFactory
{
IDbContextScopeCreate(DbContextScopeOptionjoiningOption=DbContextScopeOption
IDbContextReadOnlyScopeCreateReadOnly(DbContextScopeOptionjoiningOption

IDbContextScopeCreateWithTransaction(IsolationLevelisolationLevel);
IDbContextReadOnlyScopeCreateReadOnlyWithTransaction(IsolationLevelisolationLevel

IDisposableSuppressAmbientContext();
}

Typical usage

With DbContextScope ,yourtypicalservicemethodwouldlooklikethis:

publicvoidMarkUserAsPremium(GuiduserId)
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
varuser=_userRepository.Get(userId);
user.IsPremiumUser=true;
dbContextScope.SaveChanges();
}
}
Withina DbContextScope ,youcanaccessthe DbContext instancesthatthescopemanagesin
twoways.Youcangetthemviathe DbContextScope.DbContexts propertylikethis:

publicvoidSomeServiceMethod(GuiduserId)
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
varuser=dbContextScope.DbContexts.Get<MyDbContext>.Set<User>.Find(userId
[...]
dbContextScope.SaveChanges();
}
}

Butthat'sofcourseonlyavailableinthemethodthatcreatedthe DbContextScope .Ifyouneed


toaccesstheambient DbContext instancesanywhereelse(e.g.inarepositoryclass),youcan
justtakeadependencyon IAmbientDbContextLocator ,whichyouwoulduselikethis:

publicclassUserRepository:IUserRepository
{
privatereadonlyIAmbientDbContextLocator_contextLocator;

publicUserRepository(IAmbientDbContextLocatorcontextLocator)
{
if(contextLocator==null)thrownewArgumentNullException("contextLocator"
_contextLocator=contextLocator;
}

publicUserGet(GuiduserId)
{
return_contextLocator.Get<MyDbContext>.Set<User>().Find(userId);
}
}

Those DbContext instancesarecreatedlazilyandthe DbContextScope keepstrackofthemto


ensurethatonlyoneinstanceofanygivenDbContexttypeisevercreatedwithinitsscope.
You'llnotethattheservicemethoddoesn'tneedtoknowwhichtypeof DbContext willbe
requiredduringthecourseofthebusinesstransaction.Itonlyneedstocreatea DbContextScope
andanycomponentthatneedstoaccessthedatabasewithinthatscopewillrequestthetypeof
DbContext theyneed.

Nesting scopes

A DbContextScope canofcoursebenested.Let'ssaythatyoualreadyhaveaservicemethodthat
canmarkauserasapremiumuserlikethis:

publicvoidMarkUserAsPremium(GuiduserId)
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
varuser=_userRepository.Get(userId);
user.IsPremiumUser=true;
dbContextScope.SaveChanges();
}
}

You'reimplementinganewfeaturethatrequiresbeingabletomarkagroupofusersas
premiumwithinasinglebusinesstransaction.Youcaneasilydoitlikethis:
publicvoidMarkGroupOfUsersAsPremium(IEnumerable<Guid>userIds)
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
foreach(varuserIdinuserIds)
{
//ThechildscopecreatedbyMarkUserAsPremium()will
//joinourscope.SoitwillreuseourDbContextinstance(s)
//andthecalltoSaveChanges()madeinthechildscopewill
//havenoeffect.
MarkUserAsPremium(userId);
}

//Changeswillonlybesavedhere,inthetoplevelscope,
//ensuringthatallthechangesareeithercommittedor
//rolledbackatomically.

(thiswouldofcoursebeaveryinefficientwaytoimplementthisparticularfeaturebutit
demonstratesthepoint)

Thismakescreatingaservicemethodthatcombinesthelogicofmultipleotherservicemethods
trivial.

Read-only scopes

Ifaservicemethodisreadonly,havingtocall SaveChanges() onits DbContextScope before


returningcanbeapain.Butnotcallingitisn'tanoptioneitheras:

1.Itwillmakecodereviewandmaintenancedifficult(didyouintendnottocall
SaveChanges() ordidyouforgettocallit?)
2.Ifyourequestedanexplicitdatabasetransactiontobestarted(we'llseelaterhowtodo
it),notcalling SaveChanges() willresultinthetransactionbeingrolledback.Database
monitoringsystemswillusuallyinterprettransactionrollbacksasanindicationofan
applicationerror.Havingspuriousrollbacksisnotagoodidea.

The DbContextReadOnlyScope classaddressesthisissue.Thisisitsinterface:


publicinterfaceIDbContextReadOnlyScope:IDisposable
{
IDbContextCollectionDbContexts{get;}
}

Andthisishowyouuseit:

publicintNumberPremiumUsers()
{
using(_dbContextScopeFactory.CreateReadOnly())
{
return_userRepository.GetNumberOfPremiumUsers();
}
}

Async support

DbContextScope workswithasyncexecutionflowsasyouwouldexpect:

publicasyncTaskRandomServiceMethodAsync(GuiduserId)
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
varuser=await_userRepository.GetAsync(userId);
varorders=await_orderRepository.GetOrdersForUserAsync(userId);

[...]

awaitdbContextScope.SaveChangesAsync();
}
}

Intheexampleabove,the OrderRepository.GetOrdersForUserAsync() methodwillbeabletosee


andaccesstheambientDbContextinstancedespitethefactthatit'sbeingcalledinaseparate
threadthantheonewherethe DbContextScope wasinitiallycreated.

Thisismadepossiblebythefactthat DbContextScope storesitselfintheCallContext.The


CallContextautomaticallyflowsthroughasyncpoints.Ifyou'recuriousabouthowitallworks
behindthescenes,StephenToubhaswrittenanexcellentblogpostaboutit.Butifallyouwant
todoisuse DbContextScope ,youjusthavetoknowthat:itjustworks.

WARNING:Thereisonethingthatyoumustalwayskeepinmindwhenusinganyasyncflow
with DbContextScope .Justlike TransactionScope , DbContextScope onlysupportsbeingused
withinasinglelogicalflowofexecution.

I.e.ifyouattempttostartmultipleparalleltaskswithinthecontextofa DbContextScope (e.g.by


creatingmultiplethreadsormultipleTPL Task ),youwillgetintobigtrouble.Thisisbecause
theambient DbContextScope willflowthroughallthethreadsyourparalleltasksareusing.If
codeinthesethreadsneedtousethedatabase,theywillallusethesameambient DbContext
instance,resultingthesamethe DbContext instancebeingusedfrommultiplethreads
simultaneously.

Ingeneral,parallelizingdatabaseaccesswithinasinglebusinesstransactionhaslittletono
benefitsandonlyaddssignificantcomplexity.Anyparalleloperationperformedwithinthe
contextofabusinesstransactionshouldnotaccessthedatabase.

However,ifyoureallyneedtostartaparalleltaskwithina DbContextScope (e.g.toperform


someoutofbandbackgroundprocessingindependentlyfromtheoutcomeofthebusiness
transaction),thenyoumustsuppresstheambientcontextbeforestartingtheparalleltask.
Whichyoucaneasilydolikethis:
publicvoidRandomServiceMethod()
{
using(vardbContextScope=_dbContextScopeFactory.Create())
{
//Dosomeworkthatusestheambientcontext
[...]

using(_dbContextScopeFactory.SuppressAmbientContext())
{
//Kickoffparalleltasksthatshouldn'tbeusingthe
//ambientcontexthere.E.g.createnewthreads,
//enqueueworkitemsontheThreadPoolorcreate
//TPLTasks.
[...]
}

Creating a non-nested DbContextScope

ThisisanadvancedfeaturethatIwouldexpectmostapplicationstoneverneed.Tread
carefullywhenusingthisasitcancreatetrickyissuesandquicklyleadtoamaintenance
nightmare.

Sometimes,aservicemethodmayneedtopersistitschangestotheunderlyingdatabase
regardlessoftheoutcomeofoverallbusinesstransactionitmaybepartof.Thiswouldbethe
caseif:

Itneedstorecordcrosscuttingconcerninformationthatshouldn'tberolledbackevenif
thebusinesstransactionfails.Atypicalexamplewouldbeloggingorauditingrecords.
Itneedstorecordtheresultofanoperationthatcannotberolledback.Atypicalexample
wouldbeservicemethodsthatinteractwithnontransactionalremoteservicesorAPIs.
E.g.ifyourservicemethodusestheFacebookAPItopostanewstatusupdateon
Facebookandthenrecordsthenewlycreatedstatusupdateinthelocaldatabase,that
recordmustbepersistedeveniftheoverallbusinesstransactionfailsbecauseofsome
othererroroccurringaftertheFacebookAPIcall.TheFacebookAPIisn'ttransactional
it'simpossibleto"rollback"aFacebookAPIcall.TheresultofthatAPIcallshould
thereforeneverberolledback.
Inthatcase,youcanpassavalueof DbContextScopeOption.ForceCreateNew asthe
joiningOption parameterwhencreatinganew DbContextScope .Thiswillcreatea
DbContextScope thatwillnotjointheambientscopeevenifoneexists:

publicvoidRandomServiceMethod()
{
using(vardbContextScope=_dbContextScopeFactory.Create(DbContextScopeOption
{
//We'vecreatedanewscope.Evenifthatservicemethod
//wascalledbyanotherservicemethodthathascreatedits
//ownDbContextScope,wewon'tbejoiningit.
//OurscopewillcreatenewDbContextinstancesandwon't
//reusetheDbContextinstancesthattheparentscopeuses.
[...]

//Sincewe'veforcedthecreationofanewscope,
//thiscalltoSaveChanges()willpersist
//ourchangesregardlessofwhetherornotthe
//parentscope(ifany)savesitschangesorrollsback.
dbContextScope.SaveChanges();

Themajorissuewithdoingthisisthatthisservicemethodwilluseseparate DbContext
instancesthantheonesusedintherestofthatbusinesstransaction.Hereareafewbasicrules
toalwaysfollowinthatcaseinordertoavoidweirdbugsandmaintenancenightmares:

1. Persistent entity returned by a service method must always be attached to the


ambient context

Ifyouforcethecreationofanew DbContextScope (andthereforeofnew DbContext instances)


insteadofjoiningtheambientone,yourservicemethodmustneverreturnpersistententities
thatwerecreated/retrievedwithinthatnewscope.Thiswouldbecompletelyunexpectedand
willleadtohumongouscomplexity.

Theclientcodecallingyourservicemethodmaybeaservicemethoditselfthatcreateditsown
DbContextScope andthereforeexpectsallservicemethodsitcallstousethatsameambient
scope(thisisthewholepointofusinganambientcontext).Itwillthereforeexpectany
persistententityreturnedbyyourservicemethodtobeattachedtotheambient DbContext .

Instead,either:
Don'treturnpersistententities.Thisistheeasiest,cleanest,mostfoolproofmethod.E.g.
ifyourservicecreatesanewdomainmodelobject,don'treturnit.ReturnitsIDinstead
andlettheclientloadtheentityinitsown DbContext instanceifitneedstheactual
object.
Ifyouabsolutelyneedtoreturnapersistententity,switchbacktotheambientcontext,
loadtheentityyouwanttoreturnintheambientcontextandreturnthat.

2. Upon exit, a service method must make sure that all modi cations it made to
persistent entities have been replicated in the parent scope

Ifyourservicemethodforcesthecreationofanew DbContextScope andthenmodifies


persistententitiesinthatnewscope,itmustmakesurethattheparentambientscope(ifany)
can"see"thosemodificationwhenitreturns.

I.e.ifthe DbContext instancesintheparentscopehadalreadyloadedtheentitiesyoumodified


intheirfirstlevelcache(ObjectStateManager),yourservicemethodmustforcearefreshof
theseentitiestoensurethattheparentscopedoesn'tendupworkingwithstaleversionsof
theseobjects.

The DbContextScope classhasahandyhelpermethodthatmakesthisfairlypainless:

publicvoidRandomServiceMethod(GuidaccountId)
{
//Forcingthecreationofanewscope(i.e.we'llbeusingour
//ownDbContextinstances)
using(vardbContextScope=_dbContextScopeFactory.Create(DbContextScopeOption
{
varaccount=_accountRepository.Get(accountId);
account.Disabled=true;

//Sinceweforcedthecreationofanewscope,
//thiswillpersistourchangestothedatabase
//regardlessofwhattheparentscopedoes.
dbContextScope.SaveChanges();

//Ifthecallerofthismethodhadalready
//loadedthataccountobjectintotheirown

Why DbContextScope and not UnitOfWork?


Thefirstversionofthe DbContextScope classIwrotewasactuallycalled UnitOfWork .Thisis
arguablythemostcommonlyusednameforthistypeofcomponent.

ButasItriedtousethat UnitOfWork componentinarealworldapplication,Ikeptgetting


reallyconfusedastohowIwassupposedtouseitandwhatitreallydid.Thisisdespitethefact
thatIwastheonewhoresearched,designedandimplementeditanddespitethefactthatI
knewwhatitdidandhowitworkedinsideout.Yet,Ikeptgettingmyselfconfusedandhadto
oftentakeastepbackandthinkhardabouthowthis"unitofwork"relatedtotheactual
problemIwastryingtosolve:managingmyDbContextinstances.

IfevenI,whohadspentasignificantamountoftimeresearching,designingandimplementing
thiscomponent,keptgettingconfusedwhentryingtouseit,thereclearlywasn'tahopethat
anyoneelsewouldfinditeasytouseit.

SoIrenamedit DbContextScope andsuddenlyeverythingbecameclearer.

ThemainissueIhadwiththe UnitOfWork Ibelieveisthatattheapplicationlevel,itoften


doesn'tmakealotofsense.Atthelowerlevels,forexampleatthedatabaselevel,a"unitof
work"isaveryclearandconcreteconcept.ThisisMartinFowler'sdefinitionofaunitofwork:

Maintainsalistofobjectsaffectedbyabusinesstransactionandcoordinatesthewriting
outofchangesandtheresolutionofconcurrencyproblems.

Thereisnoambiguityattowhataunitofworkmeansatthedatabaselevel.

Attheapplicationlevelhowever,a"unitofwork"isaveryvagueconceptthatcouldmean
everythingandnothing.Andit'scertainlynotclearhowthis"unitofwork"relatestoEntity
Framework,totheissueofmanaging DbContext instancesandtotheproblemofensuringthat
thepersistententitieswe'remanipulatingareattachedtotherightDbContextinstance.

Asaresult,anydevelopertryingtousea" UnitOfWork "wouldhavetopourthroughitssource


codetofindoutwhatitreallydoes.Thedefinitionoftheunitofworkpatternissimplytoo
vaguetobeusefulattheapplicationlevel.

Infact,formanyapplications,anapplicationlevel"unitofwork"doesn'tevenmakeanysense.
Manyapplicationswillhavetouseseveralnontransactionalservicesduringthecourseofa
businesstransaction,suchasremoteAPIsornontransactionallegacycomponents.The
changesmadetherecannotberolledback.Pretendingotherwiseandiscounterproductive,
confusingandmakesitevenhardertowritecorrectcode.
A DbContextScope ontheothersidedoeswhatitsaysonthetin.Nothingmore,nothingless.It
doesn'tpretendtobewhatit'snot.AndI'vefoundthatthissimplenamechangesignificantly
reducedthecognitiveloadrequiredtousethatcomponentandtoverifythatitwasbeingused
correctly.

Ofcourse,namingthiscomponent DbContextScope meansthatyoucan'thidethefactthat


you'reusingEntityFrameworkfromyourservicesanymore. UnitOfWork isaconveniently
vaguetermthatallowsyoutoabstractawaythepersistencemechanismusedinthelower
layers.WhetherornotabstractingEFawayfromyourservicelayerisagoodthingisanother
debatethatwewon'tgetintohere.

See it in action

ThesourcecodeonGitHubincludesademoapplicationthatdemonstratesthemostcommon
usecases.

How DbContextScope works

ThesourcecodeiswellcommentedandIwouldencourageyoutoreadthroughit.Inaddition,
thisexcellentblogpostbyStephenToubontheExecutionContextisamandatoryreadifyou'd
liketofullyunderstandhowtheambientcontextpatternwasimplementedin DbContextScope .

Further reading

ThepersonalblogofRowanMiller,theprogrammanagerfortheEntityFrameworkteam,isa
mustreadforanydeveloperworkingonanEntityFrameworkbasedapplication.

Bonus material

Where not to create your DbContext instances

AnEntityFrameworkantipatterncommonlyseeninthewildistoimplementthecreationand
disposalof DbContext indataaccessmethods(e.g.inrepositorymethodsinatraditional3tier
application).Itusuallylookslikethis:
publicclassUserService:IUserService
{
privatereadonlyIUserRepository_userRepository;

publicUserService(IUserRepositoryuserRepository)
{
if(userRepository==null)thrownewArgumentNullException("userRepository"
_userRepository=userRepository;
}

publicvoidMarkUserAsPremium(GuiduserId)
{
varuser=_userRepository.Get(userId);
user.IsPremiumUser=true;
_userRepository.Save(user);
}

Bydoingthis,you'reloosingprettymucheveryfeaturethatEntityFrameworkprovidesviathe
DbContext ,includingits1stlevelcache,itsidentitymap,itsunitofwork,anditschange
trackingandlazyloadingabilities.That'sbecauseinthescenarioabove,anew DbContext
instanceiscreatedforeverydatabasequeryanddisposedimmediatelyafterwards,hence
preventingthe DbContext instancefrombeingabletotrackthestateofyourdataobjectsacross
theentirebusinesstransaction.

You'reeffectivelyreducingEntityFrameworktoabasicORMintheliteralsenseoftheterm:an
mapperfromyourobjectstotheirrelationalrepresentationinthedatabase.

Therearesomeapplicationswherethistypeofarchitecturedoesmakesense.Ifyou'reworking
onsuchanapplication,youshouldhoweveraskyourselfwhyyou'reusingEntityFrameworkin
thefirstplace.Ifyou'regoingtouseitasabasicORMandwon'tuseanyofthefeaturesthatit
providesontopofitsORMcapabilities,youmightbebetteroffusingalightweightORMlibrary
suchasDapper.Chancesareitwouldsimplifyyourcodeandofferbetterperformancebynot
havingtheadditionaloverheadthatEFintroducestosupportitsadditionalfunctionalities.

TAGS: .NET , ENTITY FRAMEWORK

Like Tweet +1
Subscribe via RSS
Proudly published with Ghost

También podría gustarte