├── .gitattributes ├── .gitignore ├── .vs └── restore.dg ├── DbContextScope.sln ├── LICENSE ├── README.md ├── global.json └── src ├── DbContextScope.EfCore ├── DbContextScope.EfCore.xproj ├── DbContextScope.Interfaces.licenseheader ├── Enums │ └── DbContextScopeOption.cs ├── Implementations │ ├── AmbientContextSuppressor.cs │ ├── AmbientDbContextLocator.cs │ ├── DbContextCollection.cs │ ├── DbContextReadOnlyScope.cs │ ├── DbContextScope.cs │ └── DbContextScopeFactory.cs ├── Interfaces │ ├── IAmbientDbContextLocator.cs │ ├── IDbContextCollection.cs │ ├── IDbContextFactory.cs │ ├── IDbContextReadOnlyScope.cs │ ├── IDbContextScope.cs │ └── IDbContextScopeFactory.cs ├── Properties │ └── AssemblyInfo.cs ├── project.json └── project.lock.json ├── DbContextScope.UnitOfWork.Core ├── DbContextScope.UnitOfWork.Core.xproj ├── Interfaces │ └── IUnitOfWork.cs ├── Properties │ └── AssemblyInfo.cs ├── Repository │ └── IRepository.cs ├── project.json └── project.lock.json ├── DbContextScope.UnitOfWork.EfCore ├── DbContextScope.UnitOfWork.EfCore.xproj ├── IEntityFrameworkUnitOfWork.cs ├── Properties │ └── AssemblyInfo.cs ├── Repository │ ├── EntityFrameworkRepository.cs │ └── IEntityFrameworkRepository.cs ├── project.json └── project.lock.json └── DemoApplication ├── BusinessLogicServices ├── UserCreationService.cs ├── UserCreditScoreService.cs ├── UserEmailService.cs └── UserQueryService.cs ├── CommandModel └── UserCreationSpec.cs ├── DatabaseContext └── UserManagementDbContext.cs ├── Demo Application.xproj ├── DomainModel └── User.cs ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── Repositories ├── IUserRepository.cs └── UserRepository.cs ├── project.json └── project.lock.json /.gitattributes: -------------------------------------------------------------------------------- 1 | #common settings that generally should always be used with your language specific settings 2 | 3 | # Auto detect text files and perform LF normalization 4 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # Documents 12 | *.doc diff=astextplain 13 | *.DOC diff=astextplain 14 | *.docx diff=astextplain 15 | *.DOCX diff=astextplain 16 | *.dot diff=astextplain 17 | *.DOT diff=astextplain 18 | *.pdf diff=astextplain 19 | *.PDF diff=astextplain 20 | *.rtf diff=astextplain 21 | *.RTF diff=astextplain 22 | 23 | # Graphics 24 | *.png binary 25 | *.jpg binary 26 | *.jpeg binary 27 | *.gif binary 28 | *.ico binary 29 | *.svg text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | # TODO: Comment the next line if you want to checkin your web deploy settings 129 | # but database connection strings (with potential passwords) will be unencrypted 130 | *.pubxml 131 | *.publishproj 132 | 133 | # NuGet Packages 134 | *.nupkg 135 | # The packages folder can be ignored because of Package Restore 136 | **/packages/* 137 | # except build/, which is used as an MSBuild target. 138 | !**/packages/build/ 139 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 140 | #!**/packages/repositories.config 141 | 142 | # Windows Azure Build Output 143 | csx/ 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.dbproj.schemaview 158 | *.pfx 159 | *.publishsettings 160 | node_modules/ 161 | 162 | # RIA/Silverlight projects 163 | Generated_Code/ 164 | 165 | # Backup & report files from converting an old project file 166 | # to a newer Visual Studio version. Backup files are not needed, 167 | # because we have git ;-) 168 | _UpgradeReport_Files/ 169 | Backup*/ 170 | UpgradeLog*.XML 171 | UpgradeLog*.htm 172 | 173 | # SQL Server files 174 | *.mdf 175 | *.ldf 176 | 177 | # Business Intelligence projects 178 | *.rdl.data 179 | *.bim.layout 180 | *.bim_*.settings 181 | 182 | # Microsoft Fakes 183 | FakesAssemblies/ 184 | /.vs/config/applicationhost.config 185 | -------------------------------------------------------------------------------- /.vs/restore.dg: -------------------------------------------------------------------------------- 1 | #:C:\Dev\Code Zion\DbContextScope\src\DemoApplication\Demo Application.xproj 2 | C:\Dev\Code Zion\DbContextScope\src\DemoApplication\Demo Application.xproj|C:\Dev\Code Zion\DbContextScope\src\DbContextScope.EfCore\DbContextScope.EfCore.xproj 3 | -------------------------------------------------------------------------------- /DbContextScope.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{81DA8C35-7725-4BEF-B90C-F6D3040498FE}" 7 | ProjectSection(SolutionItems) = preProject 8 | global.json = global.json 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6258D4E6-ECD4-45A6-951D-211A52A1183E}" 12 | EndProject 13 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Demo Application", "src\DemoApplication\Demo Application.xproj", "{C7710DE2-92AE-49C8-8645-DB1300A2CD9C}" 14 | EndProject 15 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "DbContextScope.UnitOfWork.Core", "src\DbContextScope.UnitOfWork.Core\DbContextScope.UnitOfWork.Core.xproj", "{1C3207F7-9381-453F-8783-B3D52A0C9B8B}" 16 | EndProject 17 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "DbContextScope.EfCore", "src\DbContextScope.EfCore\DbContextScope.EfCore.xproj", "{AB70542C-890D-4630-B27E-14F9587DD278}" 18 | EndProject 19 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "DbContextScope.UnitOfWork.EfCore", "src\DbContextScope.UnitOfWork.EfCore\DbContextScope.UnitOfWork.EfCore.xproj", "{E053CA11-B054-46D7-AF4E-84AEFF0AA1E3}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {C7710DE2-92AE-49C8-8645-DB1300A2CD9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {C7710DE2-92AE-49C8-8645-DB1300A2CD9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {C7710DE2-92AE-49C8-8645-DB1300A2CD9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {C7710DE2-92AE-49C8-8645-DB1300A2CD9C}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {1C3207F7-9381-453F-8783-B3D52A0C9B8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {1C3207F7-9381-453F-8783-B3D52A0C9B8B}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {1C3207F7-9381-453F-8783-B3D52A0C9B8B}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {1C3207F7-9381-453F-8783-B3D52A0C9B8B}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {AB70542C-890D-4630-B27E-14F9587DD278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {AB70542C-890D-4630-B27E-14F9587DD278}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {AB70542C-890D-4630-B27E-14F9587DD278}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {AB70542C-890D-4630-B27E-14F9587DD278}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {E053CA11-B054-46D7-AF4E-84AEFF0AA1E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {E053CA11-B054-46D7-AF4E-84AEFF0AA1E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {E053CA11-B054-46D7-AF4E-84AEFF0AA1E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {E053CA11-B054-46D7-AF4E-84AEFF0AA1E3}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(NestedProjects) = preSolution 48 | {C7710DE2-92AE-49C8-8645-DB1300A2CD9C} = {6258D4E6-ECD4-45A6-951D-211A52A1183E} 49 | {1C3207F7-9381-453F-8783-B3D52A0C9B8B} = {6258D4E6-ECD4-45A6-951D-211A52A1183E} 50 | {AB70542C-890D-4630-B27E-14F9587DD278} = {6258D4E6-ECD4-45A6-951D-211A52A1183E} 51 | {E053CA11-B054-46D7-AF4E-84AEFF0AA1E3} = {6258D4E6-ECD4-45A6-951D-211A52A1183E} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mehdi El Gueddari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DbContextScope 2 | ============== 3 | 4 | A simple and flexible way to manage your Entity Framework DbContext instances. 5 | 6 | `DbContextScope` was created out of the need for a better way to manage DbContext instances in Entity Framework-based applications. 7 | 8 | The commonly advocated method of injecting DbContext instances works fine for single-threaded web applications where each web request implements exactly one business transaction. But it breaks down quite badly when console apps, Windows Services, parallelism and requests that need to implement multiple independent business transactions make their appearance. 9 | 10 | The alternative of manually instantiating DbContext instances and manually passing them around as method parameters is (speaking from experience) more than cumbersome. 11 | 12 | `DbContextScope` implements the ambient context pattern for DbContext instances. It's something that NHibernate users or anyone who has used the `TransactionScope` class to manage ambient database transactions will be familiar with. 13 | 14 | It doesn't force any particular design pattern or application architecture to be used. It works beautifully with dependency injection. And it works beautifully without. It of course works perfectly with async execution flows, including with the new async / await support introduced in .NET 4.5 and EF6. 15 | 16 | And most importantly, at the time of writing, `DbContextScope` has been battle-tested in a large-scale application for over two months and has performed without a hitch. 17 | 18 | #Using DbContextScope 19 | 20 | The repo contains a demo application that demonstrates the most common (and a few more advanced) use-cases. 21 | 22 | I would highly recommend reading the following blog post first. It examines in great details the most commonly used approaches to manage DbContext instances and explains how `DbContextScope` addresses their shortcomings and simplifies DbContext management: [Managing DbContext the right way with Entity Framework 6: an in-depth guide](http://mehdi.me/ambient-dbcontext-in-ef6/). 23 | 24 | ###Overview 25 | 26 | This is the `DbContextScope` interface: 27 | 28 | ```C# 29 | public interface IDbContextScope : IDisposable 30 | { 31 | void SaveChanges(); 32 | Task SaveChangesAsync(); 33 | 34 | void RefreshEntitiesInParentScope(IEnumerable entities); 35 | Task RefreshEntitiesInParentScopeAsync(IEnumerable entities); 36 | 37 | IDbContextCollection DbContexts { get; } 38 | } 39 | ``` 40 | 41 | The purpose of a `DbContextScope` is to create and manage the `DbContext` instances used within a code block. A `DbContextScope` therefore effectively defines the boundary of a business transaction. 42 | 43 | Wondering why DbContextScope wasn't called "UnitOfWork" or "UnitOfWorkScope"? The answer is here: [Why DbContextScope and not UnitOfWork?](http://mehdi.me/ambient-dbcontext-in-ef6/#whydbcontextscopeandnotunitofwork) 44 | 45 | You can instantiate a `DbContextScope` directly. Or you can take a dependency on `IDbContextScopeFactory`, which provides convenience methods to create a `DbContextScope` with the most common configurations: 46 | 47 | ```C# 48 | public interface IDbContextScopeFactory 49 | { 50 | IDbContextScope Create(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting); 51 | IDbContextReadOnlyScope CreateReadOnly(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting); 52 | 53 | IDbContextScope CreateWithTransaction(IsolationLevel isolationLevel); 54 | IDbContextReadOnlyScope CreateReadOnlyWithTransaction(IsolationLevel isolationLevel); 55 | 56 | IDisposable SuppressAmbientContext(); 57 | } 58 | ``` 59 | 60 | ###Typical usage 61 | With `DbContextScope`, your typical service method would look like this: 62 | 63 | ```C# 64 | public void MarkUserAsPremium(Guid userId) 65 | { 66 | using (var dbContextScope = _dbContextScopeFactory.Create()) 67 | { 68 | var user = _userRepository.Get(userId); 69 | user.IsPremiumUser = true; 70 | dbContextScope.SaveChanges(); 71 | } 72 | } 73 | ``` 74 | 75 | Within a `DbContextScope`, you can access the `DbContext` instances that the scope manages in two ways. You can get them via the `DbContextScope.DbContexts` property like this: 76 | 77 | ```C# 78 | public void SomeServiceMethod(Guid userId) 79 | { 80 | using (var dbContextScope = _dbContextScopeFactory.Create()) 81 | { 82 | var user = dbContextScope.DbContexts.Get.Set.Find(userId); 83 | [...] 84 | dbContextScope.SaveChanges(); 85 | } 86 | } 87 | ``` 88 | 89 | But that's of course only available in the method that created the `DbContextScope`. If you need to access the ambient `DbContext` instances anywhere else (e.g. in a repository class), you can just take a dependency on `IAmbientDbContextLocator`, which you would use like this: 90 | 91 | ```C# 92 | public class UserRepository : IUserRepository 93 | { 94 | private readonly IAmbientDbContextLocator _contextLocator; 95 | 96 | public UserRepository(IAmbientDbContextLocator contextLocator) 97 | { 98 | if (contextLocator == null) throw new ArgumentNullException("contextLocator"); 99 | _contextLocator = contextLocator; 100 | } 101 | 102 | public User Get(Guid userId) 103 | { 104 | return _contextLocator.Get.Set().Find(userId); 105 | } 106 | } 107 | ``` 108 | 109 | Those `DbContext` instances are created lazily and the `DbContextScope` keeps track of them to ensure that only one instance of any given DbContext type is ever created within its scope. 110 | 111 | You'll note that the service method doesn't need to know which type of `DbContext` will be required during the course of the business transaction. It only needs to create a `DbContextScope` and any component that needs to access the database within that scope will request the type of `DbContext` they need. 112 | 113 | ###Nesting scopes 114 | A `DbContextScope` can of course be nested. Let's say that you already have a service method that can mark a user as a premium user like this: 115 | 116 | ```C# 117 | public void MarkUserAsPremium(Guid userId) 118 | { 119 | using (var dbContextScope = _dbContextScopeFactory.Create()) 120 | { 121 | var user = _userRepository.Get(userId); 122 | user.IsPremiumUser = true; 123 | dbContextScope.SaveChanges(); 124 | } 125 | } 126 | ``` 127 | 128 | You're implementing a new feature that requires being able to mark a group of users as premium within a single business transaction. You can easily do it like this: 129 | 130 | ```C# 131 | public void MarkGroupOfUsersAsPremium(IEnumerable userIds) 132 | { 133 | using (var dbContextScope = _dbContextScopeFactory.Create()) 134 | { 135 | foreach (var userId in userIds) 136 | { 137 | // The child scope created by MarkUserAsPremium() will 138 | // join our scope. So it will re-use our DbContext instance(s) 139 | // and the call to SaveChanges() made in the child scope will 140 | // have no effect. 141 | MarkUserAsPremium(userId); 142 | } 143 | 144 | // Changes will only be saved here, in the top-level scope, 145 | // ensuring that all the changes are either committed or 146 | // rolled-back atomically. 147 | dbContextScope.SaveChanges(); 148 | } 149 | } 150 | ``` 151 | 152 | (this would of course be a very inefficient way to implement this particular feature but it demonstrates the point) 153 | 154 | This makes creating a service method that combines the logic of multiple other service methods trivial. 155 | 156 | ###Read-only scopes 157 | If a service method is read-only, having to call `SaveChanges()` on its `DbContextScope` before returning can be a pain. But not calling it isn't an option either as: 158 | 159 | 1. It will make code review and maintenance difficult (did you intend not to call `SaveChanges()` or did you forget to call it?) 160 | 2. If you requested an explicit database transaction to be started (we'll see later how to do it), not calling `SaveChanges()` will result in the transaction being rolled back. Database monitoring systems will usually interpret transaction rollbacks as an indication of an application error. Having spurious rollbacks is not a good idea. 161 | 162 | The `DbContextReadOnlyScope` class addresses this issue. This is its interface: 163 | 164 | ```C# 165 | public interface IDbContextReadOnlyScope : IDisposable 166 | { 167 | IDbContextCollection DbContexts { get; } 168 | } 169 | ``` 170 | 171 | And this is how you use it: 172 | 173 | ```C# 174 | public int NumberPremiumUsers() 175 | { 176 | using (_dbContextScopeFactory.CreateReadOnly()) 177 | { 178 | return _userRepository.GetNumberOfPremiumUsers(); 179 | } 180 | } 181 | ``` 182 | 183 | ###Async support 184 | `DbContextScope` works with async execution flows as you would expect: 185 | 186 | ```C# 187 | public async Task RandomServiceMethodAsync(Guid userId) 188 | { 189 | using (var dbContextScope = _dbContextScopeFactory.Create()) 190 | { 191 | var user = await _userRepository.GetAsync(userId); 192 | var orders = await _orderRepository.GetOrdersForUserAsync(userId); 193 | 194 | [...] 195 | 196 | await dbContextScope.SaveChangesAsync(); 197 | } 198 | } 199 | ``` 200 | 201 | In the example above, the `OrderRepository.GetOrdersForUserAsync()` method will be able to see and access the ambient DbContext instance despite the fact that it's being called in a separate thread than the one where the `DbContextScope` was initially created. 202 | 203 | This is made possible by the fact that `DbContextScope` stores itself in the CallContext. The CallContext automatically flows through async points. If you're curious about how it all works behind the scenes, Stephen Toub has written [an excellent blog post about it](http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx). But if all you want to do is use `DbContextScope`, you just have to know that: it just works. 204 | 205 | **WARNING**: There is one thing that you *must* always keep in mind when using any async flow with `DbContextScope`. Just like `TransactionScope`, `DbContextScope` only supports being used within a single logical flow of execution. 206 | 207 | I.e. if you attempt to start multiple parallel tasks within the context of a `DbContextScope` (e.g. by creating multiple threads or multiple TPL `Task`), you will get into big trouble. This is because the ambient `DbContextScope` will flow through all the threads your parallel tasks are using. If code in these threads need to use the database, they will all use the same ambient `DbContext` instance, resulting the same the `DbContext` instance being used from multiple threads simultaneously. 208 | 209 | In general, parallelizing database access within a single business transaction has little to no benefits and only adds significant complexity. Any parallel operation performed within the context of a business transaction should not access the database. 210 | 211 | However, if you really need to start a parallel task within a `DbContextScope` (e.g. to perform some out-of-band background processing independently from the outcome of the business transaction), then you **must** suppress the ambient context before starting the parallel task. Which you can easily do like this: 212 | 213 | ```C# 214 | public void RandomServiceMethod() 215 | { 216 | using (var dbContextScope = _dbContextScopeFactory.Create()) 217 | { 218 | // Do some work that uses the ambient context 219 | [...] 220 | 221 | using (_dbContextScopeFactory.SuppressAmbientContext()) 222 | { 223 | // Kick off parallel tasks that shouldn't be using the 224 | // ambient context here. E.g. create new threads, 225 | // enqueue work items on the ThreadPool or create 226 | // TPL Tasks. 227 | [...] 228 | } 229 | 230 | // The ambient context is available again here. 231 | // Can keep doing more work as usual. 232 | [...] 233 | 234 | dbContextScope.SaveChanges(); 235 | } 236 | } 237 | ``` 238 | 239 | ###Creating a non-nested DbContextScope 240 | This is an advanced feature that I would expect most applications to never need. Tread carefully when using this as it can create tricky issues and quickly lead to a maintenance nightmare. 241 | 242 | Sometimes, a service method may need to persist its changes to the underlying database regardless of the outcome of overall business transaction it may be part of. This would be the case if: 243 | 244 | - It needs to record cross-cutting concern information that shouldn't be rolled-back even if the business transaction fails. A typical example would be logging or auditing records. 245 | - It needs to record the result of an operation that cannot be rolled back. A typical example would be service methods that interact with non-transactional remote services or APIs. E.g. if your service method uses the Facebook API to post a new status update on Facebook and then records the newly created status update in the local database, that record must be persisted even if the overall business transaction fails because of some other error occurring after the Facebook API call. The Facebook API isn't transactional - it's impossible to "rollback" a Facebook API call. The result of that API call should therefore never be rolled back. 246 | 247 | In that case, you can pass a value of `DbContextScopeOption.ForceCreateNew` as the `joiningOption` parameter when creating a new `DbContextScope`. This will create a `DbContextScope` that will not join the ambient scope even if one exists: 248 | 249 | ```C# 250 | public void RandomServiceMethod() 251 | { 252 | using (var dbContextScope = _dbContextScopeFactory.Create(DbContextScopeOption.ForceCreateNew)) 253 | { 254 | // We've created a new scope. Even if that service method 255 | // was called by another service method that has created its 256 | // own DbContextScope, we won't be joining it. 257 | // Our scope will create new DbContext instances and won't 258 | // re-use the DbContext instances that the parent scope uses. 259 | [...] 260 | 261 | // Since we've forced the creation of a new scope, 262 | // this call to SaveChanges() will persist 263 | // our changes regardless of whether or not the 264 | // parent scope (if any) saves its changes or rolls back. 265 | dbContextScope.SaveChanges(); 266 | } 267 | } 268 | ``` 269 | 270 | The major issue with doing this is that this service method will use separate `DbContext` instances than the ones used in the rest of that business transaction. Here are a few basic rules to always follow in that case in order to avoid weird bugs and maintenance nightmares: 271 | 272 | ####1. Persistent entity returned by a service method must always be attached to the ambient context 273 | 274 | If you force the creation of a new `DbContextScope` (and therefore of new `DbContext` instances) instead of joining the ambient one, your service method must **never** return persistent entities that were created / retrieved within that new scope. This would be completely unexpected and will lead to humongous complexity. 275 | 276 | The client code calling your service method may be a service method itself that created its own `DbContextScope` and therefore expects all service methods it calls to use that same ambient scope (this is the whole point of using an ambient context). It will therefore expect any persistent entity returned by your service method to be attached to the ambient `DbContext`. 277 | 278 | Instead, either: 279 | 280 | - Don't return persistent entities. This is the easiest, cleanest, most foolproof method. E.g. if your service creates a new domain model object, don't return it. Return its ID instead and let the client load the entity in its own `DbContext` instance if it needs the actual object. 281 | - If you absolutely need to return a persistent entity, switch back to the ambient context, load the entity you want to return in the ambient context and return that. 282 | 283 | ####2. Upon exit, a service method must make sure that all modifications it made to persistent entities have been replicated in the parent scope 284 | 285 | If your service method forces the creation of a new `DbContextScope` and then modifies persistent entities in that new scope, it must make sure that the parent ambient scope (if any) can "see" those modification when it returns. 286 | 287 | I.e. if the `DbContext` instances in the parent scope had already loaded the entities you modified in their first-level cache (ObjectStateManager), your service method must force a refresh of these entities to ensure that the parent scope doesn't end up working with stale versions of these objects. 288 | 289 | The `DbContextScope` class has a handy helper method that makes this fairly painless: 290 | 291 | ```C# 292 | public void RandomServiceMethod(Guid accountId) 293 | { 294 | // Forcing the creation of a new scope (i.e. we'll be using our 295 | // own DbContext instances) 296 | using (var dbContextScope = _dbContextScopeFactory.Create(DbContextScopeOption.ForceCreateNew)) 297 | { 298 | var account = _accountRepository.Get(accountId); 299 | account.Disabled = true; 300 | 301 | // Since we forced the creation of a new scope, 302 | // this will persist our changes to the database 303 | // regardless of what the parent scope does. 304 | dbContextScope.SaveChanges(); 305 | 306 | // If the caller of this method had already 307 | // loaded that account object into their own 308 | // DbContext instance, their version 309 | // has now become stale. They won't see that 310 | // this account has been disabled and might 311 | // therefore execute incorrect logic. 312 | // So make sure that the version our caller 313 | // has is up-to-date. 314 | dbContextScope.RefreshEntitiesInParentScope(new[] { account }); 315 | } 316 | } 317 | ``` 318 | 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { 4 | "version": "1.0.0-preview2-003121" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/DbContextScope.EfCore.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | ab70542c-890d-4630-b27e-14f9587dd278 10 | DbContextScope.Ef7 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | SAK 14 | SAK 15 | SAK 16 | SAK 17 | 18 | 19 | 2.0 20 | 21 | 22 | True 23 | 24 | 25 | True 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/DbContextScope.Interfaces.licenseheader: -------------------------------------------------------------------------------- 1 | extensions: designer.cs generated.cs 2 | extensions: .cs .cpp .h 3 | /* 4 | * Copyright (C) 2014 Mehdi El Gueddari 5 | * http://mehdi.me 6 | * 7 | * This software may be modified and distributed under the terms 8 | * of the MIT license. See the LICENSE file for details. 9 | */ 10 | 11 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Enums/DbContextScopeOption.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | namespace DbContextScope.EfCore.Enums 9 | { 10 | /// 11 | /// Indicates whether or not a new DbContextScope will join the ambient scope. 12 | /// 13 | public enum DbContextScopeOption 14 | { 15 | /// 16 | /// Join the ambient DbContextScope if one exists. Creates a new 17 | /// one otherwise. 18 | /// 19 | /// This is what you want in most cases. Joining the existing ambient scope 20 | /// ensures that all code within a business transaction uses the same DbContext 21 | /// instance and that all changes made by service methods called within that 22 | /// business transaction are either committed or rolled back atomically when the top-level 23 | /// scope completes (i.e. it ensures that there are no partial commits). 24 | /// 25 | JoinExisting, 26 | 27 | /// 28 | /// Ignore the ambient DbContextScope (if any) and force the creation of 29 | /// a new DbContextScope. 30 | /// 31 | /// This is an advanced feature that should be used with great care. 32 | /// 33 | /// When forcing the creation of a new scope, new DbContext instances will be 34 | /// created within that inner scope instead of re-using the DbContext instances that 35 | /// the parent scope (if any) is using. 36 | /// 37 | /// Any changes made to entities within that inner scope will therefore get persisted 38 | /// to the database when SaveChanges() is called in the inner scope regardless of wether 39 | /// or not the parent scope is successful. 40 | /// 41 | /// You would typically do this to ensure that the changes made within the inner scope 42 | /// are always persisted regardless of the outcome of the overall business transaction 43 | /// (e.g. to persist the results of an operation, such as a remote API call, that 44 | /// cannot be rolled back or to persist audit or log entries that must not be rolled back 45 | /// regardless of the outcome of the business transaction). 46 | /// 47 | ForceCreateNew 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/AmbientContextSuppressor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | 10 | namespace DbContextScope.EfCore.Implementations 11 | { 12 | public class AmbientContextSuppressor : IDisposable 13 | { 14 | DbContextScope _savedScope; 15 | bool _disposed; 16 | 17 | public AmbientContextSuppressor() 18 | { 19 | _savedScope = DbContextScope.GetAmbientScope(); 20 | 21 | // We're hiding the ambient scope but not removing its instance 22 | // altogether. This is to be tolerant to some programming errors. 23 | // 24 | // Suppose we removed the ambient scope instance here. If someone 25 | // was to start a parallel task without suppressing 26 | // the ambient context and then tried to suppress the ambient 27 | // context within the parallel task while the original flow 28 | // of execution was still ongoing (a strange thing to do, I know, 29 | // but I'm sure this is going to happen), we would end up 30 | // removing the ambient context instance of the original flow 31 | // of execution from within the parallel flow of execution! 32 | // 33 | // As a result, any code in the original flow of execution 34 | // that would attempt to access the ambient scope would end up 35 | // with a null value since we removed the instance. 36 | // 37 | // It would be a fairly nasty bug to track down. So don't let 38 | // that happen. Hiding the ambient scope (i.e. clearing the CallContext 39 | // in our execution flow but leaving the ambient scope instance untouched) 40 | // is safe. 41 | DbContextScope.HideAmbientScope(); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | if (_disposed) 47 | return; 48 | 49 | if (_savedScope != null) 50 | { 51 | DbContextScope.SetAmbientScope(_savedScope); 52 | _savedScope = null; 53 | } 54 | 55 | _disposed = true; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/AmbientDbContextLocator.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using DbContextScope.EfCore.Interfaces; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace DbContextScope.EfCore.Implementations 12 | { 13 | public class AmbientDbContextLocator : IAmbientDbContextLocator 14 | { 15 | public TDbContext Get() where TDbContext : DbContext 16 | { 17 | var ambientDbContextScope = DbContextScope.GetAmbientScope(); 18 | return ambientDbContextScope == null ? null : ambientDbContextScope.DbContexts.Get(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/DbContextCollection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Data; 11 | using System.Runtime.ExceptionServices; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using DbContextScope.EfCore.Interfaces; 15 | using Microsoft.EntityFrameworkCore; 16 | using Microsoft.EntityFrameworkCore.Storage; 17 | 18 | namespace DbContextScope.EfCore.Implementations 19 | { 20 | /// 21 | /// As its name suggests, DbContextCollection maintains a collection of DbContext instances. 22 | /// 23 | /// What it does in a nutshell: 24 | /// - Lazily instantiates DbContext instances when its Get Of TDbContext () method is called 25 | /// (and optionally starts an explicit database transaction). 26 | /// - Keeps track of the DbContext instances it created so that it can return the existing 27 | /// instance when asked for a DbContext of a specific type. 28 | /// - Takes care of committing / rolling back changes and transactions on all the DbContext 29 | /// instances it created when its Commit() or Rollback() method is called. 30 | /// 31 | /// 32 | public class DbContextCollection : IDbContextCollection 33 | { 34 | Dictionary _initializedDbContexts; 35 | Dictionary _transactions; 36 | IsolationLevel? _isolationLevel; 37 | readonly IDbContextFactory _dbContextFactory; 38 | bool _disposed; 39 | bool _completed; 40 | bool _readOnly; 41 | 42 | internal Dictionary InitializedDbContexts { get { return _initializedDbContexts; } } 43 | 44 | public DbContextCollection(bool readOnly = false, IsolationLevel? isolationLevel = null, IDbContextFactory dbContextFactory = null) 45 | { 46 | _disposed = false; 47 | _completed = false; 48 | 49 | _initializedDbContexts = new Dictionary(); 50 | _transactions = new Dictionary(); 51 | 52 | _readOnly = readOnly; 53 | _isolationLevel = isolationLevel; 54 | _dbContextFactory = dbContextFactory; 55 | } 56 | 57 | public TDbContext Get() where TDbContext : DbContext 58 | { 59 | if (_disposed) 60 | throw new ObjectDisposedException("DbContextCollection"); 61 | 62 | var requestedType = typeof(TDbContext); 63 | 64 | if (!_initializedDbContexts.ContainsKey(requestedType)) 65 | { 66 | // First time we've been asked for this particular DbContext type. 67 | // Create one, cache it and start its database transaction if needed. 68 | var dbContext = _dbContextFactory != null 69 | ? _dbContextFactory.CreateDbContext() 70 | : Activator.CreateInstance(); 71 | 72 | _initializedDbContexts.Add(requestedType, dbContext); 73 | 74 | dbContext.ChangeTracker.AutoDetectChangesEnabled &= !_readOnly; 75 | 76 | if (_isolationLevel.HasValue) 77 | { 78 | var tran = dbContext.Database.BeginTransaction(_isolationLevel.Value); 79 | _transactions.Add(dbContext, tran); 80 | } 81 | } 82 | 83 | return _initializedDbContexts[requestedType] as TDbContext; 84 | } 85 | 86 | public int Commit() 87 | { 88 | if (_disposed) 89 | throw new ObjectDisposedException("DbContextCollection"); 90 | if (_completed) 91 | throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there."); 92 | 93 | // Best effort. You'll note that we're not actually implementing an atomic commit 94 | // here. It entirely possible that one DbContext instance will be committed successfully 95 | // and another will fail. Implementing an atomic commit would require us to wrap 96 | // all of this in a TransactionScope. The problem with TransactionScope is that 97 | // the database transaction it creates may be automatically promoted to a 98 | // distributed transaction if our DbContext instances happen to be using different 99 | // databases. And that would require the DTC service (Distributed Transaction Coordinator) 100 | // to be enabled on all of our live and dev servers as well as on all of our dev workstations. 101 | // Otherwise the whole thing would blow up at runtime. 102 | 103 | // In practice, if our services are implemented following a reasonably DDD approach, 104 | // a business transaction (i.e. a service method) should only modify entities in a single 105 | // DbContext. So we should never find ourselves in a situation where two DbContext instances 106 | // contain uncommitted changes here. We should therefore never be in a situation where the below 107 | // would result in a partial commit. 108 | 109 | ExceptionDispatchInfo lastError = null; 110 | 111 | var c = 0; 112 | 113 | foreach (var dbContext in _initializedDbContexts.Values) 114 | { 115 | try 116 | { 117 | if (!_readOnly) 118 | { 119 | c += dbContext.SaveChanges(); 120 | } 121 | 122 | // If we've started an explicit database transaction, time to commit it now. 123 | var tran = GetValueOrDefault(_transactions, dbContext); 124 | if (tran != null) 125 | { 126 | tran.Commit(); 127 | tran.Dispose(); 128 | _transactions.Remove(dbContext); 129 | } 130 | } 131 | catch (Exception e) 132 | { 133 | lastError = ExceptionDispatchInfo.Capture(e); 134 | } 135 | } 136 | 137 | if (lastError != null) 138 | lastError.Throw(); // Re-throw while maintaining the exception's original stack track 139 | else 140 | { 141 | _completed = true; 142 | } 143 | 144 | return c; 145 | } 146 | 147 | public Task CommitAsync() 148 | { 149 | return CommitAsync(CancellationToken.None); 150 | } 151 | 152 | public async Task CommitAsync(CancellationToken cancelToken) 153 | { 154 | if (_disposed) 155 | throw new ObjectDisposedException("DbContextCollection"); 156 | if (_completed) 157 | throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there."); 158 | 159 | // See comments in the sync version of this method for more details. 160 | 161 | ExceptionDispatchInfo lastError = null; 162 | 163 | var c = 0; 164 | 165 | foreach (var dbContext in _initializedDbContexts.Values) 166 | { 167 | try 168 | { 169 | if (!_readOnly) 170 | { 171 | c += await dbContext.SaveChangesAsync(cancelToken).ConfigureAwait(false); 172 | } 173 | 174 | // If we've started an explicit database transaction, time to commit it now. 175 | var tran = GetValueOrDefault(_transactions, dbContext); 176 | if (tran != null) 177 | { 178 | tran.Commit(); 179 | tran.Dispose(); 180 | _transactions.Remove(dbContext); 181 | } 182 | } 183 | catch (Exception e) 184 | { 185 | lastError = ExceptionDispatchInfo.Capture(e); 186 | } 187 | } 188 | 189 | if (lastError != null) 190 | lastError.Throw(); // Re-throw while maintaining the exception's original stack track 191 | else 192 | { 193 | _completed = true; 194 | } 195 | 196 | return c; 197 | } 198 | 199 | public void Rollback() 200 | { 201 | if (_disposed) 202 | throw new ObjectDisposedException("DbContextCollection"); 203 | if (_completed) 204 | throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there."); 205 | 206 | ExceptionDispatchInfo lastError = null; 207 | 208 | foreach (var dbContext in _initializedDbContexts.Values) 209 | { 210 | // There's no need to explicitly rollback changes in a DbContext as 211 | // DbContext doesn't save any changes until its SaveChanges() method is called. 212 | // So "rolling back" for a DbContext simply means not calling its SaveChanges() 213 | // method. 214 | 215 | // But if we've started an explicit database transaction, then we must roll it back. 216 | var tran = GetValueOrDefault(_transactions, dbContext); 217 | if (tran != null) 218 | { 219 | try 220 | { 221 | tran.Rollback(); 222 | tran.Dispose(); 223 | } 224 | catch (Exception e) 225 | { 226 | lastError = ExceptionDispatchInfo.Capture(e); 227 | } 228 | } 229 | } 230 | 231 | _transactions.Clear(); 232 | _completed = true; 233 | 234 | if (lastError != null) 235 | lastError.Throw(); // Re-throw while maintaining the exception's original stack track 236 | } 237 | 238 | public void Dispose() 239 | { 240 | if (_disposed) 241 | return; 242 | 243 | // Do our best here to dispose as much as we can even if we get errors along the way. 244 | // Now is not the time to throw. Correctly implemented applications will have called 245 | // either Commit() or Rollback() first and would have got the error there. 246 | 247 | if (!_completed) 248 | { 249 | try 250 | { 251 | if (_readOnly) Commit(); 252 | else Rollback(); 253 | } 254 | catch (Exception e) 255 | { 256 | System.Diagnostics.Debug.WriteLine(e); 257 | } 258 | } 259 | 260 | foreach (var dbContext in _initializedDbContexts.Values) 261 | { 262 | try 263 | { 264 | dbContext.Dispose(); 265 | } 266 | catch (Exception e) 267 | { 268 | System.Diagnostics.Debug.WriteLine(e); 269 | } 270 | } 271 | 272 | _initializedDbContexts.Clear(); 273 | _disposed = true; 274 | } 275 | 276 | /// 277 | /// Returns the value associated with the specified key or the default 278 | /// value for the TValue type. 279 | /// 280 | static TValue GetValueOrDefault(IDictionary dictionary, TKey key) 281 | { 282 | TValue value; 283 | return dictionary.TryGetValue(key, out value) ? value : default(TValue); 284 | } 285 | } 286 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/DbContextReadOnlyScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System.Data; 9 | using DbContextScope.EfCore.Enums; 10 | using DbContextScope.EfCore.Interfaces; 11 | 12 | namespace DbContextScope.EfCore.Implementations 13 | { 14 | public class DbContextReadOnlyScope : IDbContextReadOnlyScope 15 | { 16 | readonly DbContextScope _internalScope; 17 | 18 | public IDbContextCollection DbContexts { get { return _internalScope.DbContexts; } } 19 | 20 | public DbContextReadOnlyScope(IDbContextFactory dbContextFactory = null) 21 | : this(joiningOption: DbContextScopeOption.JoinExisting, isolationLevel: null, dbContextFactory: dbContextFactory) 22 | {} 23 | 24 | public DbContextReadOnlyScope(IsolationLevel isolationLevel, IDbContextFactory dbContextFactory = null) 25 | : this(joiningOption: DbContextScopeOption.ForceCreateNew, isolationLevel: isolationLevel, dbContextFactory: dbContextFactory) 26 | { } 27 | 28 | public DbContextReadOnlyScope(DbContextScopeOption joiningOption, IsolationLevel? isolationLevel, IDbContextFactory dbContextFactory = null) 29 | { 30 | _internalScope = new DbContextScope(joiningOption, true, isolationLevel, dbContextFactory); 31 | } 32 | 33 | public void Dispose() 34 | { 35 | _internalScope.Dispose(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/DbContextScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using System.Collections; 10 | using System.Data; 11 | using System.Linq; 12 | using System.Runtime.CompilerServices; 13 | using System.Runtime.Remoting.Messaging; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using DbContextScope.EfCore.Enums; 17 | using DbContextScope.EfCore.Interfaces; 18 | using Microsoft.EntityFrameworkCore; 19 | using Microsoft.EntityFrameworkCore.Infrastructure; 20 | 21 | namespace DbContextScope.EfCore.Implementations 22 | { 23 | public class DbContextScope : IDbContextScope 24 | { 25 | bool _disposed; 26 | bool _readOnly; 27 | bool _completed; 28 | bool _nested; 29 | DbContextScope _parentScope; 30 | DbContextCollection _dbContexts; 31 | 32 | public IDbContextCollection DbContexts { get { return _dbContexts; } } 33 | 34 | public DbContextScope(IDbContextFactory dbContextFactory = null) : 35 | this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: false, isolationLevel: null, dbContextFactory: dbContextFactory) 36 | {} 37 | 38 | public DbContextScope(bool readOnly, IDbContextFactory dbContextFactory = null) 39 | : this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: readOnly, isolationLevel: null, dbContextFactory: dbContextFactory) 40 | {} 41 | 42 | public DbContextScope(DbContextScopeOption joiningOption, bool readOnly, IsolationLevel? isolationLevel, IDbContextFactory dbContextFactory = null) 43 | { 44 | if (isolationLevel.HasValue && joiningOption == DbContextScopeOption.JoinExisting) 45 | throw new ArgumentException("Cannot join an ambient DbContextScope when an explicit database transaction is required. When requiring explicit database transactions to be used (i.e. when the 'isolationLevel' parameter is set), you must not also ask to join the ambient context (i.e. the 'joinAmbient' parameter must be set to false)."); 46 | 47 | _disposed = false; 48 | _completed = false; 49 | _readOnly = readOnly; 50 | 51 | _parentScope = GetAmbientScope(); 52 | if (_parentScope != null && joiningOption == DbContextScopeOption.JoinExisting) 53 | { 54 | if (_parentScope._readOnly && !this._readOnly) 55 | { 56 | throw new InvalidOperationException("Cannot nest a read/write DbContextScope within a read-only DbContextScope."); 57 | } 58 | 59 | _nested = true; 60 | _dbContexts = _parentScope._dbContexts; 61 | } 62 | else 63 | { 64 | _nested = false; 65 | _dbContexts = new DbContextCollection(readOnly, isolationLevel, dbContextFactory); 66 | } 67 | 68 | SetAmbientScope(this); 69 | } 70 | 71 | public int SaveChanges() 72 | { 73 | if (_disposed) 74 | throw new ObjectDisposedException("DbContextScope"); 75 | if (_completed) 76 | throw new InvalidOperationException("You cannot call SaveChanges() more than once on a DbContextScope. A DbContextScope is meant to encapsulate a business transaction: create the scope at the start of the business transaction and then call SaveChanges() at the end. Calling SaveChanges() mid-way through a business transaction doesn't make sense and most likely mean that you should refactor your service method into two separate service method that each create their own DbContextScope and each implement a single business transaction."); 77 | 78 | // Only save changes if we're not a nested scope. Otherwise, let the top-level scope 79 | // decide when the changes should be saved. 80 | var c = 0; 81 | if (!_nested) 82 | { 83 | c = CommitInternal(); 84 | } 85 | 86 | _completed = true; 87 | 88 | return c; 89 | } 90 | 91 | public Task SaveChangesAsync() 92 | { 93 | return SaveChangesAsync(CancellationToken.None); 94 | } 95 | 96 | public async Task SaveChangesAsync(CancellationToken cancelToken) 97 | { 98 | if (_disposed) 99 | throw new ObjectDisposedException("DbContextScope"); 100 | if (_completed) 101 | throw new InvalidOperationException("You cannot call SaveChanges() more than once on a DbContextScope. A DbContextScope is meant to encapsulate a business transaction: create the scope at the start of the business transaction and then call SaveChanges() at the end. Calling SaveChanges() mid-way through a business transaction doesn't make sense and most likely mean that you should refactor your service method into two separate service method that each create their own DbContextScope and each implement a single business transaction."); 102 | 103 | // Only save changes if we're not a nested scope. Otherwise, let the top-level scope 104 | // decide when the changes should be saved. 105 | var c = 0; 106 | if (!_nested) 107 | { 108 | c = await CommitInternalAsync(cancelToken).ConfigureAwait(false); 109 | } 110 | 111 | _completed = true; 112 | return c; 113 | } 114 | 115 | int CommitInternal() 116 | { 117 | return _dbContexts.Commit(); 118 | } 119 | 120 | Task CommitInternalAsync(CancellationToken cancelToken) 121 | { 122 | return _dbContexts.CommitAsync(cancelToken); 123 | } 124 | 125 | void RollbackInternal() 126 | { 127 | _dbContexts.Rollback(); 128 | } 129 | 130 | public void RefreshEntitiesInParentScope(IEnumerable entities) 131 | { 132 | if (entities == null) 133 | return; 134 | 135 | if (_parentScope == null) 136 | return; 137 | 138 | if (_nested) // The parent scope uses the same DbContext instances as we do - no need to refresh anything 139 | return; 140 | 141 | // OK, so we must loop through all the DbContext instances in the parent scope 142 | // and see if their first-level cache (i.e. their ObjectStateManager) contains the provided entities. 143 | // If they do, we'll need to force a refresh from the database. 144 | 145 | // I'm sorry for this code but it's the only way to do this with the current version of Entity Framework 146 | // as far as I can see. 147 | 148 | // What would be much nicer would be to have a way to merge all the modified / added / deleted 149 | // entities from one DbContext instance to another. NHibernate has support for this sort of stuff 150 | // but EF still lags behind in this respect. But there is hope: https://entityframework.codeplex.com/workitem/864 151 | 152 | foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) 153 | { 154 | var correspondingParentContext = 155 | _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => parentContext.GetType() == contextInCurrentScope.GetType()); 156 | 157 | if (correspondingParentContext == null) 158 | continue; // No DbContext of this type has been created in the parent scope yet. So no need to refresh anything for this DbContext type. 159 | 160 | // Both our scope and the parent scope have an instance of the same DbContext type. 161 | // We can now look in the parent DbContext instance for entities that need to 162 | // be refreshed. 163 | foreach (var toRefresh in entities) 164 | { 165 | // First, we need to find what the EntityKey for this entity is. 166 | // We need this EntityKey in order to check if this entity has 167 | // already been loaded in the parent DbContext. 168 | var stateInCurrentScope = contextInCurrentScope.ChangeTracker.GetInfrastructure().TryGetEntry(toRefresh); 169 | if (stateInCurrentScope != null) 170 | { 171 | // Now we can see if that entity exists in the parent DbContext instance and refresh it. 172 | var pk = stateInCurrentScope.EntityType.FindPrimaryKey().Properties.Single(); 173 | var pkValue = stateInCurrentScope 174 | .Entity 175 | .GetType() 176 | .GetProperty(pk.Name) 177 | .GetValue(stateInCurrentScope.Entity); 178 | var stateInParentScope = correspondingParentContext 179 | .ChangeTracker 180 | .GetInfrastructure() 181 | .Entries 182 | .SingleOrDefault(e => e.Entity.GetType() == stateInCurrentScope.Entity.GetType() 183 | && e.Entity.GetType().GetProperty(pk.Name).GetValue(e.Entity).Equals(pkValue)); 184 | 185 | if (stateInParentScope != null) 186 | { 187 | // Only refresh the entity in the parent DbContext from the database if that entity hasn't already been 188 | // modified in the parent. Otherwise, let the whatever concurency rules the application uses 189 | // apply. 190 | if (stateInParentScope.EntityState == EntityState.Unchanged) 191 | { 192 | // TODO: Update the entity when EF Core implements missing ChangeTracker API 193 | throw new NotImplementedException("There is no way of refreshing entities between DbContexts in EF7-rc1."); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | public Task RefreshEntitiesInParentScopeAsync(IEnumerable entities) 202 | { 203 | // See comments in the sync version of this method for an explanation of what we're doing here. 204 | 205 | if (entities == null) 206 | return Task.FromResult(0); 207 | 208 | if (_parentScope == null) 209 | return Task.FromResult(0); 210 | 211 | if (_nested) 212 | return Task.FromResult(0); 213 | 214 | foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) 215 | { 216 | var correspondingParentContext = 217 | _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => parentContext.GetType() == contextInCurrentScope.GetType()); 218 | 219 | if (correspondingParentContext == null) 220 | continue; 221 | 222 | foreach (var toRefresh in entities) 223 | { 224 | var stateInCurrentScope = contextInCurrentScope.ChangeTracker.GetInfrastructure().TryGetEntry(toRefresh); 225 | if (stateInCurrentScope != null) 226 | { 227 | var pk = stateInCurrentScope.EntityType.FindPrimaryKey().Properties.Single(); 228 | var pkValue = stateInCurrentScope 229 | .Entity 230 | .GetType() 231 | .GetProperty(pk.Name) 232 | .GetValue(stateInCurrentScope.Entity); 233 | var stateInParentScope = correspondingParentContext 234 | .ChangeTracker 235 | .GetInfrastructure() 236 | .Entries 237 | .SingleOrDefault(e => e.Entity.GetType() == stateInCurrentScope.Entity.GetType() 238 | && e.Entity.GetType().GetProperty(pk.Name).GetValue(e.Entity).Equals(pkValue)); 239 | 240 | if (stateInParentScope != null) 241 | { 242 | if (stateInParentScope.EntityState == EntityState.Unchanged) 243 | { 244 | // TODO: Update the entity when EF7 implements missing ChangeTracker API 245 | var tcs = new TaskCompletionSource(); 246 | tcs.SetException(new NotImplementedException("There is no way of refreshing entities between DbContexts in EF7.")); 247 | return tcs.Task; 248 | } 249 | } 250 | } 251 | } 252 | } 253 | return Task.FromResult(0); 254 | } 255 | 256 | public void Dispose() 257 | { 258 | if (_disposed) 259 | return; 260 | 261 | // Commit / Rollback and dispose all of our DbContext instances 262 | if (!_nested) 263 | { 264 | if (!_completed) 265 | { 266 | // Do our best to clean up as much as we can but don't throw here as it's too late anyway. 267 | try 268 | { 269 | if (_readOnly) 270 | { 271 | // Disposing a read-only scope before having called its SaveChanges() method 272 | // is the normal and expected behavior. Read-only scopes get committed automatically. 273 | CommitInternal(); 274 | } 275 | else 276 | { 277 | // Disposing a read/write scope before having called its SaveChanges() method 278 | // indicates that something went wrong and that all changes should be rolled-back. 279 | RollbackInternal(); 280 | } 281 | } 282 | catch (Exception e) 283 | { 284 | System.Diagnostics.Debug.WriteLine(e); 285 | } 286 | 287 | _completed = true; 288 | } 289 | 290 | _dbContexts.Dispose(); 291 | } 292 | 293 | // Pop ourself from the ambient scope stack 294 | var currentAmbientScope = GetAmbientScope(); 295 | if (currentAmbientScope != this) // This is a serious programming error. Worth throwing here. 296 | throw new InvalidOperationException("DbContextScope instances must be disposed of in the order in which they were created!"); 297 | 298 | RemoveAmbientScope(); 299 | 300 | if (_parentScope != null) 301 | { 302 | if (_parentScope._disposed) 303 | { 304 | /* 305 | * If our parent scope has been disposed before us, it can only mean one thing: 306 | * someone started a parallel flow of execution and forgot to suppress the 307 | * ambient context before doing so. And we've been created in that parallel flow. 308 | * 309 | * Since the CallContext flows through all async points, the ambient scope in the 310 | * main flow of execution ended up becoming the ambient scope in this parallel flow 311 | * of execution as well. So when we were created, we captured it as our "parent scope". 312 | * 313 | * The main flow of execution then completed while our flow was still ongoing. When 314 | * the main flow of execution completed, the ambient scope there (which we think is our 315 | * parent scope) got disposed of as it should. 316 | * 317 | * So here we are: our parent scope isn't actually our parent scope. It was the ambient 318 | * scope in the main flow of execution from which we branched off. We should never have seen 319 | * it. Whoever wrote the code that created this parallel task should have suppressed 320 | * the ambient context before creating the task - that way we wouldn't have captured 321 | * this bogus parent scope. 322 | * 323 | * While this is definitely a programming error, it's not worth throwing here. We can only 324 | * be in one of two scenario: 325 | * 326 | * - If the developer who created the parallel task was mindful to force the creation of 327 | * a new scope in the parallel task (with IDbContextScopeFactory.CreateNew() instead of 328 | * JoinOrCreate()) then no harm has been done. We haven't tried to access the same DbContext 329 | * instance from multiple threads. 330 | * 331 | * - If this was not the case, they probably already got an exception complaining about the same 332 | * DbContext or ObjectContext being accessed from multiple threads simultaneously (or a related 333 | * error like multiple active result sets on a DataReader, which is caused by attempting to execute 334 | * several queries in parallel on the same DbContext instance). So the code has already blow up. 335 | * 336 | * So just record a warning here. Hopefully someone will see it and will fix the code. 337 | */ 338 | 339 | var message = @"PROGRAMMING ERROR - When attempting to dispose a DbContextScope, we found that our parent DbContextScope has already been disposed! This means that someone started a parallel flow of execution (e.g. created a TPL task, created a thread or enqueued a work item on the ThreadPool) within the context of a DbContextScope without suppressing the ambient context first. 340 | 341 | In order to fix this: 342 | 1) Look at the stack trace below - this is the stack trace of the parallel task in question. 343 | 2) Find out where this parallel task was created. 344 | 3) Change the code so that the ambient context is suppressed before the parallel task is created. You can do this with IDbContextScopeFactory.SuppressAmbientContext() (wrap the parallel task creation code block in this). 345 | 346 | Stack Trace: 347 | " + Environment.StackTrace; 348 | 349 | System.Diagnostics.Debug.WriteLine(message); 350 | } 351 | else 352 | { 353 | SetAmbientScope(_parentScope); 354 | } 355 | } 356 | 357 | _disposed = true; 358 | 359 | } 360 | 361 | #region Ambient Context Logic 362 | 363 | /* 364 | * This is where all the magic happens. And there is not much of it. 365 | * 366 | * This implementation is inspired by the source code of the 367 | * TransactionScope class in .NET 4.5.1 (the TransactionScope class 368 | * is prior versions of the .NET Fx didn't have support for async 369 | * operations). 370 | * 371 | * In order to understand this, you'll need to be familiar with the 372 | * concept of async points. You'll also need to be familiar with the 373 | * ExecutionContext and CallContext and understand how and why they 374 | * flow through async points. Stephen Toub has written an 375 | * excellent blog post about this - it's a highly recommended read: 376 | * http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx 377 | * 378 | * Overview: 379 | * 380 | * We want our DbContextScope instances to be ambient within 381 | * the context of a logical flow of execution. This flow may be 382 | * synchronous or it may be asynchronous. 383 | * 384 | * If we only wanted to support the synchronous flow scenario, 385 | * we could just store our DbContextScope instances in a ThreadStatic 386 | * variable. That's the "traditional" (i.e. pre-async) way of implementing 387 | * an ambient context in .NET. You can see an example implementation of 388 | * a TheadStatic-based ambient DbContext here: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/ 389 | * 390 | * But that would be hugely limiting as it would prevent us from being 391 | * able to use the new async features added to Entity Framework 392 | * in EF6 and .NET 4.5. 393 | * 394 | * So we need a storage place for our DbContextScope instances 395 | * that can flow through async points so that the ambient context is still 396 | * available after an await (or any other async point). And this is exactly 397 | * what CallContext is for. 398 | * 399 | * There are however two issues with storing our DbContextScope instances 400 | * in the CallContext: 401 | * 402 | * 1) Items stored in the CallContext should be serializable. That's because 403 | * the CallContext flows not just through async points but also through app domain 404 | * boundaries. I.e. if you make a remoting call into another app domain, the 405 | * CallContext will flow through this call (which will require all the values it 406 | * stores to get serialized) and get restored in the other app domain. 407 | * 408 | * In our case, our DbContextScope instances aren't serializable. And in any case, 409 | * we most definitely don't want them to be flown accross app domains. So we'll 410 | * use the trick used by the TransactionScope class to work around this issue. 411 | * Instead of storing our DbContextScope instances themselves in the CallContext, 412 | * we'll just generate a unique key for each instance and only store that key in 413 | * the CallContext. We'll then store the actual DbContextScope instances in a static 414 | * Dictionary against their key. 415 | * 416 | * That way, if an app domain boundary is crossed, the keys will be flown accross 417 | * but not the DbContextScope instances since a static variable is stored at the 418 | * app domain level. The code executing in the other app domain won't see the ambient 419 | * DbContextScope created in the first app domain and will therefore be able to create 420 | * their own ambient DbContextScope if necessary. 421 | * 422 | * 2) The CallContext is flow through *all* async points. This means that if someone 423 | * decides to create multiple threads within the scope of a DbContextScope, our ambient scope 424 | * will flow through all the threads. Which means that all the threads will see that single 425 | * DbContextScope instance as being their ambient DbContext. So clients need to be 426 | * careful to always suppress the ambient context before kicking off a parallel operation 427 | * to avoid our DbContext instances from being accessed from multiple threads. 428 | * 429 | */ 430 | 431 | static readonly string AmbientDbContextScopeKey = "AmbientDbcontext_" + Guid.NewGuid(); 432 | 433 | // Use a ConditionalWeakTable instead of a simple ConcurrentDictionary to store our DbContextScope instances 434 | // in order to prevent leaking DbContextScope instances if someone doesn't dispose them properly. 435 | // 436 | // For example, if we used a ConcurrentDictionary and someone let go of a DbContextScope instance without 437 | // disposing it, our ConcurrentDictionary would still have a reference to it, preventing 438 | // the GC from being able to collect it => leak. With a ConditionalWeakTable, we don't hold a reference 439 | // to the DbContextScope instances we store in there, allowing them to get GCed. 440 | // The doc for ConditionalWeakTable isn't the best. This SO anser does a good job at explaining what 441 | // it does: http://stackoverflow.com/a/18613811 442 | static readonly ConditionalWeakTable DbContextScopeInstances = new ConditionalWeakTable(); 443 | 444 | InstanceIdentifier _instanceIdentifier = new InstanceIdentifier(); 445 | 446 | /// 447 | /// Makes the provided 'dbContextScope' available as the the ambient scope via the CallContext. 448 | /// 449 | internal static void SetAmbientScope(DbContextScope newAmbientScope) 450 | { 451 | if (newAmbientScope == null) 452 | throw new ArgumentNullException(nameof(newAmbientScope)); 453 | 454 | var current = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; 455 | 456 | if (current == newAmbientScope._instanceIdentifier) 457 | return; 458 | 459 | // Store the new scope's instance identifier in the CallContext, making it the ambient scope 460 | CallContext.LogicalSetData(AmbientDbContextScopeKey, newAmbientScope._instanceIdentifier); 461 | 462 | // Keep track of this instance (or do nothing if we're already tracking it) 463 | DbContextScopeInstances.GetValue(newAmbientScope._instanceIdentifier, key => newAmbientScope); 464 | } 465 | 466 | /// 467 | /// Clears the ambient scope from the CallContext and stops tracking its instance. 468 | /// Call this when a DbContextScope is being disposed. 469 | /// 470 | internal static void RemoveAmbientScope() 471 | { 472 | var current = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; 473 | CallContext.LogicalSetData(AmbientDbContextScopeKey, null); 474 | 475 | // If there was an ambient scope, we can stop tracking it now 476 | if (current != null) 477 | { 478 | DbContextScopeInstances.Remove(current); 479 | } 480 | } 481 | 482 | /// 483 | /// Clears the ambient scope from the CallContext but keeps tracking its instance. Call this to temporarily 484 | /// hide the ambient context (e.g. to prevent it from being captured by parallel task). 485 | /// 486 | internal static void HideAmbientScope() 487 | { 488 | CallContext.LogicalSetData(AmbientDbContextScopeKey, null); 489 | } 490 | 491 | /// 492 | /// Get the current ambient scope or null if no ambient scope has been setup. 493 | /// 494 | internal static DbContextScope GetAmbientScope() 495 | { 496 | // Retrieve the identifier of the ambient scope (if any) 497 | var instanceIdentifier = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; 498 | if (instanceIdentifier == null) 499 | return null; // Either no ambient context has been set or we've crossed an app domain boundary and have (intentionally) lost the ambient context 500 | 501 | // Retrieve the DbContextScope instance corresponding to this identifier 502 | DbContextScope ambientScope; 503 | if (DbContextScopeInstances.TryGetValue(instanceIdentifier, out ambientScope)) 504 | return ambientScope; 505 | 506 | // We have an instance identifier in the CallContext but no corresponding instance 507 | // in our DbContextScopeInstances table. This should never happen! The only place where 508 | // we remove the instance from the DbContextScopeInstances table is in RemoveAmbientScope(), 509 | // which also removes the instance identifier from the CallContext. 510 | // 511 | // There's only one scenario where this could happen: someone let go of a DbContextScope 512 | // instance without disposing it. In that case, the CallContext 513 | // would still contain a reference to the scope and we'd still have that scope's instance 514 | // in our DbContextScopeInstances table. But since we use a ConditionalWeakTable to store 515 | // our DbContextScope instances and are therefore only holding a weak reference to these instances, 516 | // the GC would be able to collect it. Once collected by the GC, our ConditionalWeakTable will return 517 | // null when queried for that instance. In that case, we're OK. This is a programming error 518 | // but our use of a ConditionalWeakTable prevented a leak. 519 | System.Diagnostics.Debug.WriteLine("Programming error detected. Found a reference to an ambient DbContextScope in the CallContext but didn't have an instance for it in our DbContextScopeInstances table. This most likely means that this DbContextScope instance wasn't disposed of properly. DbContextScope instance must always be disposed. Review the code for any DbContextScope instance used outside of a 'using' block and fix it so that all DbContextScope instances are disposed of."); 520 | return null; 521 | } 522 | 523 | #endregion 524 | } 525 | 526 | /* 527 | * The idea of using an object reference as our instance identifier 528 | * instead of simply using a unique string (which we could have generated 529 | * with Guid.NewGuid() for example) comes from the TransactionScope 530 | * class. As far as I can make out, a string would have worked just fine. 531 | * I'm guessing that this is done for optimization purposes. Creating 532 | * an empty class is cheaper and uses up less memory than generating 533 | * a unique string. 534 | */ 535 | class InstanceIdentifier : MarshalByRefObject 536 | { } 537 | } 538 | 539 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Implementations/DbContextScopeFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using System.Data; 10 | using DbContextScope.EfCore.Enums; 11 | using DbContextScope.EfCore.Interfaces; 12 | 13 | namespace DbContextScope.EfCore.Implementations 14 | { 15 | public class DbContextScopeFactory : IDbContextScopeFactory 16 | { 17 | readonly IDbContextFactory _dbContextFactory; 18 | 19 | public DbContextScopeFactory(IDbContextFactory dbContextFactory = null) 20 | { 21 | _dbContextFactory = dbContextFactory; 22 | } 23 | 24 | public IDbContextScope Create(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting) 25 | { 26 | return new DbContextScope(joiningOption, false, null, _dbContextFactory); 27 | } 28 | 29 | public IDbContextReadOnlyScope CreateReadOnly(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting) 30 | { 31 | return new DbContextReadOnlyScope(joiningOption, null, _dbContextFactory); 32 | } 33 | 34 | public IDbContextScope CreateWithTransaction(IsolationLevel isolationLevel) 35 | { 36 | return new DbContextScope(DbContextScopeOption.ForceCreateNew, false, isolationLevel, _dbContextFactory); 37 | } 38 | 39 | public IDbContextReadOnlyScope CreateReadOnlyWithTransaction(IsolationLevel isolationLevel) 40 | { 41 | return new DbContextReadOnlyScope(DbContextScopeOption.ForceCreateNew, isolationLevel, _dbContextFactory); 42 | } 43 | 44 | public IDisposable SuppressAmbientContext() 45 | { 46 | return new AmbientContextSuppressor(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IAmbientDbContextLocator.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace DbContextScope.EfCore.Interfaces 11 | { 12 | /// 13 | /// Convenience methods to retrieve ambient DbContext instances. 14 | /// 15 | public interface IAmbientDbContextLocator 16 | { 17 | /// 18 | /// If called within the scope of a DbContextScope, gets or creates 19 | /// the ambient DbContext instance for the provided DbContext type. 20 | /// 21 | /// Otherwise returns null. 22 | /// 23 | TDbContext Get() where TDbContext : DbContext; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IDbContextCollection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace DbContextScope.EfCore.Interfaces 12 | { 13 | /// 14 | /// Maintains a list of lazily-created DbContext instances. 15 | /// 16 | public interface IDbContextCollection : IDisposable 17 | { 18 | /// 19 | /// Get or create a DbContext instance of the specified type. 20 | /// 21 | TDbContext Get() where TDbContext : DbContext; 22 | } 23 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace DbContextScope.EfCore.Interfaces 11 | { 12 | /// 13 | /// Factory for DbContext-derived classes that don't expose 14 | /// a default constructor. 15 | /// 16 | /// 17 | /// If your DbContext-derived classes have a default constructor, 18 | /// you can ignore this factory. DbContextScope will take care of 19 | /// instanciating your DbContext class with Activator.CreateInstance() 20 | /// when needed. 21 | /// 22 | /// If your DbContext-derived classes don't expose a default constructor 23 | /// however, you must impement this interface and provide it to DbContextScope 24 | /// so that it can create instances of your DbContexts. 25 | /// 26 | /// A typical situation where this would be needed is in the case of your DbContext-derived 27 | /// class having a dependency on some other component in your application. For example, 28 | /// some data in your database may be encrypted and you might want your DbContext-derived 29 | /// class to automatically decrypt this data on entity materialization. It would therefore 30 | /// have a mandatory dependency on an IDataDecryptor component that knows how to do that. 31 | /// In that case, you'll want to implement this interface and pass it to the DbContextScope 32 | /// you're creating so that DbContextScope is able to create your DbContext instances correctly. 33 | /// 34 | public interface IDbContextFactory 35 | { 36 | TDbContext CreateDbContext() where TDbContext : DbContext; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IDbContextReadOnlyScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | 10 | namespace DbContextScope.EfCore.Interfaces 11 | { 12 | /// 13 | /// A read-only DbContextScope. Refer to the comments for IDbContextScope 14 | /// for more details. 15 | /// 16 | public interface IDbContextReadOnlyScope : IDisposable 17 | { 18 | /// 19 | /// The DbContext instances that this DbContextScope manages. 20 | /// 21 | IDbContextCollection DbContexts { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IDbContextScope.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using System.Collections; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace DbContextScope.EfCore.Interfaces 14 | { 15 | /// 16 | /// Creates and manages the DbContext instances used by this code block. 17 | /// 18 | /// You typically use a DbContextScope at the business logic service level. Each 19 | /// business transaction (i.e. each service method) that uses Entity Framework must 20 | /// be wrapped in a DbContextScope, ensuring that the same DbContext instances 21 | /// are used throughout the business transaction and are committed or rolled 22 | /// back atomically. 23 | /// 24 | /// Think of it as TransactionScope but for managing DbContext instances instead 25 | /// of database transactions. Just like a TransactionScope, a DbContextScope is 26 | /// ambient, can be nested and supports async execution flows. 27 | /// 28 | /// And just like TransactionScope, it does not support parallel execution flows. 29 | /// You therefore MUST suppress the ambient DbContextScope before kicking off parallel 30 | /// tasks or you will end up with multiple threads attempting to use the same DbContext 31 | /// instances (use IDbContextScopeFactory.SuppressAmbientContext() for this). 32 | /// 33 | /// You can access the DbContext instances that this scopes manages via either: 34 | /// - its DbContexts property, or 35 | /// - an IAmbientDbContextLocator 36 | /// 37 | /// (you would typically use the later in the repository / query layer to allow your repository 38 | /// or query classes to access the ambient DbContext instances without giving them access to the actual 39 | /// DbContextScope). 40 | /// 41 | /// 42 | public interface IDbContextScope : IDisposable 43 | { 44 | /// 45 | /// Saves the changes in all the DbContext instances that were created within this scope. 46 | /// This method can only be called once per scope. 47 | /// 48 | int SaveChanges(); 49 | 50 | /// 51 | /// Saves the changes in all the DbContext instances that were created within this scope. 52 | /// This method can only be called once per scope. 53 | /// 54 | Task SaveChangesAsync(); 55 | 56 | /// 57 | /// Saves the changes in all the DbContext instances that were created within this scope. 58 | /// This method can only be called once per scope. 59 | /// 60 | Task SaveChangesAsync(CancellationToken cancelToken); 61 | 62 | /// 63 | /// Reloads the provided persistent entities from the data store 64 | /// in the DbContext instances managed by the parent scope. 65 | /// 66 | /// If there is no parent scope (i.e. if this DbContextScope 67 | /// if the top-level scope), does nothing. 68 | /// 69 | /// This is useful when you have forced the creation of a new 70 | /// DbContextScope and want to make sure that the parent scope 71 | /// (if any) is aware of the entities you've modified in the 72 | /// inner scope. 73 | /// 74 | /// (this is a pretty advanced feature that should be used 75 | /// with parsimony). 76 | /// 77 | void RefreshEntitiesInParentScope(IEnumerable entities); 78 | 79 | /// 80 | /// Reloads the provided persistent entities from the data store 81 | /// in the DbContext instances managed by the parent scope. 82 | /// 83 | /// If there is no parent scope (i.e. if this DbContextScope 84 | /// if the top-level scope), does nothing. 85 | /// 86 | /// This is useful when you have forced the creation of a new 87 | /// DbContextScope and want to make sure that the parent scope 88 | /// (if any) is aware of the entities you've modified in the 89 | /// inner scope. 90 | /// 91 | /// (this is a pretty advanced feature that should be used 92 | /// with parsimony). 93 | /// 94 | Task RefreshEntitiesInParentScopeAsync(IEnumerable entities); 95 | 96 | /// 97 | /// The DbContext instances that this DbContextScope manages. Don't call SaveChanges() on the DbContext themselves! 98 | /// Save the scope instead. 99 | /// 100 | IDbContextCollection DbContexts { get; } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Interfaces/IDbContextScopeFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Mehdi El Gueddari 3 | * http://mehdi.me 4 | * 5 | * This software may be modified and distributed under the terms 6 | * of the MIT license. See the LICENSE file for details. 7 | */ 8 | using System; 9 | using System.Data; 10 | using DbContextScope.EfCore.Enums; 11 | 12 | namespace DbContextScope.EfCore.Interfaces 13 | { 14 | /// 15 | /// Convenience methods to create a new ambient DbContextScope. This is the prefered method 16 | /// to create a DbContextScope. 17 | /// 18 | public interface IDbContextScopeFactory 19 | { 20 | /// 21 | /// Creates a new DbContextScope. 22 | /// 23 | /// By default, the new scope will join the existing ambient scope. This 24 | /// is what you want in most cases. This ensures that the same DbContext instances 25 | /// are used by all services methods called within the scope of a business transaction. 26 | /// 27 | /// Set 'joiningOption' to 'ForceCreateNew' if you want to ignore the ambient scope 28 | /// and force the creation of new DbContext instances within that scope. Using 'ForceCreateNew' 29 | /// is an advanced feature that should be used with great care and only if you fully understand the 30 | /// implications of doing this. 31 | /// 32 | IDbContextScope Create(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting); 33 | 34 | /// 35 | /// Creates a new DbContextScope for read-only queries. 36 | /// 37 | /// By default, the new scope will join the existing ambient scope. This 38 | /// is what you want in most cases. This ensures that the same DbContext instances 39 | /// are used by all services methods called within the scope of a business transaction. 40 | /// 41 | /// Set 'joiningOption' to 'ForceCreateNew' if you want to ignore the ambient scope 42 | /// and force the creation of new DbContext instances within that scope. Using 'ForceCreateNew' 43 | /// is an advanced feature that should be used with great care and only if you fully understand the 44 | /// implications of doing this. 45 | /// 46 | IDbContextReadOnlyScope CreateReadOnly(DbContextScopeOption joiningOption = DbContextScopeOption.JoinExisting); 47 | 48 | /// 49 | /// Forces the creation of a new ambient DbContextScope (i.e. does not 50 | /// join the ambient scope if there is one) and wraps all DbContext instances 51 | /// created within that scope in an explicit database transaction with 52 | /// the provided isolation level. 53 | /// 54 | /// WARNING: the database transaction will remain open for the whole 55 | /// duration of the scope! So keep the scope as short-lived as possible. 56 | /// Don't make any remote API calls or perform any long running computation 57 | /// within that scope. 58 | /// 59 | /// This is an advanced feature that you should use very carefully 60 | /// and only if you fully understand the implications of doing this. 61 | /// 62 | IDbContextScope CreateWithTransaction(IsolationLevel isolationLevel); 63 | 64 | /// 65 | /// Forces the creation of a new ambient read-only DbContextScope (i.e. does not 66 | /// join the ambient scope if there is one) and wraps all DbContext instances 67 | /// created within that scope in an explicit database transaction with 68 | /// the provided isolation level. 69 | /// 70 | /// WARNING: the database transaction will remain open for the whole 71 | /// duration of the scope! So keep the scope as short-lived as possible. 72 | /// Don't make any remote API calls or perform any long running computation 73 | /// within that scope. 74 | /// 75 | /// This is an advanced feature that you should use very carefully 76 | /// and only if you fully understand the implications of doing this. 77 | /// 78 | IDbContextReadOnlyScope CreateReadOnlyWithTransaction(IsolationLevel isolationLevel); 79 | 80 | /// 81 | /// Temporarily suppresses the ambient DbContextScope. 82 | /// 83 | /// Always use this if you need to kick off parallel tasks within a DbContextScope. 84 | /// This will prevent the parallel tasks from using the current ambient scope. If you 85 | /// were to kick off parallel tasks within a DbContextScope without suppressing the ambient 86 | /// context first, all the parallel tasks would end up using the same ambient DbContextScope, which 87 | /// would result in multiple threads accesssing the same DbContext instances at the same 88 | /// time. 89 | /// 90 | IDisposable SuppressAmbientContext(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("DbContextScope.EfCore")] 8 | [assembly: AssemblyDescription("A simple and flexible way to manage your Entity Framework DbContext instances.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Mehdi El Gueddari")] 11 | [assembly: AssemblyProduct("DbContextScope.EfCore")] 12 | [assembly: AssemblyCopyright("Copyright © 2015 Mehdi El Gueddari")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("4e66338f-702c-4511-850f-0f89186b1286")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /src/DbContextScope.EfCore/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "DbContextScope.EfCore", 3 | "version": "1.2.0", 4 | "description": "DbContextScope is a simple and flexible way to manage your Entity Framework Core relational DbContext instances.", 5 | "authors": [ "Mehdi El Gueddari, Camilo Fierro" ], 6 | 7 | "dependencies": { 8 | "Microsoft.EntityFrameworkCore.Relational": "1.0.0" 9 | }, 10 | 11 | "frameworks": { 12 | "net452": { } 13 | }, 14 | 15 | "packOptions": { 16 | "licenseUrl": "", 17 | "projectUrl": "https://github.com/ninety7/DbContextScope", 18 | "tags": [ "EntityFramework, ASP.NET, DbContext, DbContextScope" ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/DbContextScope.UnitOfWork.Core.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 1c3207f7-9381-453f-8783-b3d52a0c9b8b 10 | DbContextScope.UnitOfWork.Core 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | True 19 | 20 | 21 | True 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/Interfaces/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DbContextScope.UnitOfWork.Core.Interfaces 5 | { 6 | /// 7 | /// Defines Unit Of Work methods. 8 | /// 9 | public interface IUnitOfWork : IDisposable 10 | { 11 | /// 12 | /// Saves the changes made to the repositories. 13 | /// 14 | void Save(); 15 | 16 | /// 17 | /// Saves the changes made to the repositories asynchronously. 18 | /// 19 | /// 20 | Task SaveAsync(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DbContextScope.UnitOfWork.Core")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DbContextScope.UnitOfWork.Core")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1c3207f7-9381-453f-8783-b3d52a0c9b8b")] 24 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/Repository/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace DbContextScope.UnitOfWork.Core.Repository 8 | { 9 | /// 10 | /// Defines a generic repository that can be implemented 11 | /// using any data store or library. 12 | /// 13 | /// Type of the entities that the repository will manage. 14 | public interface IRepository where TEntity : class 15 | { 16 | /// 17 | /// Adds the given entity to the Unit of Work such that it will be inserted 18 | /// into the data store when Save is called on the Unit of Work. 19 | /// 20 | /// The entity to add. 21 | void Add(TEntity entity); 22 | 23 | /// 24 | /// Returns the representation of the data store. 25 | /// 26 | /// An that represents the data store. 27 | IQueryable AsQueryable(); 28 | 29 | /// 30 | /// Attaches the given entity to the Unit of Work. That is, the entity is placed into 31 | /// the Unit of Work in the Unchanged state, just as if it had been read from the data store. 32 | /// 33 | /// The entity to attach. 34 | void Attach(TEntity entity); 35 | 36 | /// 37 | /// Marks the given entity as Deleted such that it will be deleted from the data store when 38 | /// Save is called. Note that the entity must exist in the Unit of Work in some other state 39 | /// before this method is called. 40 | /// 41 | /// The entity to remove. 42 | void Delete(TEntity entity); 43 | 44 | /// 45 | /// Marks the given entity as Modified such that its changes will be saved when Save is called. 46 | /// 47 | /// The entity to mark as Modified. 48 | void Edit(TEntity entity); 49 | 50 | /// 51 | /// Returns the first entity in the data store that satisfies a specified condition. If no 52 | /// condition was specified, returns the first entity in the data store. 53 | /// 54 | /// A function to test each element for a condition. 55 | /// 56 | /// The first entity in the data store that passes the test in the specified predicate 57 | /// function. If no condition was specified, the first entity in the data store. 58 | /// 59 | TEntity First(Expression> predicate = null); 60 | 61 | /// 62 | /// Returns the first entity in the data store that satisfies a specified condition. If no 63 | /// condition was specified, returns the first entity in the data store. If no such entity 64 | /// is found, retuns a default value. 65 | /// 66 | /// A function to test each element for a condition. 67 | /// 68 | /// The first entity in the data store that passes the test in the specified predicate 69 | /// function. If no condition was specified, the first entity in the data store. If 70 | /// no entity passes the test in the predicate, a default value. 71 | /// 72 | TEntity FirstOrDefault(Expression> predicate = null); 73 | 74 | /// 75 | /// Asynchronously returns the first entity in the data store that satisfies a specified 76 | /// condition. If no condition was specified, returns the first entity in the data store. 77 | /// If no such entity is found, retuns a default value. 78 | /// 79 | /// A function to test each element for a condition. 80 | /// 81 | /// A task that represents the first entity in the data store that passes the test in the 82 | /// specified predicate function. If no condition was specified, a task that represents the 83 | /// first entity in the data store. If no entity passes the test in the predicate, task that 84 | /// represents a default value. 85 | /// 86 | Task FirstOrDefaultAsync(Expression> predicate = null); 87 | 88 | IEnumerable Get(Expression> predicate = null, 89 | Func, IOrderedQueryable> orderBy = null, params Expression>[] includeProperties); 90 | 91 | Task> GetAsync(Expression> predicate = null, 92 | Func, IOrderedQueryable> orderBy = null, params Expression>[] includeProperties); 93 | 94 | TEntity LastOrDefault(Expression> predicate = null); 95 | 96 | TEntity Single(Expression> predicate); 97 | 98 | Task SingleAsync(Expression> predicate); 99 | 100 | TEntity SingleOrDefault(Expression> predicate); 101 | 102 | Task SingleOrDefaultAsync(Expression> predicate); 103 | 104 | void Update(TEntity entityToUpdate); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "DbContextScope.UnitOfWork.Core", 3 | "version": "1.2.0", 4 | "description": "This package contains base interfaces that enable developers to implement Unit Of Work and Repository patterns with data stores.", 5 | "authors": [ "Camilo Fierro" ], 6 | 7 | "frameworks": { 8 | "net452": { } 9 | }, 10 | 11 | "packOptions": { 12 | "tags": [ "Unit of Work, Repository, Pattern, Data, EntityFramework, ASP.NET, DbContext" ], 13 | "projectUrl": "https://github.com/ninety7/DbContextScope", 14 | "licenseUrl": "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.Core/project.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "locked": false, 3 | "version": 2, 4 | "targets": { 5 | ".NETFramework,Version=v4.5.2": {} 6 | }, 7 | "libraries": {}, 8 | "projectFileDependencyGroups": { 9 | "": [], 10 | ".NETFramework,Version=v4.5.2": [] 11 | }, 12 | "tools": {}, 13 | "projectFileToolGroups": {} 14 | } -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/DbContextScope.UnitOfWork.EfCore.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | e053ca11-b054-46d7-af4e-84aeff0aa1e3 10 | DbContextScope.UnitOfWork.Ef7 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | True 19 | 20 | 21 | True 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/IEntityFrameworkUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using DbContextScope.UnitOfWork.Core.Interfaces; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | 5 | namespace DbContextScope.UnitOfWork.EfCore 6 | { 7 | public interface IEntityFrameworkUnitOfWork : IUnitOfWork where TContext : class 8 | { 9 | IDbContextTransaction BeginTransaction(); 10 | 11 | IDbContextTransaction BeginTransaction(IsolationLevel isolationLevel); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DbContextScope.UnitOfWork.EfCore")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DbContextScope.UnitOfWork.EfCore")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("e053ca11-b054-46d7-af4e-84aeff0aa1e3")] 24 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/Repository/EntityFrameworkRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using DbContextScope.EfCore.Interfaces; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DbContextScope.UnitOfWork.EfCore.Repository 10 | { 11 | /// 12 | /// Implements the IEntityFrameworkRepository interface to represent an Entity Framework repository. 13 | /// 14 | /// 15 | /// 16 | public class EntityFrameworkRepository : IEntityFrameworkRepository 17 | where TEntity : class 18 | where TContext : DbContext 19 | { 20 | #region Attributes 21 | 22 | readonly IAmbientDbContextLocator dbContextLocator; 23 | 24 | #endregion 25 | 26 | #region IEntityFrameworkRepository Implementation 27 | 28 | /// 29 | /// Constructor that receives a DbContextLocator instance. 30 | /// 31 | /// DbContextLocator instance. 32 | public EntityFrameworkRepository(IAmbientDbContextLocator dbContextLocator) 33 | { 34 | if (dbContextLocator == null) throw new ArgumentNullException(nameof(dbContextLocator)); 35 | 36 | this.dbContextLocator = dbContextLocator; 37 | } 38 | 39 | public virtual void Add(TEntity entity) 40 | { 41 | GetDbSet().Add(entity); 42 | } 43 | 44 | public IQueryable AsQueryable() 45 | { 46 | return GetDbSet().AsQueryable(); 47 | } 48 | 49 | public virtual void Attach(TEntity entity) 50 | { 51 | GetDbSet().Attach(entity); 52 | } 53 | 54 | public virtual void Delete(TEntity entity) 55 | { 56 | if (GetDbContext().Entry(entity).State == EntityState.Detached) 57 | { 58 | GetDbSet().Attach(entity); 59 | } 60 | GetDbSet().Remove(entity); 61 | } 62 | 63 | public virtual void Edit(TEntity entity) 64 | { 65 | GetDbContext().Entry(entity).State = EntityState.Modified; 66 | } 67 | 68 | public virtual TEntity First(Expression> predicate = null) 69 | { 70 | return GetDbSet().First(predicate); 71 | } 72 | 73 | public virtual TEntity FirstOrDefault(Expression> predicate = null) 74 | { 75 | return GetDbSet().FirstOrDefault(predicate); 76 | } 77 | 78 | public virtual async Task FirstOrDefaultAsync(Expression> predicate = null) 79 | { 80 | return await GetDbSet().FirstOrDefaultAsync(predicate); 81 | } 82 | 83 | public virtual IEnumerable Get(Expression> predicate = null, 84 | Func, IOrderedQueryable> orderBy = null, 85 | params Expression>[] includeProperties) 86 | { 87 | return PrepareGetQuery(predicate, orderBy, includeProperties).ToList(); 88 | } 89 | 90 | public virtual async Task> GetAsync(Expression> predicate = null, 91 | Func, IOrderedQueryable> orderBy = null, 92 | params Expression>[] includeProperties) 93 | { 94 | return await PrepareGetQuery(predicate, orderBy, includeProperties).ToListAsync(); 95 | } 96 | 97 | public virtual TEntity LastOrDefault(Expression> predicate = null) 98 | { 99 | return GetDbSet().LastOrDefault(predicate); 100 | } 101 | 102 | public virtual TEntity Single(Expression> predicate) 103 | { 104 | return GetDbSet().Single(predicate); 105 | } 106 | 107 | public virtual async Task SingleAsync(Expression> predicate) 108 | { 109 | return await GetDbSet().SingleAsync(predicate); 110 | } 111 | 112 | public virtual TEntity SingleOrDefault(Expression> predicate) 113 | { 114 | return GetDbSet().SingleOrDefault(predicate); 115 | } 116 | 117 | public virtual async Task SingleOrDefaultAsync(Expression> predicate) 118 | { 119 | return await GetDbSet().SingleOrDefaultAsync(predicate); 120 | } 121 | 122 | public virtual void Update(TEntity entityToUpdate) 123 | { 124 | GetDbSet().Attach(entityToUpdate); 125 | GetDbContext().Entry(entityToUpdate).State = EntityState.Modified; 126 | } 127 | 128 | #endregion 129 | 130 | #region Private Methods 131 | 132 | DbContext GetDbContext() 133 | { 134 | return dbContextLocator.Get(); 135 | } 136 | 137 | DbSet GetDbSet() 138 | { 139 | return dbContextLocator.Get().Set(); 140 | } 141 | 142 | /// 143 | /// 144 | /// 145 | /// 146 | /// 147 | /// 148 | /// 149 | IQueryable PrepareGetQuery(Expression> predicate = null, 150 | Func, IOrderedQueryable> orderBy = null, 151 | params Expression>[] includeProperties) 152 | { 153 | var query = AsQueryable(); 154 | 155 | if (predicate != null) 156 | { 157 | query = query.Where(predicate); 158 | } 159 | 160 | foreach (var includeProperty in includeProperties) 161 | { 162 | query = query.Include(includeProperty); 163 | } 164 | 165 | if (orderBy != null) 166 | { 167 | return orderBy(query); 168 | } 169 | 170 | return query; 171 | } 172 | 173 | #endregion 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/Repository/IEntityFrameworkRepository.cs: -------------------------------------------------------------------------------- 1 | using DbContextScope.UnitOfWork.Core.Repository; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace DbContextScope.UnitOfWork.EfCore.Repository 5 | { 6 | /// 7 | /// Defines a generic Entity Framework repository. 8 | /// 9 | /// Type of the entities that the repository will manage. 10 | /// Type of the Entity Framework context. 11 | public interface IEntityFrameworkRepository : IRepository 12 | where TEntity : class 13 | where TContext : DbContext 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DbContextScope.UnitOfWork.EfCore/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "DbContextScope.UnitOfWork.EfCore", 3 | "version": "1.2.0", 4 | "authors": [ "Camilo Fierro" ], 5 | "description": "This package provides a way to implement Unit Of Work and Repository patterns with Entity Framework Core and DbContextScope.", 6 | 7 | "dependencies": { 8 | "DbContextScope.EfCore": "1.2.0", 9 | "DbContextScope.UnitOfWork.Core": "1.2.0", 10 | "Microsoft.EntityFrameworkCore.Relational": "1.0.0" 11 | }, 12 | 13 | "frameworks": { 14 | "net452": { } 15 | }, 16 | 17 | "packOptions": { 18 | "tags": [ "Unit of Work, Repository, Pattern, Data, EntityFramework, ASP.NET, DbContext, DbContextScope" ], 19 | "projectUrl": "https://github.com/ninety7/DbContextScope", 20 | "licenseUrl": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DemoApplication/BusinessLogicServices/UserCreationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DbContextScope.EfCore.Interfaces; 3 | using Numero3.EntityFramework.Demo.CommandModel; 4 | using Numero3.EntityFramework.Demo.DomainModel; 5 | using Numero3.EntityFramework.Demo.Repositories; 6 | 7 | namespace Numero3.EntityFramework.Demo.BusinessLogicServices 8 | { 9 | /* 10 | * Example business logic service implementing command functionalities (i.e. create / update actions). 11 | */ 12 | public class UserCreationService 13 | { 14 | readonly IDbContextScopeFactory _dbContextScopeFactory; 15 | readonly IUserRepository _userRepository; 16 | 17 | public UserCreationService(IDbContextScopeFactory dbContextScopeFactory, IUserRepository userRepository) 18 | { 19 | if (dbContextScopeFactory == null) throw new ArgumentNullException(nameof(dbContextScopeFactory)); 20 | if (userRepository == null) throw new ArgumentNullException(nameof(userRepository)); 21 | _dbContextScopeFactory = dbContextScopeFactory; 22 | _userRepository = userRepository; 23 | } 24 | 25 | public void CreateUser(UserCreationSpec userToCreate) 26 | { 27 | if (userToCreate == null) 28 | throw new ArgumentNullException(nameof(userToCreate)); 29 | 30 | userToCreate.Validate(); 31 | 32 | /* 33 | * Typical usage of DbContextScope for a read-write business transaction. 34 | * It's as simple as it looks. 35 | */ 36 | using (var dbContextScope = _dbContextScopeFactory.Create()) 37 | { 38 | //-- Build domain model 39 | var user = new User 40 | { 41 | Id = userToCreate.Id, 42 | Name = userToCreate.Name, 43 | Email = userToCreate.Email, 44 | WelcomeEmailSent = false, 45 | CreatedOn = DateTime.UtcNow 46 | }; 47 | 48 | //-- Persist 49 | _userRepository.Add(user); 50 | dbContextScope.SaveChanges(); 51 | } 52 | } 53 | 54 | public void CreateListOfUsers(params UserCreationSpec[] usersToCreate) 55 | { 56 | /* 57 | * Example of DbContextScope nesting in action. 58 | * 59 | * We already have a service method - CreateUser() - that knows how to create a new user 60 | * and implements all the business rules around the creation of a new user 61 | * (e.g. validation, initialization, sending notifications to other domain model objects...). 62 | * 63 | * So we'll just call it in a loop to create the list of new users we've 64 | * been asked to create. 65 | * 66 | * Of course, since this is a business logic service method, we are making 67 | * an implicit guarantee to whoever is calling us that the changes we make to 68 | * the system will be either committed or rolled-back in an atomic manner. 69 | * I.e. either all the users we've been asked to create will get persisted 70 | * or none of them will. It would be disastrous to have a partial failure here 71 | * and end up with some users but not all having been created. 72 | * 73 | * DbContextScope makes this trivial to implement. 74 | * 75 | * The inner DbContextScope instance that the CreateUser() method creates 76 | * will join our top-level scope. This ensures that the same DbContext instance is 77 | * going to be used throughout this business transaction. 78 | * 79 | */ 80 | 81 | using (var dbContextScope = _dbContextScopeFactory.Create()) 82 | { 83 | foreach (var toCreate in usersToCreate) 84 | { 85 | CreateUser(toCreate); 86 | } 87 | 88 | // All the changes will get persisted here 89 | dbContextScope.SaveChanges(); 90 | } 91 | } 92 | 93 | public void CreateListOfUsersWithIntentionalFailure(params UserCreationSpec[] usersToCreate) 94 | { 95 | /* 96 | * Here, we'll verify that inner DbContextScopes really join the parent scope and 97 | * don't persist their changes until the parent scope completes successfully. 98 | */ 99 | 100 | var firstUser = true; 101 | 102 | using (var dbContextScope = _dbContextScopeFactory.Create()) 103 | { 104 | foreach (var toCreate in usersToCreate) 105 | { 106 | if (firstUser) 107 | { 108 | CreateUser(toCreate); 109 | Console.WriteLine("Successfully created a new User named '{0}'.", toCreate.Name); 110 | firstUser = false; 111 | } 112 | else 113 | { 114 | // OK. So we've successfully persisted one user. 115 | // We're going to simulate a failure when attempting to 116 | // persist the second user and see what ends up getting 117 | // persisted in the DB. 118 | throw new Exception(String.Format("Oh no! An error occurred when attempting to create user named '{0}' in our database.", toCreate.Name)); 119 | } 120 | } 121 | 122 | dbContextScope.SaveChanges(); 123 | } 124 | } 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/DemoApplication/BusinessLogicServices/UserCreditScoreService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using DbContextScope.EfCore.Interfaces; 6 | using Numero3.EntityFramework.Demo.DatabaseContext; 7 | 8 | namespace Numero3.EntityFramework.Demo.BusinessLogicServices 9 | { 10 | public class UserCreditScoreService 11 | { 12 | readonly IDbContextScopeFactory _dbContextScopeFactory; 13 | 14 | public UserCreditScoreService(IDbContextScopeFactory dbContextScopeFactory) 15 | { 16 | if (dbContextScopeFactory == null) throw new ArgumentNullException(nameof(dbContextScopeFactory)); 17 | _dbContextScopeFactory = dbContextScopeFactory; 18 | } 19 | 20 | public void UpdateCreditScoreForAllUsers() 21 | { 22 | /* 23 | * Demo of DbContextScope + parallel programming. 24 | */ 25 | 26 | using (var dbContextScope = _dbContextScopeFactory.Create()) 27 | { 28 | //-- Get all users 29 | var dbContext = dbContextScope.DbContexts.Get(); 30 | var userIds = dbContext.Users.Select(u => u.Id).ToList(); 31 | 32 | Console.WriteLine("Found {0} users in the database. Will calculate and store their credit scores in parallel.", userIds.Count); 33 | 34 | //-- Calculate and store the credit score of each user 35 | // We're going to imagine that calculating a credit score of a user takes some time. 36 | // So we'll do it in parallel. 37 | 38 | // You MUST call SuppressAmbientContext() when kicking off a parallel execution flow 39 | // within a DbContextScope. Otherwise, this DbContextScope will remain the ambient scope 40 | // in the parallel flows of execution, potentially leading to multiple threads 41 | // accessing the same DbContext instance. 42 | using (_dbContextScopeFactory.SuppressAmbientContext()) 43 | { 44 | Parallel.ForEach(userIds, UpdateCreditScore); 45 | } 46 | 47 | // Note: SaveChanges() isn't going to do anything in this instance since all the changes 48 | // were actually made and saved in separate DbContextScopes created in separate threads. 49 | dbContextScope.SaveChanges(); 50 | } 51 | } 52 | 53 | public void UpdateCreditScore(Guid userId) 54 | { 55 | using (var dbContextScope = _dbContextScopeFactory.Create()) 56 | { 57 | var dbContext = dbContextScope.DbContexts.Get(); 58 | var user = dbContext.Users.FirstOrDefault(u => u.Id == userId); 59 | if (user == null) 60 | throw new ArgumentException(string.Format("Invalid userId provided: {0}. Couldn't find a User with this ID.", userId)); 61 | 62 | // Simulate the calculation of a credit score taking some time 63 | var random = new Random(Thread.CurrentThread.ManagedThreadId); 64 | Thread.Sleep(random.Next(300, 1000)); 65 | 66 | user.CreditScore = random.Next(1, 100); 67 | dbContextScope.SaveChanges(); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/DemoApplication/BusinessLogicServices/UserEmailService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using DbContextScope.EfCore.Enums; 5 | using DbContextScope.EfCore.Interfaces; 6 | using Numero3.EntityFramework.Demo.DatabaseContext; 7 | using Numero3.EntityFramework.Demo.DomainModel; 8 | 9 | namespace Numero3.EntityFramework.Demo.BusinessLogicServices 10 | { 11 | public class UserEmailService 12 | { 13 | readonly IDbContextScopeFactory _dbContextScopeFactory; 14 | 15 | public UserEmailService(IDbContextScopeFactory dbContextScopeFactory) 16 | { 17 | if (dbContextScopeFactory == null) throw new ArgumentNullException(nameof(dbContextScopeFactory)); 18 | _dbContextScopeFactory = dbContextScopeFactory; 19 | } 20 | 21 | public void SendWelcomeEmail(Guid userId) 22 | { 23 | /* 24 | * Demo of forcing the creation of a new DbContextScope 25 | * to ensure that changes made to the model in this service 26 | * method are persisted even if that method happens to get 27 | * called within the scope of a wider business transaction 28 | * that eventually fails for any reason. 29 | * 30 | * This is an advanced feature that should be used as rarely 31 | * as possible (and ideally, never). 32 | */ 33 | 34 | // We're going to send a welcome email to the provided user 35 | // (if one hasn't been sent already). Once sent, we'll update 36 | // that User entity in our DB to record that its Welcome email 37 | // has been sent. 38 | 39 | // Emails can't be rolled-back. Once they're sent, they're sent. 40 | // So once the email has been sent successfully, we absolutely 41 | // must persist this fact in our DB. Even if that method is called 42 | // by another busines logic service method as part of a wider 43 | // business transaction and even if that parent business transaction 44 | // ends up failing for any reason, we still must ensure that 45 | // we have recorded the fact that the Welcome email has been sent. 46 | // Otherwise, we would risk spamming our users with repeated Welcome 47 | // emails. 48 | 49 | // Force the creation of a new DbContextScope so that the changes we make here are 50 | // guaranteed to get persisted regardless of what happens after this method has completed. 51 | using (var dbContextScope = _dbContextScopeFactory.Create(DbContextScopeOption.ForceCreateNew)) 52 | { 53 | var dbContext = dbContextScope.DbContexts.Get(); 54 | var user = dbContext.Users.FirstOrDefault(u => u.Id == userId); 55 | 56 | if (user == null) 57 | throw new ArgumentException(string.Format("Invalid userId provided: {0}. Couldn't find a User with this ID.", userId)); 58 | 59 | if (!user.WelcomeEmailSent) 60 | { 61 | SendEmail(user.Email); 62 | user.WelcomeEmailSent = true; 63 | } 64 | 65 | dbContextScope.SaveChanges(); 66 | 67 | // When you force the creation of a new DbContextScope, you must force the parent 68 | // scope (if any) to reload the entities you've modified here. Otherwise, the method calling 69 | // you might not be able to see the changes you made here. 70 | dbContextScope.RefreshEntitiesInParentScope(new List {user}); 71 | } 72 | } 73 | 74 | void SendEmail(string emailAddress) 75 | { 76 | // Send the email synchronously. Throw if any error occurs. 77 | // [...] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/DemoApplication/BusinessLogicServices/UserQueryService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using DbContextScope.EfCore.Interfaces; 7 | using Numero3.EntityFramework.Demo.DatabaseContext; 8 | using Numero3.EntityFramework.Demo.DomainModel; 9 | using Numero3.EntityFramework.Demo.Repositories; 10 | 11 | namespace Numero3.EntityFramework.Demo.BusinessLogicServices 12 | { 13 | /* 14 | * Example business logic service implementing query functionalities (i.e. read actions). 15 | */ 16 | public class UserQueryService 17 | { 18 | readonly IDbContextScopeFactory _dbContextScopeFactory; 19 | readonly IUserRepository _userRepository; 20 | 21 | public UserQueryService(IDbContextScopeFactory dbContextScopeFactory, IUserRepository userRepository) 22 | { 23 | if (dbContextScopeFactory == null) throw new ArgumentNullException(nameof(dbContextScopeFactory)); 24 | if (userRepository == null) throw new ArgumentNullException(nameof(userRepository)); 25 | _dbContextScopeFactory = dbContextScopeFactory; 26 | _userRepository = userRepository; 27 | } 28 | 29 | public User GetUser(Guid userId) 30 | { 31 | /* 32 | * An example of using DbContextScope for read-only queries. 33 | * Here, we access the Entity Framework DbContext directly from 34 | * the business logic service class. 35 | * 36 | * Calling SaveChanges() is not necessary here (and in fact not 37 | * possible) since we created a read-only scope. 38 | */ 39 | using (var dbContextScope = _dbContextScopeFactory.CreateReadOnly()) 40 | { 41 | var dbContext = dbContextScope.DbContexts.Get(); 42 | var user = dbContext.Users.FirstOrDefault(u => u.Id == userId); 43 | 44 | if (user == null) 45 | throw new ArgumentException(string.Format("Invalid value provided for userId: [{0}]. Couldn't find a user with this ID.", userId)); 46 | 47 | return user; 48 | } 49 | } 50 | 51 | public IEnumerable GetUsers(params Guid[] userIds) 52 | { 53 | using (var dbContextScope = _dbContextScopeFactory.CreateReadOnly()) 54 | { 55 | var dbContext = dbContextScope.DbContexts.Get(); 56 | return dbContext.Users.Where(u => userIds.Contains(u.Id)).ToList(); 57 | } 58 | } 59 | 60 | public User GetUserViaRepository(Guid userId) 61 | { 62 | /* 63 | * Same as GetUsers() but using a repository layer instead of accessing the 64 | * EF DbContext directly. 65 | * 66 | * Note how we don't have to worry about knowing what type of DbContext the 67 | * repository will need, about creating the DbContext instance or about passing 68 | * DbContext instances around. 69 | * 70 | * The DbContextScope will take care of creating the necessary DbContext instances 71 | * and making them available as ambient contexts for our repository layer to use. 72 | * It will also guarantee that only one instance of any given DbContext type exists 73 | * within its scope ensuring that all persistent entities managed within that scope 74 | * are attached to the same DbContext. 75 | */ 76 | using (_dbContextScopeFactory.CreateReadOnly()) 77 | { 78 | var user = _userRepository.Get(userId); 79 | 80 | if (user == null) 81 | throw new ArgumentException(String.Format("Invalid value provided for userId: [{0}]. Couldn't find a user with this ID.", userId)); 82 | 83 | return user; 84 | } 85 | } 86 | 87 | public async Task> GetTwoUsersAsync(Guid userId1, Guid userId2) 88 | { 89 | /* 90 | * A very contrived example of ambient DbContextScope within an async flow. 91 | * 92 | * Note that the ConfigureAwait(false) calls here aren't strictly necessary 93 | * and are unrelated to DbContextScope. You can remove them if you want and 94 | * the code will run in the same way. It is however good practice to configure 95 | * all your awaitables in library code to not continue 96 | * on the captured synchronization context. It avoids having to pay the overhead 97 | * of capturing the sync context and running the task continuation on it when 98 | * library code doesn't need that context. If also helps prevent potential deadlocks 99 | * if the upstream code has been poorly written and blocks on async tasks. 100 | * 101 | * "Library code" is any code in layers under the presentation tier. Typically any code 102 | * other that code in ASP.NET MVC / WebApi controllers or Window Form / WPF forms. 103 | * 104 | * See http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx for 105 | * more details. 106 | */ 107 | 108 | using (_dbContextScopeFactory.CreateReadOnly()) 109 | { 110 | var user1 = await _userRepository.GetAsync(userId1).ConfigureAwait(false); 111 | 112 | // We're now in the continuation of the first async task. This is most 113 | // likely executing in a thread from the ThreadPool, i.e. in a different 114 | // thread that the one where we created our DbContextScope. Our ambient 115 | // DbContextScope is still available here however, which allows the call 116 | // below to succeed. 117 | 118 | var user2 = await _userRepository.GetAsync(userId2).ConfigureAwait(false); 119 | 120 | // In other words, DbContextScope works with async execution flow as you'd expect: 121 | // It Just Works. 122 | 123 | return new List {user1, user2}.Where(u => u != null).ToList(); 124 | } 125 | } 126 | 127 | public User GetUserUncommitted(Guid userId) 128 | { 129 | /* 130 | * An example of explicit database transaction. 131 | * 132 | * Read the comment for CreateReadOnlyWithTransaction() before using this overload 133 | * as there are gotchas when doing this! 134 | */ 135 | using (_dbContextScopeFactory.CreateReadOnlyWithTransaction(IsolationLevel.ReadUncommitted)) 136 | { 137 | return _userRepository.Get(userId); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/DemoApplication/CommandModel/UserCreationSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Numero3.EntityFramework.Demo.CommandModel 4 | { 5 | /// 6 | /// Specifications of the CreateUser command. Defines the properties of a new user. 7 | /// 8 | public class UserCreationSpec 9 | { 10 | /// 11 | /// The Id automatically generated for this user. 12 | /// 13 | public Guid Id { get; protected set; } 14 | 15 | public string Name { get; protected set; } 16 | public string Email { get; protected set; } 17 | 18 | public UserCreationSpec(string name, string email) 19 | { 20 | Id = Guid.NewGuid(); 21 | Name = name; 22 | Email = email; 23 | } 24 | 25 | public void Validate() 26 | { 27 | // [...] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DemoApplication/DatabaseContext/UserManagementDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Numero3.EntityFramework.Demo.DomainModel; 3 | 4 | namespace Numero3.EntityFramework.Demo.DatabaseContext 5 | { 6 | public class UserManagementDbContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | 10 | readonly string connectionString; 11 | 12 | public UserManagementDbContext() 13 | { 14 | connectionString = "Server=(localdb)\\mssqllocaldb;Database=DbContextScopeDemo;Trusted_Connection=True;MultipleActiveResultSets=true"; 15 | } 16 | 17 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 18 | { 19 | optionsBuilder.UseSqlServer(connectionString); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DemoApplication/Demo Application.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | c7710de2-92ae-49c8-8645-db1300a2cd9c 10 | Numero3.EntityFramework.Demo 11 | ..\..\artifacts\obj\$(MSBuildProjectName) 12 | .\bin\ 13 | 14 | 15 | 2.0 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/DemoApplication/DomainModel/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace Numero3.EntityFramework.Demo.DomainModel 5 | { 6 | // Anemic model to keep this demo application simple. 7 | public class User 8 | { 9 | public Guid Id { get; set; } 10 | [Required] 11 | public string Name { get; set; } 12 | [Required] 13 | public string Email { get; set; } 14 | public int CreditScore { get; set; } 15 | public bool WelcomeEmailSent { get; set; } 16 | public DateTime CreatedOn { get; set; } 17 | 18 | public override string ToString() 19 | { 20 | return string.Format("Id: {0} | Name: {1} | Email: {2} | CreditScore: {3} | WelcomeEmailSent: {4} | CreatedOn (UTC): {5}", Id, Name, Email, CreditScore, WelcomeEmailSent, CreatedOn.ToString("dd MMM yyyy - HH:mm:ss")); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DemoApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DbContextScope.EfCore.Implementations; 4 | using Numero3.EntityFramework.Demo.BusinessLogicServices; 5 | using Numero3.EntityFramework.Demo.CommandModel; 6 | using Numero3.EntityFramework.Demo.DatabaseContext; 7 | using Numero3.EntityFramework.Demo.Repositories; 8 | 9 | namespace Numero3.EntityFramework.Demo 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | //-- Poor-man DI - build our dependencies by hand for this demo 16 | var dbContextScopeFactory = new DbContextScopeFactory(); 17 | var ambientDbContextLocator = new AmbientDbContextLocator(); 18 | var userRepository = new UserRepository(ambientDbContextLocator); 19 | 20 | var userCreationService = new UserCreationService(dbContextScopeFactory, userRepository); 21 | var userQueryService = new UserQueryService(dbContextScopeFactory, userRepository); 22 | var userEmailService = new UserEmailService(dbContextScopeFactory); 23 | var userCreditScoreService = new UserCreditScoreService(dbContextScopeFactory); 24 | 25 | try 26 | { 27 | Console.WriteLine("This demo application will create a database named DbContextScopeDemo in the default SQL Server instance on localhost. Edit the connection string in UserManagementDbContext if you'd like to create it somewhere else."); 28 | Console.WriteLine("Press enter to start..."); 29 | Console.ReadLine(); 30 | 31 | //-- Demo of typical usage for read and writes 32 | Console.WriteLine("Creating a user called Mary..."); 33 | var marysSpec = new UserCreationSpec("Mary", "mary@example.com"); 34 | userCreationService.CreateUser(marysSpec); 35 | Console.WriteLine("Done.\n"); 36 | 37 | Console.WriteLine("Trying to retrieve our newly created user from the data store..."); 38 | var mary = userQueryService.GetUser(marysSpec.Id); 39 | Console.WriteLine("OK. Persisted user: {0}", mary); 40 | 41 | Console.WriteLine("Press enter to continue..."); 42 | Console.ReadLine(); 43 | 44 | //-- Demo of nested DbContextScopes 45 | Console.WriteLine("Creating 2 new users called John and Jeanne in an atomic transaction..."); 46 | var johnSpec = new UserCreationSpec("John", "john@example.com"); 47 | var jeanneSpec = new UserCreationSpec("Jeanne", "jeanne@example.com"); 48 | userCreationService.CreateListOfUsers(johnSpec, jeanneSpec); 49 | Console.WriteLine("Done.\n"); 50 | 51 | Console.WriteLine("Trying to retrieve our newly created users from the data store..."); 52 | var createdUsers = userQueryService.GetUsers(johnSpec.Id, jeanneSpec.Id); 53 | Console.WriteLine("OK. Found {0} persisted users.", createdUsers.Count()); 54 | 55 | Console.WriteLine("Press enter to continue..."); 56 | Console.ReadLine(); 57 | 58 | //-- Demo of nested DbContextScopes in the face of an exception. 59 | // If any of the provided users failed to get persisted, none should get persisted. 60 | Console.WriteLine("Creating 2 new users called Julie and Marc in an atomic transaction. Will make the persistence of the second user fail intentionally in order to test the atomicity of the transaction..."); 61 | var julieSpec = new UserCreationSpec("Julie", "julie@example.com"); 62 | var marcSpec = new UserCreationSpec("Marc", "marc@example.com"); 63 | try 64 | { 65 | userCreationService.CreateListOfUsersWithIntentionalFailure(julieSpec, marcSpec); 66 | Console.WriteLine("Done.\n"); 67 | } 68 | catch (Exception e) 69 | { 70 | Console.WriteLine(e.Message); 71 | Console.WriteLine(); 72 | } 73 | 74 | Console.WriteLine("Trying to retrieve our newly created users from the data store..."); 75 | var maybeCreatedUsers = userQueryService.GetUsers(julieSpec.Id, marcSpec.Id); 76 | Console.WriteLine("Found {0} persisted users. If this number is 0, we're all good. If this number is not 0, we have a big problem.", maybeCreatedUsers.Count()); 77 | 78 | Console.WriteLine("Press enter to continue..."); 79 | Console.ReadLine(); 80 | 81 | //-- Demo of DbContextScope within an async flow 82 | Console.WriteLine("Trying to retrieve two users John and Jeanne sequentially in an asynchronous manner..."); 83 | // We're going to block on the async task here as we don't have a choice. No risk of deadlocking in any case as console apps 84 | // don't have a synchronization context. 85 | var usersFoundAsync = userQueryService.GetTwoUsersAsync(johnSpec.Id, jeanneSpec.Id).Result; 86 | Console.WriteLine("OK. Found {0} persisted users.", usersFoundAsync.Count()); 87 | 88 | Console.WriteLine("Press enter to continue..."); 89 | Console.ReadLine(); 90 | 91 | //-- Demo of explicit database transaction. 92 | Console.WriteLine("Trying to retrieve user John within a READ UNCOMMITTED database transaction..."); 93 | // You'll want to use SQL Profiler or Entity Framework Profiler to verify that the correct transaction isolation 94 | // level is being used. 95 | var userMaybeUncommitted = userQueryService.GetUserUncommitted(johnSpec.Id); 96 | Console.WriteLine("OK. User found: {0}", userMaybeUncommitted); 97 | 98 | Console.WriteLine("Press enter to continue..."); 99 | Console.ReadLine(); 100 | 101 | //-- Demo of disabling the DbContextScope nesting behaviour in order to force the persistence of changes made to entities 102 | // This is a pretty advanced feature that you can safely ignore until you actually need it. 103 | Console.WriteLine("Will simulate sending a Welcome email to John..."); 104 | 105 | using (var parentScope = dbContextScopeFactory.Create()) 106 | { 107 | var parentDbContext = parentScope.DbContexts.Get(); 108 | 109 | // Load John in the parent DbContext 110 | var john = parentDbContext.Users.FirstOrDefault(u => u.Id == johnSpec.Id); 111 | Console.WriteLine("Before calling SendWelcomeEmail(), john.WelcomeEmailSent = " + john.WelcomeEmailSent); 112 | 113 | // Now call our SendWelcomeEmail() business logic service method, which will 114 | // update John in a non-nested child context 115 | userEmailService.SendWelcomeEmail(johnSpec.Id); 116 | 117 | // Verify that we can see the modifications made to John by the SendWelcomeEmail() method 118 | Console.WriteLine("After calling SendWelcomeEmail(), john.WelcomeEmailSent = " + john.WelcomeEmailSent); 119 | 120 | // Note that even though we're not calling SaveChanges() in the parent scope here, the changes 121 | // made to John by SendWelcomeEmail() will remain persisted in the database as SendWelcomeEmail() 122 | // forced the creation of a new DbContextScope. 123 | } 124 | 125 | Console.WriteLine("Press enter to continue..."); 126 | Console.ReadLine(); 127 | 128 | //-- Demonstration of DbContextScope and parallel programming 129 | Console.WriteLine("Calculating and storing the credit score of all users in the database in parallel..."); 130 | userCreditScoreService.UpdateCreditScoreForAllUsers(); 131 | Console.WriteLine("Done."); 132 | } 133 | catch (Exception e) 134 | { 135 | Console.WriteLine(e); 136 | } 137 | 138 | Console.WriteLine(); 139 | Console.WriteLine("The end."); 140 | Console.WriteLine("Press enter to exit..."); 141 | Console.ReadLine(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/DemoApplication/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DbContextScope")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DbContextScope")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("268b6a6d-90fa-4bca-ab00-0354003eb9b9")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/DemoApplication/Repositories/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Numero3.EntityFramework.Demo.DomainModel; 4 | 5 | namespace Numero3.EntityFramework.Demo.Repositories 6 | { 7 | public interface IUserRepository 8 | { 9 | User Get(Guid userId); 10 | Task GetAsync(Guid userId); 11 | void Add(User user); 12 | } 13 | } -------------------------------------------------------------------------------- /src/DemoApplication/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DbContextScope.EfCore.Interfaces; 5 | using Microsoft.EntityFrameworkCore; 6 | using Numero3.EntityFramework.Demo.DatabaseContext; 7 | using Numero3.EntityFramework.Demo.DomainModel; 8 | 9 | namespace Numero3.EntityFramework.Demo.Repositories 10 | { 11 | /* 12 | * An example "repository" relying on an ambient DbContext instance. 13 | * 14 | * Since we use EF to persist our data, the actual repository is of course the EF DbContext. This 15 | * class is called a "repository" for old time's sake but is merely just a collection 16 | * of pre-built Linq-to-Entities queries. This avoids having these queries copied and 17 | * pasted in every service method that need them and facilitates unit testing. 18 | * 19 | * Whether your application would benefit from using this additional layer or would 20 | * be better off if its service methods queried the DbContext directly or used some sort of query 21 | * object pattern is a design decision for you to make. 22 | * 23 | * DbContextScope is agnostic to this and will happily let you use any approach you 24 | * deem most suitable for your application. 25 | * 26 | */ 27 | public class UserRepository : IUserRepository 28 | { 29 | readonly IAmbientDbContextLocator _ambientDbContextLocator; 30 | 31 | UserManagementDbContext DbContext 32 | { 33 | get 34 | { 35 | var dbContext = _ambientDbContextLocator.Get(); 36 | 37 | if (dbContext == null) 38 | throw new InvalidOperationException("No ambient DbContext of type UserManagementDbContext found. This means that this repository method has been called outside of the scope of a DbContextScope. A repository must only be accessed within the scope of a DbContextScope, which takes care of creating the DbContext instances that the repositories need and making them available as ambient contexts. This is what ensures that, for any given DbContext-derived type, the same instance is used throughout the duration of a business transaction. To fix this issue, use IDbContextScopeFactory in your top-level business logic service method to create a DbContextScope that wraps the entire business transaction that your service method implements. Then access this repository within that scope. Refer to the comments in the IDbContextScope.cs file for more details."); 39 | 40 | dbContext.Database.EnsureCreated(); 41 | return dbContext; 42 | } 43 | } 44 | 45 | public UserRepository(IAmbientDbContextLocator ambientDbContextLocator) 46 | { 47 | if (ambientDbContextLocator == null) throw new ArgumentNullException(nameof(ambientDbContextLocator)); 48 | _ambientDbContextLocator = ambientDbContextLocator; 49 | } 50 | 51 | public User Get(Guid userId) 52 | { 53 | return DbContext.Users.FirstOrDefault(u => u.Id == userId); 54 | } 55 | 56 | public Task GetAsync(Guid userId) 57 | { 58 | return DbContext.Users.FirstOrDefaultAsync(u => u.Id == userId); 59 | } 60 | 61 | public void Add(User user) 62 | { 63 | DbContext.Users.Add(user); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/DemoApplication/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "description": "Demo App for DbContextScope", 4 | "authors": [ "Mehdi El Gueddari" ], 5 | 6 | "buildOptions": { 7 | "emitEntryPoint": true 8 | }, 9 | 10 | "dependencies": { 11 | "DbContextScope.EfCore": "1.2.0", 12 | "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0" 13 | }, 14 | 15 | "commands": { 16 | "DbContextScope.Demo": "DbContextScope.Demo" 17 | }, 18 | 19 | "frameworks": { 20 | "net461": { } 21 | } 22 | } 23 | --------------------------------------------------------------------------------