diff --git a/.gitignore b/.gitignore index dd0049e..4aa3348 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,19 @@ + +# Created by https://www.gitignore.io/api/aspnetcore,visualstudio + +### ASPNETCore ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user +*.userosscache *.sln.docstates +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -13,19 +21,21 @@ [Rr]eleases/ x64/ x86/ -build/ bld/ [Bb]in/ [Oo]bj/ +[Ll]og/ -# Roslyn cache directories -*.ide/ +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -#NUNIT +# NUNIT *.VisualState.xml TestResult.xml @@ -34,6 +44,11 @@ TestResult.xml [Rr]eleasePS/ dlldata.c +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + *_i.c *_p.c *_i.h @@ -66,14 +81,18 @@ _Chutzpah* ipch/ *.aps *.ncb +*.opendb *.opensdf *.sdf *.cachefile +*.VC.db +*.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx +*.sap # TFS 2012 Local Workspace $tf/ @@ -86,7 +105,7 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding addin-in +# JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in @@ -95,9 +114,14 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# Visual Studio code coverage results +*.coverage +*.coveragexml + # NCrunch _NCrunch_* .*crunch*.local.xml +nCrunchTemp_* # MightyMoose *.mm.* @@ -125,39 +149,63 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ -# If using the old MSBuild-Integrated Package Restore, uncomment this: +# Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets -# Windows Azure Build Output +# Microsoft Azure Build Output csx/ *.build.csdef -# Windows Store app package directory +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ # Others -sql/ -*.Cache ClientBin/ -[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview +*.jfm *.pfx *.publishsettings node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ # RIA/Silverlight projects Generated_Code/ @@ -182,4 +230,194 @@ UpgradeLog*.htm # Microsoft Fakes FakesAssemblies/ -/.vs/DbContextScope/v15/sqlite3 +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Build results + +# Visual Studio 2015 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results + +# NUNIT + +# Build Results of an ATL Project + +# .NET Core +**/Properties/launchSettings.json + + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# JustCode is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# TODO: Uncomment the next line to ignore your web deploy settings. +# By default, sensitive information, such as encrypted password +# should be stored in the .pubxml.user file. +#*.pubxml +*.pubxml.user + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files +*.ndf + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# JetBrains Rider + +# CodeRush + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +### VisualStudio Patch ### +# By default, sensitive information, such as encrypted password +# should be stored in the .pubxml.user file. + + +# End of https://www.gitignore.io/api/aspnetcore,visualstudio diff --git a/.nuget/NuGet.Config b/.nuget/NuGet.Config deleted file mode 100644 index 67f8ea0..0000000 --- a/.nuget/NuGet.Config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.nuget/NuGet.exe b/.nuget/NuGet.exe deleted file mode 100644 index 8dd7e45..0000000 Binary files a/.nuget/NuGet.exe and /dev/null differ diff --git a/.nuget/NuGet.targets b/.nuget/NuGet.targets deleted file mode 100644 index 3f8c37b..0000000 --- a/.nuget/NuGet.targets +++ /dev/null @@ -1,144 +0,0 @@ - - - - $(MSBuildProjectDirectory)\..\ - - - false - - - false - - - true - - - false - - - - - - - - - - - $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) - - - - - $(SolutionDir).nuget - - - - $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config - $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config - - - - $(MSBuildProjectDirectory)\packages.config - $(PackagesProjectConfig) - - - - - $(NuGetToolsPath)\NuGet.exe - @(PackageSource) - - "$(NuGetExePath)" - mono --runtime=v4.0.30319 "$(NuGetExePath)" - - $(TargetDir.Trim('\\')) - - -RequireConsent - -NonInteractive - - "$(SolutionDir) " - "$(SolutionDir)" - - - $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) - $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols - - - - RestorePackages; - $(BuildDependsOn); - - - - - $(BuildDependsOn); - BuildPackage; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DbContextScope/DbContextScope.csproj b/DbContextScope/DbContextScope.csproj deleted file mode 100644 index e87ad91..0000000 --- a/DbContextScope/DbContextScope.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net461 - - EntityFrameworkCore.DbContextScope - - 1.0.0 - 1.0.0 - - - - Mehdi El Gueddari,Tim Calvert - EntityFrameworkCore.DbContextScope - https://github.com/tncalvert/DbContextScope - https://raw.githubusercontent.com/tncalvert/DbContextScope/master/LICENSE - 1.0.0 - DbContextScope for EF Core. Forked from https://github.com/mehdime/DbContextScope. - git - https://github.com/tncalvert/DbContextScope - EntityFramework EFCore DbContext DbContextScope - - - - - - - diff --git a/DbContextScope/DbContextScope.licenseheader b/DbContextScope/DbContextScope.licenseheader deleted file mode 100644 index 2a2a49b..0000000 --- a/DbContextScope/DbContextScope.licenseheader +++ /dev/null @@ -1,9 +0,0 @@ -extensions: designer.cs generated.cs -extensions: .cs .cpp .h -/* - * Copyright (C) 2014 Mehdi El Gueddari - * http://mehdi.me - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj deleted file mode 100644 index fb56285..0000000 --- a/Demo/Demo.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net461 - - - - - - - - - - - diff --git a/Demo/Program.cs b/Demo/Program.cs deleted file mode 100644 index 9512646..0000000 --- a/Demo/Program.cs +++ /dev/null @@ -1,150 +0,0 @@ -using EntityFrameworkCore.DbContextScope; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Numero3.EntityFramework.Demo.BusinessLogicServices; -using Numero3.EntityFramework.Demo.CommandModel; -using Numero3.EntityFramework.Demo.DatabaseContext; -using Numero3.EntityFramework.Demo.Repositories; -using System; -using System.Linq; - -namespace Numero3.EntityFramework.Demo { - class Program { - static void Main(string[] args) { - //-- Poor-man DI - build our dependencies by hand for this demo - var dbContextScopeFactory = new DbContextScopeFactory(new DbContextFactory()); - var ambientDbContextLocator = new AmbientDbContextLocator(); - var userRepository = new UserRepository(ambientDbContextLocator); - - var userCreationService = new UserCreationService(dbContextScopeFactory, userRepository); - var userQueryService = new UserQueryService(dbContextScopeFactory, userRepository); - var userEmailService = new UserEmailService(dbContextScopeFactory); - var userCreditScoreService = new UserCreditScoreService(dbContextScopeFactory); - - try { - Console.WriteLine("This demo uses an EF Core In Memory database. It does not create any external databases."); - Console.WriteLine(""); - - //-- Demo of typical usage for read and writes - Console.WriteLine("Creating a user called Mary..."); - var marysSpec = new UserCreationSpec("Mary", "mary@example.com"); - userCreationService.CreateUser(marysSpec); - Console.WriteLine("Done.\n"); - - Console.WriteLine("Trying to retrieve our newly created user from the data store..."); - var mary = userQueryService.GetUser(marysSpec.Id); - Console.WriteLine("OK. Persisted user: {0}", mary); - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demo of nested DbContextScopes - Console.WriteLine("Creating 2 new users called John and Jeanne in an atomic transaction..."); - var johnSpec = new UserCreationSpec("John", "john@example.com"); - var jeanneSpec = new UserCreationSpec("Jeanne", "jeanne@example.com"); - userCreationService.CreateListOfUsers(johnSpec, jeanneSpec); - Console.WriteLine("Done.\n"); - - Console.WriteLine("Trying to retrieve our newly created users from the data store..."); - var createdUsers = userQueryService.GetUsers(johnSpec.Id, jeanneSpec.Id); - Console.WriteLine("OK. Found {0} persisted users.", createdUsers.Count()); - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demo of nested DbContextScopes in the face of an exception. - // If any of the provided users failed to get persisted, none should get persisted. - 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..."); - var julieSpec = new UserCreationSpec("Julie", "julie@example.com"); - var marcSpec = new UserCreationSpec("Marc", "marc@example.com"); - try { - userCreationService.CreateListOfUsersWithIntentionalFailure(julieSpec, marcSpec); - Console.WriteLine("Done.\n"); - } catch (Exception e) { - Console.WriteLine(e.Message); - Console.WriteLine(); - } - - Console.WriteLine("Trying to retrieve our newly created users from the data store..."); - var maybeCreatedUsers = userQueryService.GetUsers(julieSpec.Id, marcSpec.Id); - 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()); - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demo of DbContextScope within an async flow - Console.WriteLine("Trying to retrieve two users John and Jeanne sequentially in an asynchronous manner..."); - // 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 - // don't have a synchronization context. - var usersFoundAsync = userQueryService.GetTwoUsersAsync(johnSpec.Id, jeanneSpec.Id).Result; - Console.WriteLine("OK. Found {0} persisted users.", usersFoundAsync.Count()); - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demo of explicit database transaction. - Console.WriteLine("Trying to retrieve user John within a READ UNCOMMITTED database transaction..."); - // You'll want to use SQL Profiler or Entity Framework Profiler to verify that the correct transaction isolation - // level is being used. - var userMaybeUncommitted = userQueryService.GetUserUncommitted(johnSpec.Id); - Console.WriteLine("OK. User found: {0}", userMaybeUncommitted); - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demo of disabling the DbContextScope nesting behaviour in order to force the persistence of changes made to entities - // This is a pretty advanced feature that you can safely ignore until you actually need it. - Console.WriteLine("Will simulate sending a Welcome email to John..."); - - using (var parentScope = dbContextScopeFactory.Create()) { - var parentDbContext = parentScope.DbContexts.Get(); - - // Load John in the parent DbContext - var john = parentDbContext.Users.Find(johnSpec.Id); - Console.WriteLine("Before calling SendWelcomeEmail(), john.WelcomeEmailSent = " + john.WelcomeEmailSent); - - // Now call our SendWelcomeEmail() business logic service method, which will - // update John in a non-nested child context - userEmailService.SendWelcomeEmail(johnSpec.Id); - - // Verify that we can see the modifications made to John by the SendWelcomeEmail() method - Console.WriteLine("After calling SendWelcomeEmail(), john.WelcomeEmailSent = " + john.WelcomeEmailSent); - - // Note that even though we're not calling SaveChanges() in the parent scope here, the changes - // made to John by SendWelcomeEmail() will remain persisted in the database as SendWelcomeEmail() - // forced the creation of a new DbContextScope. - } - - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - - //-- Demonstration of DbContextScope and parallel programming - Console.WriteLine("Calculating and storing the credit score of all users in the database in parallel..."); - userCreditScoreService.UpdateCreditScoreForAllUsers(); - Console.WriteLine("Done."); - } catch (Exception e) { - Console.WriteLine(e); - } - - Console.WriteLine(); - Console.WriteLine("The end."); - Console.WriteLine("Press enter to exit..."); - Console.ReadLine(); - } - } - - class DbContextFactory : IDbContextFactory { - public TDbContext CreateDbContext() where TDbContext : DbContext { - if (typeof(TDbContext) == typeof(UserManagementDbContext)) { - var config = new DbContextOptionsBuilder() - .UseInMemoryDatabase() - .ConfigureWarnings(warnings => { - warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning); - }); - return new UserManagementDbContext(config.Options) as TDbContext; - } - - throw new NotImplementedException(typeof(TDbContext).Name); - } - } -} diff --git a/Directory.build.props b/Directory.build.props new file mode 100644 index 0000000..8f5af2b --- /dev/null +++ b/Directory.build.props @@ -0,0 +1,35 @@ + + + latest + Dominic Jonas + Dominic Jonas + DoJo.EntityFrameworkCore.DbContextScope + https://github.com/dojo90/DbContextScope + https://raw.githubusercontent.com/dojo90/DbContextScope/master/LICENSE + git@github.com:dojo90/DbContextScope.git + git + sentinel nlog + DbContextScope for EF v2|v3 Core. Forked from https://github.com/tncalvert/DbContextScope. + EntityFramework EFCore DbContext DbContextScope + + + + + false + false + false + false + + $(GitVersion_FullSemVer) + $(GitVersion_MajorMinorPatch) + $(GitVersion_NuGetPreReleaseTag) + $(GitVersion_PreReleaseTag) + $(GitVersion_NuGetVersion) + $(GitVersion_FullSemVer) + $(GitVersion_InformationalVersion) + $(GitVersion_AssemblySemVer) + $(GitVersion_AssemblySemFileVer) + $(GitVersion_BranchName) + $(GitVersion_Sha) + + \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..bc6c5ae --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,19 @@ +mode: ContinuousDeployment +branches: + issue: + tag: issue-{BranchName} + increment: Inherit + prevent-increment-of-merged-branch-version: false + regex: ^(?=\d+-) + source-branches: + - develop + - master + - release + - feature + - support + - hotfix + - issue + develop: + increment: Patch +ignore: + sha: [] diff --git a/README.md b/README.md index 26f6547..6cde05b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,29 @@ -DbContextScope -============== +## Foreword +This is a fork from [tncalvert/DbContextScope](https://github.com/tncalvert/DbContextScope). The target framework is `.NET Standart2.0` and minimum `Microsoft.EntityFrameworkCore` version is `v2.0.0`. + +If you use this library in your project, please note to use same `nuget versions`! The `DbContextScope` uses `Microsoft.EntityFrameworkCore.Relational v2.0.0`! + +Example `*.csproj` to use `v3.1.1`: + +```xml + + ``` -2017-09-29 - tncalvert -This is a fork to support Entity Framework Core. -As of right now, the project targets .NET 4.6.1. -``` +## Nuget + +[![NuGet](https://img.shields.io/nuget/v/DoJo.EntityFrameworkCore.DbContextScope.svg "nuget")](https://www.nuget.org/packages/DoJo.EntityFrameworkCore.DbContextScope) +[![NuGetDownloads](https://img.shields.io/nuget/dt/DoJo.EntityFrameworkCore.DbContextScope.svg "nuget downloads")](https://www.nuget.org/packages/DoJo.EntityFrameworkCore.DbContextScope) + +A NuGet-package is available [here](https://nuget.org/packages/DoJo.EntityFrameworkCore.DbContextScope/). + + +## Contributors + +Feel free to make a PullRequest or open an Issue to extend this library! + +# DbContextScope A simple and flexible way to manage your Entity Framework DbContext instances. `DbContextScope` was created out of the need for a better way to manage DbContext instances in Entity Framework-based applications. diff --git a/createBranch.ps1 b/createBranch.ps1 new file mode 100644 index 0000000..2dd14a5 --- /dev/null +++ b/createBranch.ps1 @@ -0,0 +1,31 @@ +param([string]$IssueName) + +$branchName = $IssueName + +########################################################### +# clean input string # +########################################################### + +# character replacement +$rWhiteSpace = [regex]'[ ]{1,}' +$rSlash = [regex]'[/]{1,}' +$rBackSlash = [regex]'[\\]{1,}' +$rSeperator = [regex]'[-]{2,}' + +$branchName = $rWhiteSpace.Replace($branchName, "-") +$branchName = $rSlash.Replace($branchName, "-") +$branchName = $rBackSlash.Replace($branchName, "-") +$branchName = $rSeperator.Replace($branchName, "-") +$branchName = $branchName.Trim('-') + +# limit bracnhname to 40 characters length +$branchName = $branchName.Substring('0', '40') + +echo "New branch '$branchName' created" + +git checkout -b $branchName +git add . +git add -u + +#git commit -m "Description of my changes for issue $IssueName" +#git push -u origin $branchName \ No newline at end of file diff --git a/Demo/BusinessLogicServices/UserCreationService.cs b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreationService.cs similarity index 94% rename from Demo/BusinessLogicServices/UserCreationService.cs rename to src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreationService.cs index 31ab13c..ece6d66 100644 --- a/Demo/BusinessLogicServices/UserCreationService.cs +++ b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreationService.cs @@ -1,10 +1,9 @@ -using EntityFrameworkCore.DbContextScope; -using Numero3.EntityFramework.Demo.CommandModel; -using Numero3.EntityFramework.Demo.DomainModel; -using Numero3.EntityFramework.Demo.Repositories; -using System; +using System; +using EntityFrameworkCore.DbContextScope.Test.CommandModel; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; -namespace Numero3.EntityFramework.Demo.BusinessLogicServices { +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { /* * Example business logic service implementing command functionalities (i.e. create / update actions). */ @@ -39,6 +38,7 @@ public void CreateUser(UserCreationSpec userToCreate) { //-- Persist _userRepository.Add(user); + dbContextScope.SaveChanges(); } } diff --git a/Demo/BusinessLogicServices/UserCreditScoreService.cs b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreditScoreService.cs similarity index 94% rename from Demo/BusinessLogicServices/UserCreditScoreService.cs rename to src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreditScoreService.cs index f080d4a..85be0fc 100644 --- a/Demo/BusinessLogicServices/UserCreditScoreService.cs +++ b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserCreditScoreService.cs @@ -1,11 +1,10 @@ -using EntityFrameworkCore.DbContextScope; -using Numero3.EntityFramework.Demo.DatabaseContext; -using System; +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; -namespace Numero3.EntityFramework.Demo.BusinessLogicServices { +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { public class UserCreditScoreService { private readonly IDbContextScopeFactory _dbContextScopeFactory; diff --git a/Demo/BusinessLogicServices/UserEmailService.cs b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserEmailService.cs similarity index 93% rename from Demo/BusinessLogicServices/UserEmailService.cs rename to src/DbContextScope.EF2.Test/BusinessLogicServices/UserEmailService.cs index cda07e7..44b90d3 100644 --- a/Demo/BusinessLogicServices/UserEmailService.cs +++ b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserEmailService.cs @@ -1,10 +1,9 @@ -using EntityFrameworkCore.DbContextScope; -using Numero3.EntityFramework.Demo.DatabaseContext; -using Numero3.EntityFramework.Demo.DomainModel; -using System; +using System; using System.Collections.Generic; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; -namespace Numero3.EntityFramework.Demo.BusinessLogicServices { +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { public class UserEmailService { private readonly IDbContextScopeFactory _dbContextScopeFactory; diff --git a/Demo/BusinessLogicServices/UserQueryService.cs b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserQueryService.cs similarity index 95% rename from Demo/BusinessLogicServices/UserQueryService.cs rename to src/DbContextScope.EF2.Test/BusinessLogicServices/UserQueryService.cs index 5a983ab..5857a80 100644 --- a/Demo/BusinessLogicServices/UserQueryService.cs +++ b/src/DbContextScope.EF2.Test/BusinessLogicServices/UserQueryService.cs @@ -1,14 +1,13 @@ -using EntityFrameworkCore.DbContextScope; -using Numero3.EntityFramework.Demo.DatabaseContext; -using Numero3.EntityFramework.Demo.DomainModel; -using Numero3.EntityFramework.Demo.Repositories; -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; -namespace Numero3.EntityFramework.Demo.BusinessLogicServices { +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { /* * Example business logic service implementing query functionalities (i.e. read actions). */ diff --git a/Demo/CommandModel/UserCreationSpec.cs b/src/DbContextScope.EF2.Test/CommandModel/UserCreationSpec.cs similarity index 91% rename from Demo/CommandModel/UserCreationSpec.cs rename to src/DbContextScope.EF2.Test/CommandModel/UserCreationSpec.cs index fdab605..05cfc47 100644 --- a/Demo/CommandModel/UserCreationSpec.cs +++ b/src/DbContextScope.EF2.Test/CommandModel/UserCreationSpec.cs @@ -1,6 +1,6 @@ using System; -namespace Numero3.EntityFramework.Demo.CommandModel { +namespace EntityFrameworkCore.DbContextScope.Test.CommandModel { /// /// Specifications of the CreateUser command. Defines the properties of a new user. /// diff --git a/Demo/DatabaseContext/UserManagementDbContext.cs b/src/DbContextScope.EF2.Test/DatabaseContext/UserManagementDbContext.cs similarity index 78% rename from Demo/DatabaseContext/UserManagementDbContext.cs rename to src/DbContextScope.EF2.Test/DatabaseContext/UserManagementDbContext.cs index 66c0c1e..fe4112d 100644 --- a/Demo/DatabaseContext/UserManagementDbContext.cs +++ b/src/DbContextScope.EF2.Test/DatabaseContext/UserManagementDbContext.cs @@ -1,7 +1,7 @@ -using Microsoft.EntityFrameworkCore; -using Numero3.EntityFramework.Demo.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using Microsoft.EntityFrameworkCore; -namespace Numero3.EntityFramework.Demo.DatabaseContext { +namespace EntityFrameworkCore.DbContextScope.Test.DatabaseContext { public class UserManagementDbContext : DbContext { // Map our 'User' model by convention public DbSet Users { get; set; } diff --git a/src/DbContextScope.EF2.Test/DbContextScope.EF2.Test.csproj b/src/DbContextScope.EF2.Test/DbContextScope.EF2.Test.csproj new file mode 100644 index 0000000..b19401b --- /dev/null +++ b/src/DbContextScope.EF2.Test/DbContextScope.EF2.Test.csproj @@ -0,0 +1,23 @@ + + + + net461;netcoreapp2.0;netcoreapp3.0 + false + EntityFrameworkCore.DbContextScope.Test + DoJo.EntityFrameworkCore.DbContextScope.Test + + + + + + + + + + + + + + + + diff --git a/src/DbContextScope.EF2.Test/DbContextScopeTest.cs b/src/DbContextScope.EF2.Test/DbContextScopeTest.cs new file mode 100644 index 0000000..2e70509 --- /dev/null +++ b/src/DbContextScope.EF2.Test/DbContextScopeTest.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices; +using EntityFrameworkCore.DbContextScope.Test.CommandModel; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.DbContextScope.Test +{ + public static class UserSpecExtensions + { + public static void Equal(this UserCreationSpec spec, User user) + { + Assert.NotNull(spec); + Assert.NotNull(user); + Assert.Equal(spec.Id, user.Id); + Assert.Equal(spec.Email, user.Email); + Assert.Equal(spec.Name, user.Name); + } + } + + public class DbContextScopeTest + { + private readonly ITestOutputHelper _Output; + + public DbContextScopeTest(ITestOutputHelper output) + { + _Output = output; + } + + private class DbContextFactory : IDbContextFactory + { + public TDbContext CreateDbContext() where TDbContext : DbContext + { + if (typeof(TDbContext) == typeof(UserManagementDbContext)) + { + var config = new DbContextOptionsBuilder() + .UseInMemoryDatabase("1337") + .ConfigureWarnings(warnings => { warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning); }); + return new UserManagementDbContext(config.Options) as TDbContext; + } + + throw new NotImplementedException(typeof(TDbContext).Name); + } + } + + [Fact] + public void FullTest() + { + //-- Poor-man DI - build our dependencies by hand for this demo + var dbContextScopeFactory = new DbContextScopeFactory(new DbContextFactory()); + var ambientDbContextLocator = new AmbientDbContextLocator(); + var userRepository = new UserRepository(ambientDbContextLocator); + + var userCreationService = new UserCreationService(dbContextScopeFactory, userRepository); + var userQueryService = new UserQueryService(dbContextScopeFactory, userRepository); + var userEmailService = new UserEmailService(dbContextScopeFactory); + var userCreditScoreService = new UserCreditScoreService(dbContextScopeFactory); + + _Output.WriteLine( + "This demo uses an EF Core In Memory database. It does not create any external databases."); + _Output.WriteLine(""); + + //-- Demo of typical usage for read and writes + _Output.WriteLine("Creating a user called Mary..."); + var marysSpec = new UserCreationSpec("Mary", "mary@example.com"); + userCreationService.CreateUser(marysSpec); + _Output.WriteLine("Done.\n"); + + _Output.WriteLine("Trying to retrieve our newly created user from the data store..."); + var mary = userQueryService.GetUser(marysSpec.Id); + _Output.WriteLine("OK. Persisted user: {0}", mary); + marysSpec.Equal(mary); + + //-- Demo of nested DbContextScopes + _Output.WriteLine("Creating 2 new users called John and Jeanne in an atomic transaction..."); + var johnSpec = new UserCreationSpec("John", "john@example.com"); + var jeanneSpec = new UserCreationSpec("Jeanne", "jeanne@example.com"); + userCreationService.CreateListOfUsers(johnSpec, jeanneSpec); + _Output.WriteLine("Done.\n"); + + _Output.WriteLine("Trying to retrieve our newly created users from the data store..."); + var createdUsers = userQueryService.GetUsers(johnSpec.Id, jeanneSpec.Id).ToList(); + _Output.WriteLine("OK. Found {0} persisted users.", createdUsers.Count); + + Assert.Equal(2, createdUsers.Count); + johnSpec.Equal(createdUsers[0]); + jeanneSpec.Equal(createdUsers[1]); + + //-- Demo of nested DbContextScopes in the face of an exception. + // If any of the provided users failed to get persisted, none should get persisted. + _Output.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..."); + var julieSpec = new UserCreationSpec("Julie", "julie@example.com"); + var marcSpec = new UserCreationSpec("Marc", "marc@example.com"); + + Assert.ThrowsAny(() => + { + userCreationService.CreateListOfUsersWithIntentionalFailure(julieSpec, marcSpec); + }); + + _Output.WriteLine("Trying to retrieve our newly created users from the data store..."); + var maybeCreatedUsers = userQueryService.GetUsers(julieSpec.Id, marcSpec.Id).ToList(); + _Output.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); + Assert.Equal(0, maybeCreatedUsers.Count); + + //-- Demo of DbContextScope within an async flow + _Output.WriteLine("Trying to retrieve two users John and Jeanne sequentially in an asynchronous manner..."); + // 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 + // don't have a synchronization context. + var usersFoundAsync = userQueryService.GetTwoUsersAsync(johnSpec.Id, jeanneSpec.Id).Result; + _Output.WriteLine("OK. Found {0} persisted users.", usersFoundAsync.Count); + Assert.Equal(2, usersFoundAsync.Count); + johnSpec.Equal(usersFoundAsync[0]); + jeanneSpec.Equal(usersFoundAsync[1]); + + //-- Demo of explicit database transaction. + _Output.WriteLine("Trying to retrieve user John within a READ UNCOMMITTED database transaction..."); + // You'll want to use SQL Profiler or Entity Framework Profiler to verify that the correct transaction isolation + // level is being used. + var userMaybeUncommitted = userQueryService.GetUserUncommitted(johnSpec.Id); + _Output.WriteLine("OK. User found: {0}", userMaybeUncommitted); + johnSpec.Equal(userMaybeUncommitted); + + //-- Demo of disabling the DbContextScope nesting behaviour in order to force the persistence of changes made to entities + // This is a pretty advanced feature that you can safely ignore until you actually need it. + _Output.WriteLine("Will simulate sending a Welcome email to John..."); + + using (var parentScope = dbContextScopeFactory.Create()) + { + var parentDbContext = parentScope.DbContexts.Get(); + + // Load John in the parent DbContext + var john = parentDbContext.Users.Find(johnSpec.Id); + _Output.WriteLine("Before calling SendWelcomeEmail(), john.WelcomeEmailSent = " + + john.WelcomeEmailSent); + + // Now call our SendWelcomeEmail() business logic service method, which will + // update John in a non-nested child context + userEmailService.SendWelcomeEmail(johnSpec.Id); + + // Verify that we can see the modifications made to John by the SendWelcomeEmail() method + _Output.WriteLine("After calling SendWelcomeEmail(), john.WelcomeEmailSent = " + + john.WelcomeEmailSent); + + // Note that even though we're not calling SaveChanges() in the parent scope here, the changes + // made to John by SendWelcomeEmail() will remain persisted in the database as SendWelcomeEmail() + // forced the creation of a new DbContextScope. + } + + //-- Demonstration of DbContextScope and parallel programming + _Output.WriteLine( + "Calculating and storing the credit score of all users in the database in parallel..."); + userCreditScoreService.UpdateCreditScoreForAllUsers(); + _Output.WriteLine("Done."); + } + } +} \ No newline at end of file diff --git a/Demo/DomainModel/User.cs b/src/DbContextScope.EF2.Test/DomainModel/User.cs similarity index 91% rename from Demo/DomainModel/User.cs rename to src/DbContextScope.EF2.Test/DomainModel/User.cs index eabc905..ed9e808 100644 --- a/Demo/DomainModel/User.cs +++ b/src/DbContextScope.EF2.Test/DomainModel/User.cs @@ -1,6 +1,6 @@ using System; -namespace Numero3.EntityFramework.Demo.DomainModel { +namespace EntityFrameworkCore.DbContextScope.Test.DomainModel { // Anemic model to keep this demo application simple. public class User { public Guid Id { get; set; } diff --git a/Demo/Repositories/IUserRepository.cs b/src/DbContextScope.EF2.Test/Repositories/IUserRepository.cs similarity index 55% rename from Demo/Repositories/IUserRepository.cs rename to src/DbContextScope.EF2.Test/Repositories/IUserRepository.cs index 30aa8e7..6493fab 100644 --- a/Demo/Repositories/IUserRepository.cs +++ b/src/DbContextScope.EF2.Test/Repositories/IUserRepository.cs @@ -1,8 +1,8 @@ -using Numero3.EntityFramework.Demo.DomainModel; -using System; +using System; using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; -namespace Numero3.EntityFramework.Demo.Repositories { +namespace EntityFrameworkCore.DbContextScope.Test.Repositories { public interface IUserRepository { User Get(Guid userId); Task GetAsync(Guid userId); diff --git a/Demo/Repositories/UserRepository.cs b/src/DbContextScope.EF2.Test/Repositories/UserRepository.cs similarity index 85% rename from Demo/Repositories/UserRepository.cs rename to src/DbContextScope.EF2.Test/Repositories/UserRepository.cs index 9d3e5fe..e93b165 100644 --- a/Demo/Repositories/UserRepository.cs +++ b/src/DbContextScope.EF2.Test/Repositories/UserRepository.cs @@ -1,10 +1,9 @@ -using EntityFrameworkCore.DbContextScope; -using Numero3.EntityFramework.Demo.DatabaseContext; -using Numero3.EntityFramework.Demo.DomainModel; -using System; +using System; using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; -namespace Numero3.EntityFramework.Demo.Repositories { +namespace EntityFrameworkCore.DbContextScope.Test.Repositories { /* * An example "repository" relying on an ambient DbContext instance. * @@ -43,8 +42,16 @@ public User Get(Guid userId) { return DbContext.Users.Find(userId); } - public Task GetAsync(Guid userId) { + public Task GetAsync(Guid userId) + { return DbContext.Users.FindAsync(userId); + +//#if NETCOREAPP2_0 || NET461 +// return DbContext.Users.FindAsync(userId); +//#endif +//#if NETCOREAPP3_0 +// return DbContext.Users.FindAsync(userId).AsTask(); +//#endif } public void Add(User user) { diff --git a/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreationService.cs b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreationService.cs new file mode 100644 index 0000000..ece6d66 --- /dev/null +++ b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreationService.cs @@ -0,0 +1,110 @@ +using System; +using EntityFrameworkCore.DbContextScope.Test.CommandModel; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; + +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { + /* + * Example business logic service implementing command functionalities (i.e. create / update actions). + */ + public class UserCreationService { + private readonly IDbContextScopeFactory _dbContextScopeFactory; + private readonly IUserRepository _userRepository; + + public UserCreationService(IDbContextScopeFactory dbContextScopeFactory, IUserRepository userRepository) { + _dbContextScopeFactory = dbContextScopeFactory ?? throw new ArgumentNullException("dbContextScopeFactory"); + _userRepository = userRepository ?? throw new ArgumentNullException("userRepository"); + } + + public void CreateUser(UserCreationSpec userToCreate) { + if (userToCreate == null) + throw new ArgumentNullException("userToCreate"); + + userToCreate.Validate(); + + /* + * Typical usage of DbContextScope for a read-write business transaction. + * It's as simple as it looks. + */ + using (var dbContextScope = _dbContextScopeFactory.Create()) { + //-- Build domain model + var user = new User() { + Id = userToCreate.Id, + Name = userToCreate.Name, + Email = userToCreate.Email, + WelcomeEmailSent = false, + CreatedOn = DateTime.UtcNow + }; + + //-- Persist + _userRepository.Add(user); + + dbContextScope.SaveChanges(); + } + } + + public void CreateListOfUsers(params UserCreationSpec[] usersToCreate) { + /* + * Example of DbContextScope nesting in action. + * + * We already have a service method - CreateUser() - that knows how to create a new user + * and implements all the business rules around the creation of a new user + * (e.g. validation, initialization, sending notifications to other domain model objects...). + * + * So we'll just call it in a loop to create the list of new users we've + * been asked to create. + * + * Of course, since this is a business logic service method, we are making + * an implicit guarantee to whoever is calling us that the changes we make to + * the system will be either committed or rolled-back in an atomic manner. + * I.e. either all the users we've been asked to create will get persisted + * or none of them will. It would be disastrous to have a partial failure here + * and end up with some users but not all having been created. + * + * DbContextScope makes this trivial to implement. + * + * The inner DbContextScope instance that the CreateUser() method creates + * will join our top-level scope. This ensures that the same DbContext instance is + * going to be used throughout this business transaction. + * + */ + + using (var dbContextScope = _dbContextScopeFactory.Create()) { + foreach (var toCreate in usersToCreate) { + CreateUser(toCreate); + } + + // All the changes will get persisted here + dbContextScope.SaveChanges(); + } + } + + public void CreateListOfUsersWithIntentionalFailure(params UserCreationSpec[] usersToCreate) { + /* + * Here, we'll verify that inner DbContextScopes really join the parent scope and + * don't persist their changes until the parent scope completes successfully. + */ + + var firstUser = true; + + using (var dbContextScope = _dbContextScopeFactory.Create()) { + foreach (var toCreate in usersToCreate) { + if (firstUser) { + CreateUser(toCreate); + Console.WriteLine("Successfully created a new User named '{0}'.", toCreate.Name); + firstUser = false; + } else { + // OK. So we've successfully persisted one user. + // We're going to simulate a failure when attempting to + // persist the second user and see what ends up getting + // persisted in the DB. + throw new Exception(String.Format("Oh no! An error occurred when attempting to create user named '{0}' in our database.", toCreate.Name)); + } + } + + dbContextScope.SaveChanges(); + } + } + } +} + diff --git a/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreditScoreService.cs b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreditScoreService.cs new file mode 100644 index 0000000..85be0fc --- /dev/null +++ b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserCreditScoreService.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; + +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { + public class UserCreditScoreService { + private readonly IDbContextScopeFactory _dbContextScopeFactory; + + public UserCreditScoreService(IDbContextScopeFactory dbContextScopeFactory) { + _dbContextScopeFactory = dbContextScopeFactory ?? throw new ArgumentNullException("dbContextScopeFactory"); + } + + public void UpdateCreditScoreForAllUsers() { + /* + * Demo of DbContextScope + parallel programming. + */ + + using (var dbContextScope = _dbContextScopeFactory.Create()) { + //-- Get all users + var dbContext = dbContextScope.DbContexts.Get(); + var userIds = dbContext.Users.Select(u => u.Id).ToList(); + + Console.WriteLine("Found {0} users in the database. Will calculate and store their credit scores in parallel.", userIds.Count); + + //-- Calculate and store the credit score of each user + // We're going to imagine that calculating a credit score of a user takes some time. + // So we'll do it in parallel. + + // You MUST call SuppressAmbientContext() when kicking off a parallel execution flow + // within a DbContextScope. Otherwise, this DbContextScope will remain the ambient scope + // in the parallel flows of execution, potentially leading to multiple threads + // accessing the same DbContext instance. + using (_dbContextScopeFactory.SuppressAmbientContext()) { + Parallel.ForEach(userIds, UpdateCreditScore); + } + + // Note: SaveChanges() isn't going to do anything in this instance since all the changes + // were actually made and saved in separate DbContextScopes created in separate threads. + dbContextScope.SaveChanges(); + } + } + + public void UpdateCreditScore(Guid userId) { + using (var dbContextScope = _dbContextScopeFactory.Create()) { + var dbContext = dbContextScope.DbContexts.Get(); + var user = dbContext.Users.Find(userId); + if (user == null) + throw new ArgumentException(String.Format("Invalid userId provided: {0}. Couldn't find a User with this ID.", userId)); + + // Simulate the calculation of a credit score taking some time + var random = new Random(Thread.CurrentThread.ManagedThreadId); + Thread.Sleep(random.Next(300, 1000)); + + user.CreditScore = random.Next(1, 100); + dbContextScope.SaveChanges(); + } + } + } +} diff --git a/src/DbContextScope.EF3.Test/BusinessLogicServices/UserEmailService.cs b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserEmailService.cs new file mode 100644 index 0000000..44b90d3 --- /dev/null +++ b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserEmailService.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; + +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { + public class UserEmailService { + private readonly IDbContextScopeFactory _dbContextScopeFactory; + + public UserEmailService(IDbContextScopeFactory dbContextScopeFactory) { + _dbContextScopeFactory = dbContextScopeFactory ?? throw new ArgumentNullException("dbContextScopeFactory"); + } + + public void SendWelcomeEmail(Guid userId) { + /* + * Demo of forcing the creation of a new DbContextScope + * to ensure that changes made to the model in this service + * method are persisted even if that method happens to get + * called within the scope of a wider business transaction + * that eventually fails for any reason. + * + * This is an advanced feature that should be used as rarely + * as possible (and ideally, never). + */ + + // We're going to send a welcome email to the provided user + // (if one hasn't been sent already). Once sent, we'll update + // that User entity in our DB to record that its Welcome email + // has been sent. + + // Emails can't be rolled-back. Once they're sent, they're sent. + // So once the email has been sent successfully, we absolutely + // must persist this fact in our DB. Even if that method is called + // by another busines logic service method as part of a wider + // business transaction and even if that parent business transaction + // ends up failing for any reason, we still must ensure that + // we have recorded the fact that the Welcome email has been sent. + // Otherwise, we would risk spamming our users with repeated Welcome + // emails. + + // Force the creation of a new DbContextScope so that the changes we make here are + // guaranteed to get persisted regardless of what happens after this method has completed. + using (var dbContextScope = _dbContextScopeFactory.Create(DbContextScopeOption.ForceCreateNew)) { + var dbContext = dbContextScope.DbContexts.Get(); + var user = dbContext.Users.Find(userId); + + if (user == null) + throw new ArgumentException(String.Format("Invalid userId provided: {0}. Couldn't find a User with this ID.", userId)); + + if (!user.WelcomeEmailSent) { + SendEmail(user.Email); + user.WelcomeEmailSent = true; + } + + dbContextScope.SaveChanges(); + + // When you force the creation of a new DbContextScope, you must force the parent + // scope (if any) to reload the entities you've modified here. Otherwise, the method calling + // you might not be able to see the changes you made here. + dbContextScope.RefreshEntitiesInParentScope(new List { user }); + } + } + + private void SendEmail(string emailAddress) { + // Send the email synchronously. Throw if any error occurs. + // [...] + } + } +} diff --git a/src/DbContextScope.EF3.Test/BusinessLogicServices/UserQueryService.cs b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserQueryService.cs new file mode 100644 index 0000000..5857a80 --- /dev/null +++ b/src/DbContextScope.EF3.Test/BusinessLogicServices/UserQueryService.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; + +namespace EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices { + /* + * Example business logic service implementing query functionalities (i.e. read actions). + */ + public class UserQueryService { + private readonly IDbContextScopeFactory _dbContextScopeFactory; + private readonly IUserRepository _userRepository; + + public UserQueryService(IDbContextScopeFactory dbContextScopeFactory, IUserRepository userRepository) { + _dbContextScopeFactory = dbContextScopeFactory ?? throw new ArgumentNullException("dbContextScopeFactory"); + _userRepository = userRepository ?? throw new ArgumentNullException("userRepository"); + } + + public User GetUser(Guid userId) { + /* + * An example of using DbContextScope for read-only queries. + * Here, we access the Entity Framework DbContext directly from + * the business logic service class. + * + * Calling SaveChanges() is not necessary here (and in fact not + * possible) since we created a read-only scope. + */ + using (var dbContextScope = _dbContextScopeFactory.CreateReadOnly()) { + var dbContext = dbContextScope.DbContexts.Get(); + var user = dbContext.Users.Find(userId); + + if (user == null) + throw new ArgumentException(String.Format("Invalid value provided for userId: [{0}]. Couldn't find a user with this ID.", userId)); + + return user; + } + } + + public IEnumerable GetUsers(params Guid[] userIds) { + using (var dbContextScope = _dbContextScopeFactory.CreateReadOnly()) { + var dbContext = dbContextScope.DbContexts.Get(); + return dbContext.Users.Where(u => userIds.Contains(u.Id)).ToList(); + } + } + + public User GetUserViaRepository(Guid userId) { + /* + * Same as GetUsers() but using a repository layer instead of accessing the + * EF DbContext directly. + * + * Note how we don't have to worry about knowing what type of DbContext the + * repository will need, about creating the DbContext instance or about passing + * DbContext instances around. + * + * The DbContextScope will take care of creating the necessary DbContext instances + * and making them available as ambient contexts for our repository layer to use. + * It will also guarantee that only one instance of any given DbContext type exists + * within its scope ensuring that all persistent entities managed within that scope + * are attached to the same DbContext. + */ + using (_dbContextScopeFactory.CreateReadOnly()) { + var user = _userRepository.Get(userId); + + if (user == null) + throw new ArgumentException(String.Format("Invalid value provided for userId: [{0}]. Couldn't find a user with this ID.", userId)); + + return user; + } + } + + public async Task> GetTwoUsersAsync(Guid userId1, Guid userId2) { + /* + * A very contrived example of ambient DbContextScope within an async flow. + * + * Note that the ConfigureAwait(false) calls here aren't strictly necessary + * and are unrelated to DbContextScope. You can remove them if you want and + * the code will run in the same way. It is however good practice to configure + * all your awaitables in library code to not continue + * on the captured synchronization context. It avoids having to pay the overhead + * of capturing the sync context and running the task continuation on it when + * library code doesn't need that context. If also helps prevent potential deadlocks + * if the upstream code has been poorly written and blocks on async tasks. + * + * "Library code" is any code in layers under the presentation tier. Typically any code + * other that code in ASP.NET MVC / WebApi controllers or Window Form / WPF forms. + * + * See http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx for + * more details. + */ + + using (_dbContextScopeFactory.CreateReadOnly()) { + var user1 = await _userRepository.GetAsync(userId1).ConfigureAwait(false); + + // We're now in the continuation of the first async task. This is most + // likely executing in a thread from the ThreadPool, i.e. in a different + // thread that the one where we created our DbContextScope. Our ambient + // DbContextScope is still available here however, which allows the call + // below to succeed. + + var user2 = await _userRepository.GetAsync(userId2).ConfigureAwait(false); + + // In other words, DbContextScope works with async execution flow as you'd expect: + // It Just Works. + + return new List { user1, user2 }.Where(u => u != null).ToList(); + } + } + + public User GetUserUncommitted(Guid userId) { + /* + * An example of explicit database transaction. + * + * Read the comment for CreateReadOnlyWithTransaction() before using this overload + * as there are gotchas when doing this! + */ + using (_dbContextScopeFactory.CreateReadOnlyWithTransaction(IsolationLevel.ReadUncommitted)) { + return _userRepository.Get(userId); + } + } + } +} diff --git a/src/DbContextScope.EF3.Test/CommandModel/UserCreationSpec.cs b/src/DbContextScope.EF3.Test/CommandModel/UserCreationSpec.cs new file mode 100644 index 0000000..05cfc47 --- /dev/null +++ b/src/DbContextScope.EF3.Test/CommandModel/UserCreationSpec.cs @@ -0,0 +1,26 @@ +using System; + +namespace EntityFrameworkCore.DbContextScope.Test.CommandModel { + /// + /// Specifications of the CreateUser command. Defines the properties of a new user. + /// + public class UserCreationSpec { + /// + /// The Id automatically generated for this user. + /// + public Guid Id { get; protected set; } + + public string Name { get; protected set; } + public string Email { get; protected set; } + + public UserCreationSpec(string name, string email) { + Id = Guid.NewGuid(); + Name = name; + Email = email; + } + + public void Validate() { + // [...] + } + } +} diff --git a/src/DbContextScope.EF3.Test/DatabaseContext/UserManagementDbContext.cs b/src/DbContextScope.EF3.Test/DatabaseContext/UserManagementDbContext.cs new file mode 100644 index 0000000..fe4112d --- /dev/null +++ b/src/DbContextScope.EF3.Test/DatabaseContext/UserManagementDbContext.cs @@ -0,0 +1,21 @@ +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.DbContextScope.Test.DatabaseContext { + public class UserManagementDbContext : DbContext { + // Map our 'User' model by convention + public DbSet Users { get; set; } + + public UserManagementDbContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(builder => { + builder.Property(m => m.Name).IsRequired(); + builder.Property(m => m.Email).IsRequired(); + }); + } + } +} diff --git a/src/DbContextScope.EF3.Test/DbContextScope.EF3.Test.csproj b/src/DbContextScope.EF3.Test/DbContextScope.EF3.Test.csproj new file mode 100644 index 0000000..24cf54c --- /dev/null +++ b/src/DbContextScope.EF3.Test/DbContextScope.EF3.Test.csproj @@ -0,0 +1,23 @@ + + + + net461;netcoreapp2.0;netcoreapp3.0 + false + EntityFrameworkCore.DbContextScope.Test + DoJo.EntityFrameworkCore.DbContextScope.Test + + + + + + + + + + + + + + + + diff --git a/src/DbContextScope.EF3.Test/DbContextScopeTest.cs b/src/DbContextScope.EF3.Test/DbContextScopeTest.cs new file mode 100644 index 0000000..2e70509 --- /dev/null +++ b/src/DbContextScope.EF3.Test/DbContextScopeTest.cs @@ -0,0 +1,165 @@ +using System; +using System.Linq; +using EntityFrameworkCore.DbContextScope.Test.BusinessLogicServices; +using EntityFrameworkCore.DbContextScope.Test.CommandModel; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; +using EntityFrameworkCore.DbContextScope.Test.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Xunit; +using Xunit.Abstractions; + +namespace EntityFrameworkCore.DbContextScope.Test +{ + public static class UserSpecExtensions + { + public static void Equal(this UserCreationSpec spec, User user) + { + Assert.NotNull(spec); + Assert.NotNull(user); + Assert.Equal(spec.Id, user.Id); + Assert.Equal(spec.Email, user.Email); + Assert.Equal(spec.Name, user.Name); + } + } + + public class DbContextScopeTest + { + private readonly ITestOutputHelper _Output; + + public DbContextScopeTest(ITestOutputHelper output) + { + _Output = output; + } + + private class DbContextFactory : IDbContextFactory + { + public TDbContext CreateDbContext() where TDbContext : DbContext + { + if (typeof(TDbContext) == typeof(UserManagementDbContext)) + { + var config = new DbContextOptionsBuilder() + .UseInMemoryDatabase("1337") + .ConfigureWarnings(warnings => { warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning); }); + return new UserManagementDbContext(config.Options) as TDbContext; + } + + throw new NotImplementedException(typeof(TDbContext).Name); + } + } + + [Fact] + public void FullTest() + { + //-- Poor-man DI - build our dependencies by hand for this demo + var dbContextScopeFactory = new DbContextScopeFactory(new DbContextFactory()); + var ambientDbContextLocator = new AmbientDbContextLocator(); + var userRepository = new UserRepository(ambientDbContextLocator); + + var userCreationService = new UserCreationService(dbContextScopeFactory, userRepository); + var userQueryService = new UserQueryService(dbContextScopeFactory, userRepository); + var userEmailService = new UserEmailService(dbContextScopeFactory); + var userCreditScoreService = new UserCreditScoreService(dbContextScopeFactory); + + _Output.WriteLine( + "This demo uses an EF Core In Memory database. It does not create any external databases."); + _Output.WriteLine(""); + + //-- Demo of typical usage for read and writes + _Output.WriteLine("Creating a user called Mary..."); + var marysSpec = new UserCreationSpec("Mary", "mary@example.com"); + userCreationService.CreateUser(marysSpec); + _Output.WriteLine("Done.\n"); + + _Output.WriteLine("Trying to retrieve our newly created user from the data store..."); + var mary = userQueryService.GetUser(marysSpec.Id); + _Output.WriteLine("OK. Persisted user: {0}", mary); + marysSpec.Equal(mary); + + //-- Demo of nested DbContextScopes + _Output.WriteLine("Creating 2 new users called John and Jeanne in an atomic transaction..."); + var johnSpec = new UserCreationSpec("John", "john@example.com"); + var jeanneSpec = new UserCreationSpec("Jeanne", "jeanne@example.com"); + userCreationService.CreateListOfUsers(johnSpec, jeanneSpec); + _Output.WriteLine("Done.\n"); + + _Output.WriteLine("Trying to retrieve our newly created users from the data store..."); + var createdUsers = userQueryService.GetUsers(johnSpec.Id, jeanneSpec.Id).ToList(); + _Output.WriteLine("OK. Found {0} persisted users.", createdUsers.Count); + + Assert.Equal(2, createdUsers.Count); + johnSpec.Equal(createdUsers[0]); + jeanneSpec.Equal(createdUsers[1]); + + //-- Demo of nested DbContextScopes in the face of an exception. + // If any of the provided users failed to get persisted, none should get persisted. + _Output.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..."); + var julieSpec = new UserCreationSpec("Julie", "julie@example.com"); + var marcSpec = new UserCreationSpec("Marc", "marc@example.com"); + + Assert.ThrowsAny(() => + { + userCreationService.CreateListOfUsersWithIntentionalFailure(julieSpec, marcSpec); + }); + + _Output.WriteLine("Trying to retrieve our newly created users from the data store..."); + var maybeCreatedUsers = userQueryService.GetUsers(julieSpec.Id, marcSpec.Id).ToList(); + _Output.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); + Assert.Equal(0, maybeCreatedUsers.Count); + + //-- Demo of DbContextScope within an async flow + _Output.WriteLine("Trying to retrieve two users John and Jeanne sequentially in an asynchronous manner..."); + // 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 + // don't have a synchronization context. + var usersFoundAsync = userQueryService.GetTwoUsersAsync(johnSpec.Id, jeanneSpec.Id).Result; + _Output.WriteLine("OK. Found {0} persisted users.", usersFoundAsync.Count); + Assert.Equal(2, usersFoundAsync.Count); + johnSpec.Equal(usersFoundAsync[0]); + jeanneSpec.Equal(usersFoundAsync[1]); + + //-- Demo of explicit database transaction. + _Output.WriteLine("Trying to retrieve user John within a READ UNCOMMITTED database transaction..."); + // You'll want to use SQL Profiler or Entity Framework Profiler to verify that the correct transaction isolation + // level is being used. + var userMaybeUncommitted = userQueryService.GetUserUncommitted(johnSpec.Id); + _Output.WriteLine("OK. User found: {0}", userMaybeUncommitted); + johnSpec.Equal(userMaybeUncommitted); + + //-- Demo of disabling the DbContextScope nesting behaviour in order to force the persistence of changes made to entities + // This is a pretty advanced feature that you can safely ignore until you actually need it. + _Output.WriteLine("Will simulate sending a Welcome email to John..."); + + using (var parentScope = dbContextScopeFactory.Create()) + { + var parentDbContext = parentScope.DbContexts.Get(); + + // Load John in the parent DbContext + var john = parentDbContext.Users.Find(johnSpec.Id); + _Output.WriteLine("Before calling SendWelcomeEmail(), john.WelcomeEmailSent = " + + john.WelcomeEmailSent); + + // Now call our SendWelcomeEmail() business logic service method, which will + // update John in a non-nested child context + userEmailService.SendWelcomeEmail(johnSpec.Id); + + // Verify that we can see the modifications made to John by the SendWelcomeEmail() method + _Output.WriteLine("After calling SendWelcomeEmail(), john.WelcomeEmailSent = " + + john.WelcomeEmailSent); + + // Note that even though we're not calling SaveChanges() in the parent scope here, the changes + // made to John by SendWelcomeEmail() will remain persisted in the database as SendWelcomeEmail() + // forced the creation of a new DbContextScope. + } + + //-- Demonstration of DbContextScope and parallel programming + _Output.WriteLine( + "Calculating and storing the credit score of all users in the database in parallel..."); + userCreditScoreService.UpdateCreditScoreForAllUsers(); + _Output.WriteLine("Done."); + } + } +} \ No newline at end of file diff --git a/src/DbContextScope.EF3.Test/DomainModel/User.cs b/src/DbContextScope.EF3.Test/DomainModel/User.cs new file mode 100644 index 0000000..ed9e808 --- /dev/null +++ b/src/DbContextScope.EF3.Test/DomainModel/User.cs @@ -0,0 +1,17 @@ +using System; + +namespace EntityFrameworkCore.DbContextScope.Test.DomainModel { + // Anemic model to keep this demo application simple. + public class User { + public Guid Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public int CreditScore { get; set; } + public bool WelcomeEmailSent { get; set; } + public DateTime CreatedOn { get; set; } + + public override string ToString() { + 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")); + } + } +} diff --git a/src/DbContextScope.EF3.Test/Repositories/IUserRepository.cs b/src/DbContextScope.EF3.Test/Repositories/IUserRepository.cs new file mode 100644 index 0000000..6493fab --- /dev/null +++ b/src/DbContextScope.EF3.Test/Repositories/IUserRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; + +namespace EntityFrameworkCore.DbContextScope.Test.Repositories { + public interface IUserRepository { + User Get(Guid userId); + Task GetAsync(Guid userId); + void Add(User user); + } +} \ No newline at end of file diff --git a/src/DbContextScope.EF3.Test/Repositories/UserRepository.cs b/src/DbContextScope.EF3.Test/Repositories/UserRepository.cs new file mode 100644 index 0000000..fccc3af --- /dev/null +++ b/src/DbContextScope.EF3.Test/Repositories/UserRepository.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using EntityFrameworkCore.DbContextScope.Test.DatabaseContext; +using EntityFrameworkCore.DbContextScope.Test.DomainModel; + +namespace EntityFrameworkCore.DbContextScope.Test.Repositories { + /* + * An example "repository" relying on an ambient DbContext instance. + * + * Since we use EF to persist our data, the actual repository is of course the EF DbContext. This + * class is called a "repository" for old time's sake but is merely just a collection + * of pre-built Linq-to-Entities queries. This avoids having these queries copied and + * pasted in every service method that need them and facilitates unit testing. + * + * Whether your application would benefit from using this additional layer or would + * be better off if its service methods queried the DbContext directly or used some sort of query + * object pattern is a design decision for you to make. + * + * DbContextScope is agnostic to this and will happily let you use any approach you + * deem most suitable for your application. + * + */ + public class UserRepository : IUserRepository { + private readonly IAmbientDbContextLocator _ambientDbContextLocator; + + private UserManagementDbContext DbContext { + get { + var dbContext = _ambientDbContextLocator.Get(); + + if (dbContext == null) + 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."); + + return dbContext; + } + } + + public UserRepository(IAmbientDbContextLocator ambientDbContextLocator) { + _ambientDbContextLocator = ambientDbContextLocator ?? throw new ArgumentNullException("ambientDbContextLocator"); + } + + public User Get(Guid userId) { + return DbContext.Users.Find(userId); + } + + public Task GetAsync(Guid userId) + { + return DbContext.Users.FindAsync(userId).AsTask(); + +//#if NETCOREAPP2_0 || NET461 +// return DbContext.Users.FindAsync(userId); +//#endif +//#if NETCOREAPP3_0 +// return DbContext.Users.FindAsync(userId).AsTask(); +//#endif + } + + public void Add(User user) { + DbContext.Users.Add(user); + } + } +} diff --git a/DbContextScope.sln b/src/DbContextScope.sln similarity index 56% rename from DbContextScope.sln rename to src/DbContextScope.sln index 482ee3e..98a52e6 100644 --- a/DbContextScope.sln +++ b/src/DbContextScope.sln @@ -1,24 +1,21 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.16 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29806.167 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{B9148EB1-044F-442C-AC23-B6ADD1E3FB2E}" - ProjectSection(SolutionItems) = preProject - .nuget\NuGet.Config = .nuget\NuGet.Config - .nuget\NuGet.exe = .nuget\NuGet.exe - .nuget\NuGet.targets = .nuget\NuGet.targets - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9BE61ABE-754C-4880-9B6D-0187E33BAB09}" ProjectSection(SolutionItems) = preProject + ..\Directory.build.props = ..\Directory.build.props + ..\GitVersion.yml = ..\GitVersion.yml LICENSE = LICENSE README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DbContextScope", "DbContextScope\DbContextScope.csproj", "{D84F0F17-48F7-4BF6-B679-07F57A23CEA6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "Demo\Demo.csproj", "{BB1A9648-FEF6-4A80-B7EC-C0C8B69698FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DbContextScope.EF2.Test", "DbContextScope.EF2.Test\DbContextScope.EF2.Test.csproj", "{A0EAD0E3-563E-4DBA-B372-6184F621D83A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DbContextScope.EF3.Test", "DbContextScope.EF3.Test\DbContextScope.EF3.Test.csproj", "{CDDBFD34-CE32-419F-B778-C54EFACA5B6C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -30,10 +27,14 @@ Global {D84F0F17-48F7-4BF6-B679-07F57A23CEA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {D84F0F17-48F7-4BF6-B679-07F57A23CEA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {D84F0F17-48F7-4BF6-B679-07F57A23CEA6}.Release|Any CPU.Build.0 = Release|Any CPU - {BB1A9648-FEF6-4A80-B7EC-C0C8B69698FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB1A9648-FEF6-4A80-B7EC-C0C8B69698FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB1A9648-FEF6-4A80-B7EC-C0C8B69698FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB1A9648-FEF6-4A80-B7EC-C0C8B69698FB}.Release|Any CPU.Build.0 = Release|Any CPU + {A0EAD0E3-563E-4DBA-B372-6184F621D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0EAD0E3-563E-4DBA-B372-6184F621D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0EAD0E3-563E-4DBA-B372-6184F621D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0EAD0E3-563E-4DBA-B372-6184F621D83A}.Release|Any CPU.Build.0 = Release|Any CPU + {CDDBFD34-CE32-419F-B778-C54EFACA5B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDDBFD34-CE32-419F-B778-C54EFACA5B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDDBFD34-CE32-419F-B778-C54EFACA5B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDDBFD34-CE32-419F-B778-C54EFACA5B6C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/DbContextScope/CallContext.cs b/src/DbContextScope/CallContext.cs new file mode 100644 index 0000000..24d321d --- /dev/null +++ b/src/DbContextScope/CallContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using System.Threading; + +namespace EntityFrameworkCore.DbContextScope +{ + + /// + /// Provides a way to set contextual data that flows with the call and + /// async context of a test or invocation. + /// http://www.cazzulino.com/callcontext-netstandard-netcore.html + /// + internal static class CallContext + { + static ConcurrentDictionary> state = new ConcurrentDictionary>(); + + /// + /// Stores a given object and associates it with the specified name. + /// + /// The name with which to associate the new item in the call context. + /// The object to store in the call context. + public static void SetData(string name, object data) => + state.GetOrAdd(name, _ => new AsyncLocal()).Value = data; + + /// + /// Retrieves an object with the specified name from the . + /// + /// The name of the item in the call context. + /// The object in the call context associated with the specified name, or if not found. + public static object GetData(string name) => + state.TryGetValue(name, out AsyncLocal data) ? data.Value : null; + } +} \ No newline at end of file diff --git a/src/DbContextScope/DbContextScope.csproj b/src/DbContextScope/DbContextScope.csproj new file mode 100644 index 0000000..65d2238 --- /dev/null +++ b/src/DbContextScope/DbContextScope.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + DoJo.EntityFrameworkCore.DbContextScope + true + DoJo.EntityFrameworkCore.DbContextScope + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/DbContextScope/Enums/DbContextScopeOptions.cs b/src/DbContextScope/Enums/DbContextScopeOptions.cs similarity index 100% rename from DbContextScope/Enums/DbContextScopeOptions.cs rename to src/DbContextScope/Enums/DbContextScopeOptions.cs diff --git a/src/DbContextScope/Extensions/DbContextExtensions.cs b/src/DbContextScope/Extensions/DbContextExtensions.cs new file mode 100644 index 0000000..be771d1 --- /dev/null +++ b/src/DbContextScope/Extensions/DbContextExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; + +#if NETCOREAPP2_0 || NET461 +using Microsoft.EntityFrameworkCore.Infrastructure; +#endif + +#if NETCOREAPP3_0 +using Microsoft.EntityFrameworkCore.Internal; +#endif + +namespace DbContextScope.Extensions +{ + public static class DbContextExtensions + { + /// + /// Convenience method to get the + /// + /// + /// + public static IStateManager GetStateManager(this DbContext context) + { + // seems to work for both frameworks + // v2.2.6 + // v3.1.1 + return context.GetDependencies().StateManager; +#if NETCOREAPP2_0 || NET461 + return context.ChangeTracker.GetInfrastructure(); +#endif + +#if NETCOREAPP3_0 + return context.GetDependencies().StateManager; +#endif + } + } +} diff --git a/DbContextScope/Implementations/AmbientContextSuppressor.cs b/src/DbContextScope/Implementations/AmbientContextSuppressor.cs similarity index 100% rename from DbContextScope/Implementations/AmbientContextSuppressor.cs rename to src/DbContextScope/Implementations/AmbientContextSuppressor.cs diff --git a/DbContextScope/Implementations/AmbientDbContextLocator.cs b/src/DbContextScope/Implementations/AmbientDbContextLocator.cs similarity index 100% rename from DbContextScope/Implementations/AmbientDbContextLocator.cs rename to src/DbContextScope/Implementations/AmbientDbContextLocator.cs diff --git a/DbContextScope/Implementations/DbContextCollection.cs b/src/DbContextScope/Implementations/DbContextCollection.cs similarity index 100% rename from DbContextScope/Implementations/DbContextCollection.cs rename to src/DbContextScope/Implementations/DbContextCollection.cs diff --git a/DbContextScope/Implementations/DbContextReadOnlyScope.cs b/src/DbContextScope/Implementations/DbContextReadOnlyScope.cs similarity index 100% rename from DbContextScope/Implementations/DbContextReadOnlyScope.cs rename to src/DbContextScope/Implementations/DbContextReadOnlyScope.cs diff --git a/DbContextScope/Implementations/DbContextScope.cs b/src/DbContextScope/Implementations/DbContextScope.cs similarity index 74% rename from DbContextScope/Implementations/DbContextScope.cs rename to src/DbContextScope/Implementations/DbContextScope.cs index c67f75f..63d51c1 100644 --- a/DbContextScope/Implementations/DbContextScope.cs +++ b/src/DbContextScope/Implementations/DbContextScope.cs @@ -5,6 +5,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ + using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; @@ -12,12 +13,17 @@ using System.Data; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.Remoting.Messaging; using System.Threading; using System.Threading.Tasks; - -namespace EntityFrameworkCore.DbContextScope { - public class DbContextScope : IDbContextScope { +using DbContextScope.Extensions; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Internal; + +namespace EntityFrameworkCore.DbContextScope +{ + public class DbContextScope : IDbContextScope + { private bool _disposed; private bool _readOnly; private bool _completed; @@ -25,31 +31,48 @@ public class DbContextScope : IDbContextScope { private DbContextScope _parentScope; private DbContextCollection _dbContexts; - public IDbContextCollection DbContexts { get { return _dbContexts; } } + public IDbContextCollection DbContexts + { + get { return _dbContexts; } + } public DbContextScope(IDbContextFactory dbContextFactory = null) : - this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: false, isolationLevel: null, dbContextFactory: dbContextFactory) { } + this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: false, isolationLevel: null, + dbContextFactory: dbContextFactory) + { + } public DbContextScope(bool readOnly, IDbContextFactory dbContextFactory = null) - : this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: readOnly, isolationLevel: null, dbContextFactory: dbContextFactory) { } + : this(joiningOption: DbContextScopeOption.JoinExisting, readOnly: readOnly, isolationLevel: null, + dbContextFactory: dbContextFactory) + { + } - public DbContextScope(DbContextScopeOption joiningOption, bool readOnly, IsolationLevel? isolationLevel, IDbContextFactory dbContextFactory = null) { + public DbContextScope(DbContextScopeOption joiningOption, bool readOnly, IsolationLevel? isolationLevel, + IDbContextFactory dbContextFactory = null) + { if (isolationLevel.HasValue && joiningOption == DbContextScopeOption.JoinExisting) - 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)."); + 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)."); _disposed = false; _completed = false; _readOnly = readOnly; _parentScope = GetAmbientScope(); - if (_parentScope != null && joiningOption == DbContextScopeOption.JoinExisting) { - if (_parentScope._readOnly && !this._readOnly) { - throw new InvalidOperationException("Cannot nest a read/write DbContextScope within a read-only DbContextScope."); + if (_parentScope != null && joiningOption == DbContextScopeOption.JoinExisting) + { + if (_parentScope._readOnly && !this._readOnly) + { + throw new InvalidOperationException( + "Cannot nest a read/write DbContextScope within a read-only DbContextScope."); } _nested = true; _dbContexts = _parentScope._dbContexts; - } else { + } + else + { _nested = false; _dbContexts = new DbContextCollection(readOnly, isolationLevel, dbContextFactory); } @@ -57,16 +80,19 @@ public DbContextScope(DbContextScopeOption joiningOption, bool readOnly, Isolati SetAmbientScope(this); } - public int SaveChanges() { + public int SaveChanges() + { if (_disposed) throw new ObjectDisposedException("DbContextScope"); if (_completed) - 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."); + 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."); // Only save changes if we're not a nested scope. Otherwise, let the top-level scope // decide when the changes should be saved. var c = 0; - if (!_nested) { + if (!_nested) + { c = CommitInternal(); } @@ -75,22 +101,26 @@ public int SaveChanges() { return c; } - public Task SaveChangesAsync() { + public Task SaveChangesAsync() + { return SaveChangesAsync(CancellationToken.None); } - public async Task SaveChangesAsync(CancellationToken cancelToken) { + public async Task SaveChangesAsync(CancellationToken cancelToken) + { if (cancelToken == null) throw new ArgumentNullException("cancelToken"); if (_disposed) throw new ObjectDisposedException("DbContextScope"); if (_completed) - 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."); + 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."); // Only save changes if we're not a nested scope. Otherwise, let the top-level scope // decide when the changes should be saved. var c = 0; - if (!_nested) { + if (!_nested) + { c = await CommitInternalAsync(cancelToken).ConfigureAwait(false); } @@ -98,19 +128,23 @@ public async Task SaveChangesAsync(CancellationToken cancelToken) { return c; } - private int CommitInternal() { + private int CommitInternal() + { return _dbContexts.Commit(); } - private Task CommitInternalAsync(CancellationToken cancelToken) { + private Task CommitInternalAsync(CancellationToken cancelToken) + { return _dbContexts.CommitAsync(cancelToken); } - private void RollbackInternal() { + private void RollbackInternal() + { _dbContexts.Rollback(); } - public void RefreshEntitiesInParentScope(IEnumerable entities) { + public void RefreshEntitiesInParentScope(IEnumerable entities) + { if (entities == null) return; @@ -135,10 +169,12 @@ public void RefreshEntitiesInParentScope(IEnumerable entities) { // So we must cast the DbContext instances to IObjectContextAdapter in order to access their ObjectContext. // This cast is completely safe. - foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) { + foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) + { var correspondingParentContext = - _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => parentContext.GetType() == contextInCurrentScope.GetType()) - as DbContext; + _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => + parentContext.GetType() == contextInCurrentScope.GetType()) + as DbContext; if (correspondingParentContext == null) continue; // No DbContext of this type has been created in the parent scope yet. So no need to refresh anything for this DbContext type. @@ -146,12 +182,15 @@ public void RefreshEntitiesInParentScope(IEnumerable entities) { // Both our scope and the parent scope have an instance of the same DbContext type. // We can now look in the parent DbContext instance for entities that need to // be refreshed. - foreach (var toRefresh in entities) { + foreach (var toRefresh in entities) + { // First, we need to find what the EntityKey for this entity is. // We need this EntityKey in order to check if this entity has // already been loaded in the parent DbContext's first-level cache (the ObjectStateManager). - var stateInCurrentScope = contextInCurrentScope.ChangeTracker.GetInfrastructure().TryGetEntry(toRefresh); - if (stateInCurrentScope != null) { + + var stateInCurrentScope = contextInCurrentScope.GetStateManager().TryGetEntry(toRefresh); + if (stateInCurrentScope != null) + { // NOTE(tim): Thanks to ninety7 (https://github.com/ninety7/DbContextScope) and apawsey (https://github.com/apawsey/DbContextScope) // for examples on how identify the matching entities in EF Core. var entityType = stateInCurrentScope.Entity.GetType(); @@ -161,12 +200,15 @@ public void RefreshEntitiesInParentScope(IEnumerable entities) { .ToArray(); // Now we can see if that entity exists in the parent DbContext instance and refresh it. - var stateInParentScope = correspondingParentContext.ChangeTracker.GetInfrastructure().TryGetEntry(key, keyValues); - if (stateInParentScope != null) { + var stateInParentScope = correspondingParentContext.GetStateManager() + .TryGetEntry(key, keyValues); + if (stateInParentScope != null) + { // Only refresh the entity in the parent DbContext from the database if that entity hasn't already been // modified in the parent. Otherwise, let the whatever concurency rules the application uses // apply. - if (stateInParentScope.EntityState == EntityState.Unchanged) { + if (stateInParentScope.EntityState == EntityState.Unchanged) + { correspondingParentContext.Entry(stateInParentScope.Entity).Reload(); } } @@ -175,7 +217,8 @@ public void RefreshEntitiesInParentScope(IEnumerable entities) { } } - public async Task RefreshEntitiesInParentScopeAsync(IEnumerable entities) { + public async Task RefreshEntitiesInParentScopeAsync(IEnumerable entities) + { // See comments in the sync version of this method for an explanation of what we're doing here. if (entities == null) @@ -187,27 +230,35 @@ public async Task RefreshEntitiesInParentScopeAsync(IEnumerable entities) { if (_nested) return; - foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) { + foreach (var contextInCurrentScope in _dbContexts.InitializedDbContexts.Values) + { var correspondingParentContext = - _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => parentContext.GetType() == contextInCurrentScope.GetType()) - as DbContext; + _parentScope._dbContexts.InitializedDbContexts.Values.SingleOrDefault(parentContext => + parentContext.GetType() == contextInCurrentScope.GetType()) + as DbContext; if (correspondingParentContext == null) continue; - foreach (var toRefresh in entities) { - var stateInCurrentScope = contextInCurrentScope.ChangeTracker.GetInfrastructure().TryGetEntry(toRefresh); - if (stateInCurrentScope != null) { + foreach (var toRefresh in entities) + { + var stateInCurrentScope = + contextInCurrentScope.GetStateManager().TryGetEntry(toRefresh); + if (stateInCurrentScope != null) + { var entityType = stateInCurrentScope.Entity.GetType(); var key = stateInCurrentScope.EntityType.FindPrimaryKey(); var keyValues = key.Properties .Select(s => entityType.GetProperty(s.Name).GetValue(stateInCurrentScope.Entity)) .ToArray(); - - var stateInParentScope = correspondingParentContext.ChangeTracker.GetInfrastructure().TryGetEntry(key, keyValues); - if (stateInParentScope != null) { - if (stateInParentScope.EntityState == EntityState.Unchanged) { - await correspondingParentContext.Entry(stateInParentScope.Entity).ReloadAsync().ConfigureAwait(false); + var stateInParentScope = correspondingParentContext.GetStateManager() + .TryGetEntry(key, keyValues); + if (stateInParentScope != null) + { + if (stateInParentScope.EntityState == EntityState.Unchanged) + { + await correspondingParentContext.Entry(stateInParentScope.Entity).ReloadAsync() + .ConfigureAwait(false); } } } @@ -215,25 +266,34 @@ public async Task RefreshEntitiesInParentScopeAsync(IEnumerable entities) { } } - public void Dispose() { + public void Dispose() + { if (_disposed) return; // Commit / Rollback and dispose all of our DbContext instances - if (!_nested) { - if (!_completed) { + if (!_nested) + { + if (!_completed) + { // Do our best to clean up as much as we can but don't throw here as it's too late anyway. - try { - if (_readOnly) { + try + { + if (_readOnly) + { // Disposing a read-only scope before having called its SaveChanges() method // is the normal and expected behavior. Read-only scopes get committed automatically. CommitInternal(); - } else { + } + else + { // Disposing a read/write scope before having called its SaveChanges() method // indicates that something went wrong and that all changes should be rolled-back. RollbackInternal(); } - } catch (Exception e) { + } + catch (Exception e) + { System.Diagnostics.Debug.WriteLine(e); } @@ -246,12 +306,15 @@ public void Dispose() { // Pop ourself from the ambient scope stack var currentAmbientScope = GetAmbientScope(); if (currentAmbientScope != this) // This is a serious programming error. Worth throwing here. - throw new InvalidOperationException("DbContextScope instances must be disposed of in the order in which they were created!"); + throw new InvalidOperationException( + "DbContextScope instances must be disposed of in the order in which they were created!"); RemoveAmbientScope(); - if (_parentScope != null) { - if (_parentScope._disposed) { + if (_parentScope != null) + { + if (_parentScope._disposed) + { /* * If our parent scope has been disposed before us, it can only mean one thing: * someone started a parallel flow of execution and forgot to suppress the @@ -287,7 +350,8 @@ public void Dispose() { * So just record a warning here. Hopefully someone will see it and will fix the code. */ - 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. + 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. In order to fix this: 1) Look at the stack trace below - this is the stack trace of the parallel task in question. @@ -298,13 +362,14 @@ public void Dispose() { " + Environment.StackTrace; System.Diagnostics.Debug.WriteLine(message); - } else { + } + else + { SetAmbientScope(_parentScope); } } _disposed = true; - } #region Ambient Context Logic @@ -388,25 +453,27 @@ public void Dispose() { // to the DbContextScope instances we store in there, allowing them to get GCed. // The doc for ConditionalWeakTable isn't the best. This SO anser does a good job at explaining what // it does: http://stackoverflow.com/a/18613811 - private static readonly ConditionalWeakTable DbContextScopeInstances = new ConditionalWeakTable(); + private static readonly ConditionalWeakTable DbContextScopeInstances = + new ConditionalWeakTable(); private InstanceIdentifier _instanceIdentifier = new InstanceIdentifier(); /// /// Makes the provided 'dbContextScope' available as the the ambient scope via the CallContext. /// - internal static void SetAmbientScope(DbContextScope newAmbientScope) { + internal static void SetAmbientScope(DbContextScope newAmbientScope) + { if (newAmbientScope == null) throw new ArgumentNullException("newAmbientScope"); //return Thread.CurrentThread.GetExecutionContextReader().LogicalCallContext.GetData(name); - var current = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; + var current = CallContext.GetData(AmbientDbContextScopeKey) as InstanceIdentifier; if (current == newAmbientScope._instanceIdentifier) return; // Store the new scope's instance identifier in the CallContext, making it the ambient scope - CallContext.LogicalSetData(AmbientDbContextScopeKey, newAmbientScope._instanceIdentifier); + CallContext.SetData(AmbientDbContextScopeKey, newAmbientScope._instanceIdentifier); // Keep track of this instance (or do nothing if we're already tracking it) DbContextScopeInstances.GetValue(newAmbientScope._instanceIdentifier, key => newAmbientScope); @@ -416,12 +483,14 @@ internal static void SetAmbientScope(DbContextScope newAmbientScope) { /// Clears the ambient scope from the CallContext and stops tracking its instance. /// Call this when a DbContextScope is being disposed. /// - internal static void RemoveAmbientScope() { - var current = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; - CallContext.LogicalSetData(AmbientDbContextScopeKey, null); + internal static void RemoveAmbientScope() + { + var current = CallContext.GetData(AmbientDbContextScopeKey) as InstanceIdentifier; + CallContext.SetData(AmbientDbContextScopeKey, null); // If there was an ambient scope, we can stop tracking it now - if (current != null) { + if (current != null) + { DbContextScopeInstances.Remove(current); } } @@ -430,18 +499,21 @@ internal static void RemoveAmbientScope() { /// Clears the ambient scope from the CallContext but keeps tracking its instance. Call this to temporarily /// hide the ambient context (e.g. to prevent it from being captured by parallel task). /// - internal static void HideAmbientScope() { - CallContext.LogicalSetData(AmbientDbContextScopeKey, null); + internal static void HideAmbientScope() + { + CallContext.SetData(AmbientDbContextScopeKey, null); } /// /// Get the current ambient scope or null if no ambient scope has been setup. /// - internal static DbContextScope GetAmbientScope() { + internal static DbContextScope GetAmbientScope() + { // Retrieve the identifier of the ambient scope (if any) - var instanceIdentifier = CallContext.LogicalGetData(AmbientDbContextScopeKey) as InstanceIdentifier; + var instanceIdentifier = CallContext.GetData(AmbientDbContextScopeKey) as InstanceIdentifier; if (instanceIdentifier == null) - return null; // Either no ambient context has been set or we've crossed an app domain boundary and have (intentionally) lost the ambient context + return + null; // Either no ambient context has been set or we've crossed an app domain boundary and have (intentionally) lost the ambient context // Retrieve the DbContextScope instance corresponding to this identifier if (DbContextScopeInstances.TryGetValue(instanceIdentifier, out DbContextScope ambientScope)) @@ -460,7 +532,8 @@ internal static DbContextScope GetAmbientScope() { // the GC would be able to collect it. Once collected by the GC, our ConditionalWeakTable will return // null when queried for that instance. In that case, we're OK. This is a programming error // but our use of a ConditionalWeakTable prevented a leak. - 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."); + 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."); return null; } @@ -476,6 +549,7 @@ internal static DbContextScope GetAmbientScope() { * an empty class is cheaper and uses up less memory than generating * a unique string. */ - internal class InstanceIdentifier : MarshalByRefObject { } -} - + internal class InstanceIdentifier : MarshalByRefObject + { + } +} \ No newline at end of file diff --git a/DbContextScope/Implementations/DbContextScopeFactory.cs b/src/DbContextScope/Implementations/DbContextScopeFactory.cs similarity index 100% rename from DbContextScope/Implementations/DbContextScopeFactory.cs rename to src/DbContextScope/Implementations/DbContextScopeFactory.cs diff --git a/DbContextScope/Interfaces/IAmbientDbContextLocator.cs b/src/DbContextScope/Interfaces/IAmbientDbContextLocator.cs similarity index 100% rename from DbContextScope/Interfaces/IAmbientDbContextLocator.cs rename to src/DbContextScope/Interfaces/IAmbientDbContextLocator.cs diff --git a/DbContextScope/Interfaces/IDbContextCollection.cs b/src/DbContextScope/Interfaces/IDbContextCollection.cs similarity index 100% rename from DbContextScope/Interfaces/IDbContextCollection.cs rename to src/DbContextScope/Interfaces/IDbContextCollection.cs diff --git a/DbContextScope/Interfaces/IDbContextFactory.cs b/src/DbContextScope/Interfaces/IDbContextFactory.cs similarity index 100% rename from DbContextScope/Interfaces/IDbContextFactory.cs rename to src/DbContextScope/Interfaces/IDbContextFactory.cs diff --git a/DbContextScope/Interfaces/IDbContextReadOnlyScope.cs b/src/DbContextScope/Interfaces/IDbContextReadOnlyScope.cs similarity index 100% rename from DbContextScope/Interfaces/IDbContextReadOnlyScope.cs rename to src/DbContextScope/Interfaces/IDbContextReadOnlyScope.cs diff --git a/DbContextScope/Interfaces/IDbContextScope.cs b/src/DbContextScope/Interfaces/IDbContextScope.cs similarity index 100% rename from DbContextScope/Interfaces/IDbContextScope.cs rename to src/DbContextScope/Interfaces/IDbContextScope.cs diff --git a/DbContextScope/Interfaces/IDbContextScopeFactory.cs b/src/DbContextScope/Interfaces/IDbContextScopeFactory.cs similarity index 100% rename from DbContextScope/Interfaces/IDbContextScopeFactory.cs rename to src/DbContextScope/Interfaces/IDbContextScopeFactory.cs