Documente Academic
Documente Profesional
Documente Cultură
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
A bit of context
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.
HereareafewpointsthatIwouldconsidertobeessentialformostapplications.
Your services must be in control of the business transaction boundary (but not
necessarily in control of the DbContext instance lifetime)
DbContext implementstheUnitofWorkpattern:
Maintainsalistofobjectsaffectedbyabusinesstransactionandcoordinatesthewriting
outofchangesandtheresolutionofconcurrencyproblems.
Aservicemethod,asdefinedabove,isresponsiblefordefiningtheboundaryofabusiness
transaction.
Thepracticalconsequenceofthisisthat:
A DbContext instancecanhoweverspanacrossmultiple(sequential)businesstransactions.
Onceabusinesstransactionhascompletedandhascalledthe DbContext.SaveChanges()
methodtopersistallthechangesitmade,it'sentirelypossibletojustreusethesame
DbContext instanceforthenextbusinesstransaction.
Pros and cons of managing the DbContext instance lifetime independently of the
business transaction lifetime.
Example
Pros
Therearetwomainreasonswhyyouwouldwanttodecouplethelifetimeofthe DbContext
instancefromthebusinesstransactionlifetime.
Forexample,foranapplicationthatstartsoffasasimplewebapplicationandreliesan
instanceperwebrequeststrategytomanagethelifetimeofits DbContext instances,it'seasyto
fallintothetrapofrelyingonlazyloadingincontrollersorviewsoronpassingpersistent
entitiesacrossservicemethodsontheassumptionthattheywillallusethesame DbContext
instancebehindthescenes.Whentheneedtointroducemultithreadingormoveoperationsto
backgroundWindowsServicesinevitablyarises,thiscarefullyconstructedsandcastleoften
collapsesastherearenomorewebrequeststobind DbContext instancesto.
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.
Thearchitectureofasoftwaresystemandthedesignpatternsitreliesonalwaysevolveover
timetoadapttonewconstraints,businessrequirementsandincreasingload.
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.
Ifyourapplicationneedstoconnecttomultipledatabases(forexampleifitusesseparate
reporting,loggingand/orauditingdatabases)orifyouhavesplityourdomainmodelinto
multipleaggregategroups,youwillhavetomanagemultiple DbContext derivedtypes.
ForthosecomingfromanNHibernatebackground,thisistheequivalentofhavingtomanage
multiple SessionFactory instances.
Your DbContext management strategy should work with EF6's async work ow
In.NET4.5,ADO.NETintroduced(atverylonglast)supportforasyncdatabasequeries.Async
supportwasthenincludedinEntityFramework6,allowingyoutouseafullyasyncworkflow
forallreadandwritequeriesmadethroughEF.
Inamultithreadedapplication,youmustcreateanduseaseparateinstanceofyour
DbContext derivedclassineachthread.
Asecondoperationstartedonthiscontextbeforeapreviousasynchronous
operationcompleted.Use'await'toensurethatanyasynchronousoperationshave
completedbeforecallinganothermethodonthiscontext.Anyinstancemembersare
notguaranteedtobethreadsafe.
EntityFramework'sasyncfeaturesaretheretosupportanasynchronousprogrammingmodel,
nottoenableparallelism.
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
Ifyou'recomingfromanNHibernatebackground,thewayEntityFrameworkpersistschanges
tothedatabaseisoneofthemajordifferencesbetweenEFandNHibernate.
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.
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.
EntityFrameworkautomaticallywrapsallthequeriesmadebythe DbContext.SaveChanges()
methodinasingleexplicitdatabasetransaction,thereforeensuringthatallthechangesapplied
tothecontextareeithercommittedorrolledbackinfull.
Itwillusewhateverdefaulttransactionisolationlevelthedatabaseenginehasbeenconfigured
touse(READCOMMITTEDbydefaultforSQLServer).
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
Inpractice,andunlessyouactuallyneedadistributedtransaction,youshouldavoidusing
TransactionScope . TransactionScope ,anddistributedtransactionsingeneral,arenot
necessaryformostapplicationsandtendtointroducemoreproblemsthantheysolve.EF's
documentationhasmoredetailsonworkingwith TransactionScope withEntityFrameworkif
youreallyneeddistributedtransactions.
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.
Akeydecisionyou'llhavetomakeatthestartofanyEntityFrameworkbasedprojectishow
yourcodewillhandlepassingthe DbContext instancesdowntothemethod/layerthatwill
maketheactualdatabasequeries.
Explicit DbContext
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.
The Bad
Themaindrawbackofthisapproachisthatitrequiresyoutopolluteallyourrepository
methods(ifyouhavearepositorylayer)aswellasmostofyourservicemethodswitha
mandatory DbContext parameter(orsomesortof IDbContext abstractionifyoudon'twantto
betiedtoaconcreteimplementationbutthepointstillstands).Youcouldseethisasbeinga
sortofMethodInjectionpattern.
Thingsarequitedifferentinyourservicelayerhowever.Chancesarethatmostofyourservice
methodswon'tusethe DbContext atall,particularlyifyou'veisolatedyourdataaccesscode
awayinqueryobjectsorinarepositorylayer.Asaresult,thesemethodswillonlyrequiretobe
providedwitha DbContext parametersothattheycanpassitdownthelineuntiliteventually
reacheswhatevermethodactuallyusesit.
Nevertheless,thesimplicityandfoolproofnessofthisapproachishardtobeat.
Ambient DbContext
NHibernateuserswillbeveryfamiliarwiththisapproachastheambientcontextpatternisthe
predominantapproachusedintheNHibernateworldtomanageNH's Session (NHibernate's
equivalenttoEF's DbContext ).NHibernateevencomeswithbuiltinsupportforthispattern,
whichitcallscontextualsessions.
In.NETitself,thispatternisusedquiteextensively.You'veprobablyalreadyused
HttpContext.Current orthe TransactionScope class,bothofwhichrelyontheambientcontext
pattern.
AndersAbelhaswrittenasimpleimplementationofanambientDbContextthatreliesona
ThreadStatic variabletostoretheambient DbContext .Havealookthere'slesstoitthanit
sounds.
The Good
Theadvantagesofthisapproachareobvious.Yourserviceandrepositorymethodsarenowfree
of DbContext parameters,makingyourinterfacescleanerandyourmethodcontractscleareras
theycannowonlyrequesttheparametersthattheyactuallyneedtodotheirjob.Noneedto
pass DbContext instancesallovertheplaceanymore.
The Bad
Thisapproachdoeshoweverintroduceacertainamountofmagicwhichcancertainlymakethe
codemoredifficulttounderstandandmaintain.Whenlookingatthedataaccesscode,it'snot
necessarilyeasytofigureoutwheretheambient DbContext iscomingfrom.Youjusthaveto
hopethatsomeonesomehowregistereditbeforecallingthedataaccesscode.
Injected DbContext
Thisiswhatitlookslike:
publicclassUserService:IUserService
{
privatereadonlyIUserRepository_userRepository;
publicUserService(IUserRepositoryuserRepository)
{
if(userRepository==null)thrownewArgumentNullException("userRepository"
_userRepository=userRepository;
}
publicvoidMarkUserAsPremium(GuiduserId)
{
varuser=_userRepository.Get(context,userId);
user.IsPremiumUser=true;
}
}
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.
Perhapsthemostglaringissueinthecodesampleaboveis:whoisresponsibleforcommitting
changestothedatastore?I.e.whoiscallingthe DbContext.SaveChanges() method?It'sunclear.
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.
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.
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.
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.
publicinterfaceIDbContextScopeFactory
{
IDbContextScopeCreate(DbContextScopeOptionjoiningOption=DbContextScopeOption
IDbContextReadOnlyScopeCreateReadOnly(DbContextScopeOptionjoiningOption
IDbContextScopeCreateWithTransaction(IsolationLevelisolationLevel);
IDbContextReadOnlyScopeCreateReadOnlyWithTransaction(IsolationLevelisolationLevel
IDisposableSuppressAmbientContext();
}
Typical usage
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();
}
}
publicclassUserRepository:IUserRepository
{
privatereadonlyIAmbientDbContextLocator_contextLocator;
publicUserRepository(IAmbientDbContextLocatorcontextLocator)
{
if(contextLocator==null)thrownewArgumentNullException("contextLocator"
_contextLocator=contextLocator;
}
publicUserGet(GuiduserId)
{
return_contextLocator.Get<MyDbContext>.Set<User>().Find(userId);
}
}
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
1.Itwillmakecodereviewandmaintenancedifficult(didyouintendnottocall
SaveChanges() ordidyouforgettocallit?)
2.Ifyourequestedanexplicitdatabasetransactiontobestarted(we'llseelaterhowtodo
it),notcalling SaveChanges() willresultinthetransactionbeingrolledback.Database
monitoringsystemswillusuallyinterprettransactionrollbacksasanindicationofan
applicationerror.Havingspuriousrollbacksisnotagoodidea.
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();
}
}
WARNING:Thereisonethingthatyoumustalwayskeepinmindwhenusinganyasyncflow
with DbContextScope .Justlike TransactionScope , DbContextScope onlysupportsbeingused
withinasinglelogicalflowofexecution.
Ingeneral,parallelizingdatabaseaccesswithinasinglebusinesstransactionhaslittletono
benefitsandonlyaddssignificantcomplexity.Anyparalleloperationperformedwithinthe
contextofabusinesstransactionshouldnotaccessthedatabase.
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:
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
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
IfevenI,whohadspentasignificantamountoftimeresearching,designingandimplementing
thiscomponent,keptgettingconfusedwhentryingtouseit,thereclearlywasn'tahopethat
anyoneelsewouldfinditeasytouseit.
Maintainsalistofobjectsaffectedbyabusinesstransactionandcoordinatesthewriting
outofchangesandtheresolutionofconcurrencyproblems.
Thereisnoambiguityattowhataunitofworkmeansatthedatabaselevel.
Attheapplicationlevelhowever,a"unitofwork"isaveryvagueconceptthatcouldmean
everythingandnothing.Andit'scertainlynotclearhowthis"unitofwork"relatestoEntity
Framework,totheissueofmanaging DbContext instancesandtotheproblemofensuringthat
thepersistententitieswe'remanipulatingareattachedtotherightDbContextinstance.
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.
See it in action
ThesourcecodeonGitHubincludesademoapplicationthatdemonstratesthemostcommon
usecases.
ThesourcecodeiswellcommentedandIwouldencourageyoutoreadthroughit.Inaddition,
thisexcellentblogpostbyStephenToubontheExecutionContextisamandatoryreadifyou'd
liketofullyunderstandhowtheambientcontextpatternwasimplementedin DbContextScope .
Further reading
ThepersonalblogofRowanMiller,theprogrammanagerfortheEntityFrameworkteam,isa
mustreadforanydeveloperworkingonanEntityFrameworkbasedapplication.
Bonus material
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.
Like Tweet +1
Subscribe via RSS
Proudly published with Ghost