├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json ├── settings.json ├── tasks.json └── tasks.json.old ├── DataLayer ├── BookApp │ ├── Author.cs │ ├── Book.cs │ ├── BookAuthor.cs │ ├── EfCode │ │ ├── BookContext.cs │ │ ├── BookOrderContext.cs │ │ ├── BookOrderSchemaContext.cs │ │ ├── Configurations │ │ │ ├── BookAuthorConfig.cs │ │ │ ├── BookConfig.cs │ │ │ ├── LineItemConfig.cs │ │ │ └── PriceOfferConfig.cs │ │ └── OrderContext.cs │ ├── LineItem.cs │ ├── Order.cs │ ├── PriceOffer.cs │ └── Review.cs ├── DataLayer.csproj ├── Database1 │ ├── DbContext1.cs │ ├── Dependent1.cs │ └── TopClass1.cs ├── Database2 │ ├── DbContext2.cs │ ├── Dependent2.cs │ └── TopClass2.cs ├── DddBookApp │ ├── DddAuthor.cs │ ├── DddBook.cs │ ├── DddBookAuthor.cs │ ├── DddReview.cs │ └── EfCode │ │ ├── Configurations │ │ ├── BookAuthorConfig.cs │ │ └── BookConfig.cs │ │ └── DddBookContext.cs ├── DecodedParts │ ├── AllTypesDbContext.cs │ └── AllTypesEntity.cs ├── MutipleSchema │ ├── Class1.cs │ ├── Class2.cs │ ├── Class3.cs │ ├── Class4.cs │ └── ManySchemaDbContext.cs ├── SpecialisedEntities │ ├── Address.cs │ ├── AllTypesEntity.cs │ ├── BookDetail.cs │ ├── BookSummary.cs │ ├── Configurations │ │ ├── BookDetailConfig.cs │ │ ├── BookSummaryConfig.cs │ │ ├── OrderInfoConfig.cs │ │ ├── PaymentConfig.cs │ │ └── UserConfig.cs │ ├── MyEntityReadOnly.cs │ ├── OrderInfo.cs │ ├── OwnedWithKeyDbContext.cs │ ├── Payment.cs │ ├── PaymentCard.cs │ ├── PaymentCash.cs │ ├── SpecializedDbContext.cs │ └── User.cs └── appsettings.json ├── EfCoreInAction.Test.sln ├── EfCoreInAction.Test.sln.DotSettings ├── LICENSE ├── README.md ├── ReleaseNotes.md ├── SeedFromProductionOverview.png ├── Test ├── AltTestDataDir │ └── Alt dummy file.txt ├── Helpers │ ├── DddEfTestData.cs │ └── EfTestData.cs ├── Test.csproj ├── TestData │ ├── AddUserDefinedFunctions.sql │ ├── AlterMyClassesToNotHaveAPrimaryKey.sql │ ├── DbContextCompareLog01 - MyEntity default.json │ ├── Dummy file.txt │ ├── Index01 - Create MyEntites with unique constraint.sql │ ├── Logging01 - example logged, no values.txt │ ├── Logging02 - funny name param, no values.txt │ ├── Logging03 - sensitive data with odd string.txt │ ├── Script01 - Add row to Authors table.sql │ ├── Script02 - Add two rows to Authors table.sql │ ├── SeedData-DddExampleDatabase.json │ ├── SeedData-DddExampleDatabaseAnonymised.json │ ├── SeedData-ExampleDatabase.json │ ├── SeedData-ExampleDatabaseAnonymised.json │ ├── SubDirWithOneFileInIt │ │ └── One file.txt │ └── differentAppSettings.json ├── UnitCommands │ └── DeleteAllUnitTestDatabases.cs ├── UnitTests │ ├── TestDataLayer │ │ ├── ExampleTest.cs │ │ ├── TestApplyScriptExtension.cs │ │ ├── TestDisconnectedState.cs │ │ ├── TestMyLoggerProviderActionOut.cs │ │ ├── TestOptionsWithLogTo.cs │ │ ├── TestPostgreSqlHelpers.cs │ │ ├── TestSqlServerHelpers.cs │ │ └── TestSqliteInMemory.cs │ ├── TestDataResetter │ │ ├── DddExampleSetupAndSeed.cs │ │ ├── ExampleSetupAndSeed.cs │ │ ├── TestDuplicateObjectInJsonSerialize.cs │ │ ├── TestResetKeysEntityAndRelationships.cs │ │ ├── TestResetKeysSingleEntity.cs │ │ └── TestResetKeysSingleEntityAnonymise.cs │ └── TestSupport │ │ ├── TestAppSettings.cs │ │ ├── TestFileData.cs │ │ ├── TestTimeThings.cs │ │ └── UnitTest1.cs └── appsettings.json ├── TestFromSqlRaw ├── DbContext1.cs ├── MyEntity.cs ├── Program.cs └── TestFromSqlRaw.csproj ├── TestSupport ├── Assert.Extensions │ ├── BooleanAssertionExtensions.cs │ ├── CollectionAssertionExtensions.cs │ ├── ExtraStringAssertionExtensions.cs │ ├── ObjectAssertExtensions.cs │ └── StringAssertionExtensions.cs ├── Attributes │ └── RunnableInDebugOnlyAttribute .cs ├── EfCoreTestSupportNuGetIcon128.png ├── EfHelpers │ ├── ApplyScriptExtension.cs │ ├── CleanDatabaseExtensions.cs │ ├── CosmosDbExtensions.cs │ ├── DatabaseTidyHelper.cs │ ├── DbContextOptionsDisposable.cs │ ├── Internal │ │ ├── EfCoreLogDecoder.cs │ │ └── OptionBuilderHelpers.cs │ ├── LogOutput.cs │ ├── LogToOptions.cs │ ├── MyLoggerProviderActionOut.cs │ ├── PostgreSqlHelpers.cs │ ├── SqlAdoNetHelpers.cs │ ├── SqlServerHelpers.cs │ ├── SqliteInMemory.cs │ ├── TimeThingResult.cs │ └── TimeThings.cs ├── Helpers │ ├── AppSettings.cs │ └── TestData.cs ├── SeedDatabase │ ├── AnonymiserData.cs │ ├── DataResetter.cs │ ├── DataResetterConfig.cs │ ├── Internal │ │ └── MemberAnonymiseData.cs │ ├── SeedJsonHelpers.cs │ └── SqlServerProductionSetup.cs ├── TestSupport.csproj └── TestSupport.xml ├── UnitTestExample.png └── Version5UpgradeDocs.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Debug xunit tests", 10 | "type": "coreclr", 11 | "request": "launch", 12 | "preLaunchTask": "build", 13 | // If you have changed target frameworks, make sure to update the program path. 14 | "program": "${workspaceRoot}/Test/bin/Debug/netcoreapp2.0/Test.dll", 15 | "args": [], 16 | "cwd": "${workspaceRoot}/Test", 17 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 18 | "console": "internalConsole", 19 | "stopAtEntry": false, 20 | "internalConsoleOptions": "openOnSessionStart" 21 | }, 22 | { 23 | "name": ".NET Core Attach", 24 | "type": "coreclr", 25 | "request": "attach", 26 | "processId": "${command:pickProcess}" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "Test" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "dotnet", 4 | "args": [], 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "dotnet", 10 | "args": [ 11 | "build", 12 | "${workspaceRoot}/Test/Test.csproj" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "_id": "build", 17 | "isDefault": false 18 | } 19 | }, 20 | { 21 | "label": "test", 22 | "type": "shell", 23 | "command": "dotnet", 24 | "args": [ 25 | "test" 26 | ], 27 | "problemMatcher": "$msCompile", 28 | "group": { 29 | "_id": "test", 30 | "isDefault": false 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscode/tasks.json.old: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "${workspaceRoot}/Test/Test.csproj" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | }, 15 | { 16 | "taskName": "test", 17 | "args": [ ], 18 | "isTestCommand": true, 19 | "showOutput": "always", 20 | "problemMatcher": "$msCompile" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/Author.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace DataLayer.BookApp 8 | { 9 | public class Author 10 | { 11 | public const int NameLength = 100; 12 | 13 | public int AuthorId { get; set; } 14 | 15 | [Required] 16 | [MaxLength(NameLength)] 17 | public string Name { get; set; } 18 | 19 | //------------------------------ 20 | //Relationships 21 | 22 | public ICollection 23 | BooksLink { get; set; } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/Book.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | 8 | namespace DataLayer.BookApp 9 | { 10 | public class Book 11 | { 12 | public int BookId { get; set; } 13 | 14 | [Required] //#A 15 | [MaxLength(256)] //#B 16 | public string Title { get; set; } 17 | 18 | public string Description { get; set; } 19 | public DateTime PublishedOn { get; set; } 20 | 21 | [MaxLength(64)] //#B 22 | public string Publisher { get; set; } 23 | 24 | public decimal Price { get; set; } 25 | 26 | [MaxLength(512)] //#B 27 | public string ImageUrl { get; set; } 28 | 29 | public bool SoftDeleted { get; set; } 30 | 31 | //----------------------------------------------- 32 | //relationships 33 | 34 | public PriceOffer Promotion { get; set; } 35 | public ICollection Reviews { get; set; } 36 | 37 | public ICollection 38 | AuthorsLink { get; set; } 39 | } 40 | /**************************************************** 41 | #A This tells EF Core that the string is non-nullable. 42 | #B The [MaxLength] attibute defines the the size of the string column in the database 43 | * **************************************************/ 44 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/BookAuthor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.BookApp 5 | { 6 | public class BookAuthor 7 | { 8 | public int BookId { get; set; } //#A 9 | public int AuthorId { get; set; }//#A 10 | public byte Order { get; set; } 11 | 12 | //----------------------------- 13 | //Relationships 14 | 15 | public Book Book { get; set; } 16 | public Author Author { get; set; } 17 | } 18 | /************************************************************ 19 | A# The primary key is make up of the two foreign keys 20 | * ********************************************************/ 21 | 22 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/BookContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.BookApp.EfCode.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.BookApp.EfCode 8 | { 9 | public class BookContext : DbContext 10 | { 11 | public BookContext( 12 | DbContextOptions options) 13 | : base(options) {} 14 | 15 | public DbSet Books { get; set; } 16 | public DbSet Authors { get; set; } 17 | public DbSet PriceOffers { get; set; } 18 | 19 | protected override void 20 | OnModelCreating(ModelBuilder modelBuilder) 21 | { 22 | modelBuilder.ApplyConfiguration(new BookConfig()); 23 | modelBuilder.ApplyConfiguration(new BookAuthorConfig()); 24 | modelBuilder.ApplyConfiguration(new PriceOfferConfig()); 25 | } 26 | } 27 | 28 | /****************************************************************************** 29 | * NOTES ON MIGRATION: 30 | * 31 | * see https://docs.microsoft.com/en-us/aspnet/core/data/ef-rp/migrations?tabs=visual-studio 32 | * 33 | * The following NuGet libraries must be loaded 34 | * 1. Add to DataLayer: "Microsoft.EntityFrameworkCore.Tools" 35 | * 2. Add to DataLayer: "Microsoft.EntityFrameworkCore.SqlServer" (or another database provider) 36 | * 37 | * 2. Using Package Manager Console commands 38 | * The steps are: 39 | * a) Make sure the default project is Test 40 | * b) Use the PMC command 41 | * Add-Migration Initial -Project DataLayer -Context BookContext -OutputDir BookApp\Migrations 42 | * 43 | * If you want to start afresh then: 44 | * a) Delete the current database 45 | * b) Delete all the class in the Migration directory 46 | * c) follow the steps to add a migration 47 | ******************************************************************************/ 48 | } 49 | 50 | -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/BookOrderContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.BookApp.EfCode.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.BookApp.EfCode 8 | { 9 | public class BookOrderContext : DbContext 10 | { 11 | public BookOrderContext( 12 | DbContextOptions options) 13 | : base(options) {} 14 | 15 | public DbSet Books { get; set; } 16 | public DbSet Authors { get; set; } 17 | public DbSet PriceOffers { get; set; } 18 | public DbSet Orders { get; set; } 19 | 20 | protected override void 21 | OnModelCreating(ModelBuilder modelBuilder) 22 | { 23 | modelBuilder.ApplyConfiguration(new BookConfig()); 24 | modelBuilder.ApplyConfiguration(new BookAuthorConfig()); 25 | modelBuilder.ApplyConfiguration(new PriceOfferConfig()); 26 | modelBuilder.ApplyConfiguration(new LineItemConfig()); 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/BookOrderSchemaContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.BookApp.EfCode.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.BookApp.EfCode 8 | { 9 | public class BookOrderSchemaContext : DbContext 10 | { 11 | public BookOrderSchemaContext( 12 | DbContextOptions options) 13 | : base(options) {} 14 | 15 | public DbSet Books { get; set; } 16 | public DbSet Authors { get; set; } 17 | public DbSet PriceOffers { get; set; } 18 | public DbSet Orders { get; set; } 19 | 20 | protected override void 21 | OnModelCreating(ModelBuilder modelBuilder) 22 | { 23 | modelBuilder.ApplyConfiguration(new BookConfig()); 24 | modelBuilder.ApplyConfiguration(new BookAuthorConfig()); 25 | modelBuilder.ApplyConfiguration(new PriceOfferConfig()); 26 | modelBuilder.ApplyConfiguration(new LineItemConfig()); 27 | 28 | modelBuilder.Entity().ToTable("DupTable", "BookSchema"); 29 | modelBuilder.Entity().ToTable("DupTable", "OrderSchema"); 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/Configurations/BookAuthorConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.BookApp.EfCode.Configurations 8 | { 9 | public class BookAuthorConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.HasKey(p => 15 | new { p.BookId, p.AuthorId }); 16 | 17 | //----------------------------- 18 | //Relationships 19 | 20 | entity.HasOne(pt => pt.Book) 21 | .WithMany(p => p.AuthorsLink) 22 | .HasForeignKey(pt => pt.BookId); 23 | 24 | entity.HasOne(pt => pt.Author) 25 | .WithMany(t => t.BooksLink) 26 | .HasForeignKey(pt => pt.AuthorId); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/Configurations/BookConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.BookApp.EfCode.Configurations 8 | { 9 | public class BookConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.Property(p => p.PublishedOn)//#A 15 | .HasColumnType("date"); 16 | 17 | entity.Property(p => p.Price) //#B 18 | .HasColumnType("decimal(9,2)"); 19 | 20 | entity.Property(x => x.ImageUrl) //#C 21 | .IsUnicode(false); 22 | 23 | entity.HasIndex(x => x.PublishedOn); //#D 24 | 25 | //Model-level query filter 26 | 27 | entity 28 | .HasQueryFilter(p => !p.SoftDeleted); //#E 29 | 30 | //---------------------------- 31 | //relationships 32 | 33 | entity.HasOne(p => p.Promotion) //#A 34 | .WithOne() //#A 35 | .HasForeignKey(p => p.BookId); //#A 36 | 37 | entity.HasMany(p => p.Reviews) //#B 38 | .WithOne() //#B 39 | .HasForeignKey(p => p.BookId); //#B 40 | } 41 | } 42 | /*Type/Size setting********************************************** 43 | #A The convention-based mapping for .NET DateTime is SQL datetime2. This command changes the SQL column type to date, which only holds the date, not time 44 | #B I set a smaller precision and scale of (9,2) for the price instead of the default (18,2) 45 | #C The convention-based mapping for .NET string is SQL nvarchar (16 bit Unicode). This command changes the SQL column type to varchar (8 bit ASCII) 46 | #D I add an index to the PublishedOn property because I sort and filter on this property 47 | #E This sets a model-level query filter on the Book entity. By default, a query will exclude Book entites where th SoftDeleted property is true 48 | * * ******************************************************/ 49 | /*CH07******************************************************** 50 | #A This defines the One-to-One relationship to the promotion that a book can optionally have. The foreign key is in the PriceOffer 51 | #B This defines the One-to-Many relationship, with a book having zero to many reviews 52 | * ***********************************************************/ 53 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/Configurations/LineItemConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.BookApp.EfCode.Configurations 8 | { 9 | public class LineItemConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.HasOne(p => p.ChosenBook) 15 | .WithMany() 16 | .OnDelete(DeleteBehavior.Restrict); //#A 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/Configurations/PriceOfferConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.BookApp.EfCode.Configurations 8 | { 9 | public class PriceOfferConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.Property(p => p.NewPrice) 15 | .HasColumnType("decimal(9,2)"); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/EfCode/OrderContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using DataLayer.BookApp.EfCode.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.BookApp.EfCode 8 | { 9 | public class OrderContext : DbContext 10 | { 11 | public OrderContext( 12 | DbContextOptions options) 13 | : base(options) { } 14 | 15 | public DbSet Books { get; set; } //#A 16 | public DbSet Orders { get; set; } 17 | 18 | protected override void 19 | OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | modelBuilder.ApplyConfiguration(new BookConfig()); 22 | modelBuilder.ApplyConfiguration( new LineItemConfig()); 23 | 24 | modelBuilder.Ignore(); 25 | modelBuilder.Ignore(); 26 | modelBuilder.Ignore(); 27 | modelBuilder.Ignore(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/LineItem.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace DataLayer.BookApp 9 | { 10 | public class LineItem : IValidatableObject //#A 11 | { 12 | public int LineItemId { get; set; } 13 | 14 | [Range(1,5, ErrorMessage = //#B 15 | "This order is over the limit of 5 books.")] //#B 16 | public byte LineNum { get; set; } 17 | 18 | public short NumBooks { get; set; } 19 | 20 | /// 21 | /// This holds a copy of the book price. We do this in case the price of the book changes, 22 | /// e.g. if the price was discounted in the future the order is still correct. 23 | /// 24 | public decimal BookPrice { get; set; } 25 | 26 | // relationships 27 | 28 | public int OrderId { get; set; } 29 | public int BookId { get; set; } 30 | 31 | public Book ChosenBook { get; set; } 32 | 33 | IEnumerable IValidatableObject.Validate //#C 34 | (ValidationContext validationContext) //#C 35 | { 36 | var currContext = 37 | validationContext.GetService(typeof(DbContext));//#D 38 | 39 | if (ChosenBook.Price < 0) //#E 40 | yield return new ValidationResult( //#E 41 | $"Sorry, the book '{ChosenBook.Title}' is not for sale."); //#E 42 | 43 | if (NumBooks > 100) 44 | yield return new ValidationResult(//#F 45 | "If you want to order a 100 or more books"+ //#F 46 | " please phone us on 01234-5678-90", //#F 47 | new[] { nameof(NumBooks) }); //#F 48 | } 49 | } 50 | /********************************************************** 51 | #A By applying the IValidatableObject interface then the validation will call the method the interface defines 52 | #B This is one of the validation DataAnnotations. The validator will show my error message if the LineNum property is not in range 53 | #C This is the method that the IValidatableObject interface requires me to create 54 | #D I can access the current DbContext that this database access is using. In this case I don't use it, but you could use it to get better error feedback information for the user 55 | #D Here I use the ChosenBook link to look at the date the book was published. I can also format my own error message, which is helpful 56 | #E This moves the Price check out of the business logic 57 | #F This tests a property in this class so I can return that property with the error. 58 | * *******************************************************/ 59 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/Order.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace DataLayer.BookApp 8 | { 9 | public class Order 10 | { 11 | public Order() 12 | { 13 | DateOrderedUtc = DateTime.UtcNow; 14 | } 15 | 16 | public int OrderId { get; set; } 17 | 18 | public DateTime DateOrderedUtc { get; set; } 19 | 20 | /// 21 | /// In this simple example the cookie holds a GUID for everyone that 22 | /// 23 | public Guid CustomerName { get; set; } 24 | 25 | // relationships 26 | 27 | public ICollection LineItems { get; set; } 28 | 29 | // Extra columns not used by EF 30 | 31 | public string OrderNumber => $"SO{OrderId:D6}"; 32 | } 33 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/PriceOffer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace DataLayer.BookApp 7 | { 8 | public class PriceOffer 9 | { 10 | public const int PromotionalTextLength = 200; 11 | 12 | public int PriceOfferId { get; set; } 13 | public decimal NewPrice { get; set; } 14 | 15 | [Required] 16 | [MaxLength(PromotionalTextLength)] 17 | public string PromotionalText { get; set; } 18 | 19 | //----------------------------------------------- 20 | //Relationships 21 | 22 | public int BookId { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /DataLayer/BookApp/Review.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace DataLayer.BookApp 7 | { 8 | public class Review 9 | { 10 | public const int NameLength = 100; 11 | 12 | public int ReviewId { get; set; } 13 | 14 | [MaxLength(NameLength)] 15 | public string VoterName { get; set; } 16 | 17 | public int NumStars { get; set; } 18 | public string Comment { get; set; } 19 | 20 | //----------------------------------------- 21 | //Relationships 22 | 23 | public int BookId { get; set; } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /DataLayer/DataLayer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /DataLayer/Database1/DbContext1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DataLayer.Database1 7 | { 8 | public class DbContext1 : DbContext 9 | { 10 | public DbContext1(DbContextOptions options) 11 | : base(options) { } 12 | 13 | public DbSet TopClasses { get; set; } 14 | public DbSet Dependents { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/Database1/Dependent1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace DataLayer.Database1 7 | { 8 | public class Dependent1 9 | { 10 | public int Id { get; set; } 11 | 12 | public Guid MyGuid { get; set; } 13 | 14 | public int TopClass1Id { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/Database1/TopClass1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace DataLayer.Database1 8 | { 9 | public class TopClass1 10 | { 11 | public int Id { get; set; } 12 | 13 | public Guid MyGuid { get; set; } 14 | 15 | public ICollection Dependents { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /DataLayer/Database2/DbContext2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DataLayer.Database2 7 | { 8 | public class DbContext2 : DbContext 9 | { 10 | public DbContext2(DbContextOptions options) 11 | : base(options) { } 12 | 13 | public DbSet TopClasses { get; set; } 14 | public DbSet Dependents { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/Database2/Dependent2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.Database2 5 | { 6 | public class Dependent2 7 | { 8 | public int Id { get; set; } 9 | 10 | public string MyString { get; set; } 11 | 12 | public int TopClass2Id { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /DataLayer/Database2/TopClass2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace DataLayer.Database2 7 | { 8 | public class TopClass2 9 | { 10 | public int Id { get; set; } 11 | 12 | public string MyString { get; set; } 13 | 14 | public ICollection Dependents { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/DddAuthor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace DataLayer.DddBookApp 8 | { 9 | //I have styled the Author entity class as a standard-styled entity, 10 | //i.e. it can be created/updated via its property setters. 11 | //Technically it has to have a public, parameterless constructor and all properties should have public setters 12 | public class DddAuthor 13 | { 14 | public const int NameLength = 100; 15 | public const int EmailLength = 100; 16 | 17 | public DddAuthor() { } 18 | 19 | [Key] 20 | public int AuthorId { get; set; } 21 | 22 | [Required(AllowEmptyStrings = false)] 23 | [MaxLength(NameLength)] 24 | public string Name { get; set; } 25 | 26 | [MaxLength(EmailLength)] 27 | public string Email { get; set; } 28 | 29 | //------------------------------ 30 | //Relationships 31 | 32 | public ICollection BooksLink { get; set; } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/DddBookAuthor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Newtonsoft.Json; 5 | 6 | namespace DataLayer.DddBookApp 7 | { 8 | public class DddBookAuthor 9 | { 10 | private DddBookAuthor() { } 11 | 12 | internal DddBookAuthor(DddBook dddBook, DddAuthor dddAuthor, byte order) 13 | { 14 | DddBook = dddBook; 15 | DddAuthor = dddAuthor; 16 | Order = order; 17 | } 18 | 19 | public int BookId { get; private set; } 20 | public int AuthorId { get; private set; } 21 | public byte Order { get; private set; } 22 | 23 | //----------------------------- 24 | //Relationships 25 | 26 | public DddBook DddBook { get; private set; } 27 | public DddAuthor DddAuthor { get; private set; } 28 | } 29 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/DddReview.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace DataLayer.DddBookApp 7 | { 8 | public class DddReview 9 | { 10 | public const int NameLength = 100; 11 | 12 | private DddReview() { } 13 | 14 | internal DddReview(int numStars, string comment, string voterName, int bookId = 0) 15 | { 16 | NumStars = numStars; 17 | Comment = comment; 18 | VoterName = voterName; 19 | BookId = bookId; 20 | } 21 | 22 | [Key] 23 | public int ReviewId { get; private set; } 24 | 25 | [MaxLength(NameLength)] 26 | public string VoterName { get; private set; } 27 | 28 | public int NumStars { get; private set; } 29 | public string Comment { get; private set; } 30 | 31 | //----------------------------------------- 32 | //Relationships 33 | 34 | public int BookId { get; private set; } 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/EfCode/Configurations/BookAuthorConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.DddBookApp.EfCode.Configurations 8 | { 9 | public class BookAuthorConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.HasKey(p => new { p.BookId, p.AuthorId }); 15 | 16 | //----------------------------- 17 | //Relationships 18 | 19 | entity.HasOne(pt => pt.DddBook) 20 | .WithMany(p => p.AuthorsLink) 21 | .HasForeignKey(pt => pt.BookId); 22 | 23 | entity.HasOne(pt => pt.DddAuthor) 24 | .WithMany(t => t.BooksLink) 25 | .HasForeignKey(pt => pt.AuthorId); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/EfCode/Configurations/BookConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.DddBookApp.EfCode.Configurations 8 | { 9 | public class BookConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.Property(p => p.PublishedOn).HasColumnType("date"); 15 | 16 | entity.Property(p => p.ActualPrice) 17 | .HasColumnType("decimal(9,2)"); 18 | 19 | entity.Property(x => x.ImageUrl) 20 | .IsUnicode(false); 21 | 22 | entity.HasIndex(x => x.PublishedOn); 23 | entity.HasIndex(x => x.ActualPrice); 24 | 25 | //---------------------------- 26 | //relationships 27 | 28 | entity.HasMany(p => p.Reviews) 29 | .WithOne() 30 | .HasForeignKey(p => p.BookId); 31 | 32 | //entity.Metadata 33 | // .FindNavigation(nameof(DddBook.Reviews)) 34 | // .SetPropertyAccessMode(PropertyAccessMode.Field); 35 | 36 | //entity.Metadata 37 | // .FindNavigation(nameof(DddBook.AuthorsLink)) 38 | // .SetPropertyAccessMode(PropertyAccessMode.Field); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /DataLayer/DddBookApp/EfCode/DddBookContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using DataLayer.DddBookApp.EfCode.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.DddBookApp.EfCode 8 | { 9 | public class DddBookContext : DbContext 10 | { 11 | public DddBookContext( 12 | DbContextOptions options) 13 | : base(options) {} 14 | 15 | public DbSet DddBooks { get; set; } 16 | public DbSet DddAuthors { get; set; } 17 | 18 | protected override void 19 | OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | modelBuilder.ApplyConfiguration(new BookConfig()); 22 | modelBuilder.ApplyConfiguration(new BookAuthorConfig()); 23 | } 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /DataLayer/DecodedParts/AllTypesDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace DataLayer.DecodedParts 7 | { 8 | public class AllTypesDbContext : DbContext 9 | { 10 | public AllTypesDbContext(DbContextOptions options) 11 | : base(options) {} 12 | 13 | public DbSet AllTypesEntities { get; set; } 14 | 15 | protected override void OnModelCreating 16 | (ModelBuilder modelBuilder) 17 | { 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /DataLayer/DecodedParts/AllTypesEntity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace DataLayer.DecodedParts 9 | { 10 | public class AllTypesEntity 11 | { 12 | public int Id { get; private set; } 13 | public bool MyBool { get; private set; } = true; 14 | public bool? MyBoolNullable { get; private set; } = null; 15 | public int MyInt { get; private set; } = 1234; 16 | public int? MyIntNullable { get; private set; } = null; 17 | public double MyDouble { get; private set; } = 5678.9012; 18 | public decimal MyDecimal { get; private set; } = 3456.789m; 19 | public Guid MyGuid { get; set; } 20 | public Guid? MyGuidNullable { get; private set; } = null; 21 | 22 | public string MyString { get; private set; } = "string with ' in it"; 23 | public string MyStringNull { get; private set; } = null; 24 | public string MyStringEmptyString { get; private set; } = string.Empty; 25 | 26 | [Required] 27 | [Column(TypeName = "varchar(123)")] 28 | public string MyAnsiNonNullString { get; private set; } = "ascii only"; 29 | 30 | public DateTime MyDateTime { get; set; } 31 | public DateTime? MyDateTimeNullable { get; private set; } = null; 32 | public TimeSpan MyTimeSpan { get; set; } 33 | public DateTimeOffset MyDateTimeOffset { get; set; } 34 | public byte[] MyByteArray { get; set; } 35 | 36 | public void SetId(int id) 37 | { 38 | Id = id; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /DataLayer/MutipleSchema/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DataLayer.MutipleSchema 6 | { 7 | public class Class1 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DataLayer/MutipleSchema/Class2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DataLayer.MutipleSchema 6 | { 7 | public class Class2 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DataLayer/MutipleSchema/Class3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DataLayer.MutipleSchema 6 | { 7 | public class Class3 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DataLayer/MutipleSchema/Class4.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DataLayer.MutipleSchema 6 | { 7 | public class Class4 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DataLayer/MutipleSchema/ManySchemaDbContext.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace DataLayer.MutipleSchema 5 | { 6 | public class ManySchemaDbContext : DbContext 7 | { 8 | public ManySchemaDbContext(DbContextOptions options) 9 | : base(options) { } 10 | 11 | public DbSet Class1s { get; set; } 12 | public DbSet Class2s { get; set; } 13 | public DbSet Class3s { get; set; } 14 | public DbSet Class4s { get; set; } 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity().ToTable("Class2s", schema: "Schema2"); 19 | modelBuilder.Entity().ToTable("Class3s", schema: "Schema3"); 20 | modelBuilder.Entity().ToTable("Class4s", schema: "Schema4"); 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Address.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | namespace DataLayer.SpecialisedEntities 4 | { 5 | public class Address 6 | { 7 | public string NumberAndStreet { get; set; } 8 | public string City { get; set; } 9 | public string ZipPostCode { get; set; } 10 | public string CountryCodeIso2 { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/AllTypesEntity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.ComponentModel.DataAnnotations.Schema; 7 | 8 | namespace DataLayer.SpecialisedEntities 9 | { 10 | public class AllTypesEntity 11 | { 12 | public int Id { get; private set; } 13 | public bool MyBool { get; private set; } = true; 14 | public bool? MyBoolNullable { get; private set; } = null; 15 | public int MyInt { get; private set; } = 1234; 16 | public int? MyIntNullable { get; private set; } = null; 17 | public double MyDouble { get; private set; } = 5678.9012; 18 | public Decimal MyDecimal { get; private set; } = 3456.789m; 19 | public Guid MyGuid { get; set; } 20 | public Guid? MyGuidNullable { get; private set; } = null; 21 | 22 | public string MyString { get; private set; } = "string with ' in it"; 23 | public string MyStringNull { get; private set; } = null; 24 | public string MyStringEmptyString { get; private set; } = string.Empty; 25 | 26 | [Required] 27 | [Column(TypeName = "varchar(123)")] 28 | public string MyAnsiNonNullString { get; private set; } = "ascii only"; 29 | 30 | public DateTime MyDateTime { get; set; } 31 | public DateTime? MyDateTimeNullable { get; private set; } = null; 32 | public TimeSpan MyTimeSpan { get; set; } 33 | public DateTimeOffset MyDateTimeOffset { get; set; } 34 | public byte[] MyByteArray { get; set; } 35 | 36 | public void SetId(int id) 37 | { 38 | Id = id; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/BookDetail.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class BookDetail 7 | { 8 | public int BookDetailId { get; set; } 9 | public string Description { get; set; } 10 | public decimal Price { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/BookSummary.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class BookSummary 7 | { 8 | public int BookSummaryId { get; set; } 9 | 10 | public string Title { get; set; } 11 | 12 | public string AuthorsString { get; set; } 13 | 14 | public BookDetail Details { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Configurations/BookDetailConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.SpecialisedEntities.Configurations 8 | { 9 | public class BookDetailConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.ToTable("Books"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Configurations/BookSummaryConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.SpecialisedEntities.Configurations 8 | { 9 | public class BookSummaryConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.HasKey(p => p.BookSummaryId); 15 | 16 | entity 17 | .HasOne(e => e.Details).WithOne() 18 | .HasForeignKey(e => e.BookDetailId); 19 | entity.ToTable("Books"); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Configurations/OrderInfoConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.SpecialisedEntities.Configurations 8 | { 9 | public class OrderInfoConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity 15 | .OwnsOne(p => p.BillingAddress); 16 | entity 17 | .OwnsOne(p => p.DeliveryAddress); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Configurations/PaymentConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | 8 | namespace DataLayer.SpecialisedEntities.Configurations 9 | { 10 | public class PaymentConfig : IEntityTypeConfiguration 11 | { 12 | public void Configure 13 | (EntityTypeBuilder entity) 14 | { 15 | entity.HasDiscriminator(b => b.PType) //#A 16 | .HasValue(PTypes.Cash) //#B 17 | .HasValue(PTypes.Card); //#C 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Configurations/UserConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace DataLayer.SpecialisedEntities.Configurations 8 | { 9 | public class UserConfig : IEntityTypeConfiguration 10 | { 11 | public void Configure 12 | (EntityTypeBuilder entity) 13 | { 14 | entity.HasAlternateKey(p => p.Email); 15 | 16 | entity.OwnsOne(e => e.HomeAddress); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/MyEntityReadOnly.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class MyEntityReadOnly 7 | { 8 | public int MyInt { get; set; } 9 | public string MyString { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/OrderInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class OrderInfo 7 | { 8 | public int OrderInfoId { get; set; } 9 | public string OrderNumber { get; set; } 10 | 11 | public Address BillingAddress { get; set; } //#B 12 | public Address DeliveryAddress { get; set; } //#B 13 | } 14 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/OwnedWithKeyDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using DataLayer.SpecialisedEntities.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.SpecialisedEntities 8 | { 9 | public class OwnedWithKeyDbContext : DbContext 10 | { 11 | public DbSet Users { get; set; } 12 | 13 | public OwnedWithKeyDbContext(DbContextOptions options) 14 | : base(options) {} 15 | 16 | protected override void OnModelCreating 17 | (ModelBuilder modelBuilder) 18 | { 19 | modelBuilder.ApplyConfiguration(new UserConfig()); 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/Payment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | namespace DataLayer.SpecialisedEntities 4 | { 5 | public enum PTypes : byte { Cash = 1, Card = 2} 6 | public abstract class Payment 7 | { 8 | public int PaymentId { get; set; } 9 | 10 | public PTypes PType { get; set; } 11 | 12 | public decimal Amount { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/PaymentCard.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class PaymentCard : Payment 7 | { 8 | public string ReceiptCode { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/PaymentCash.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class PaymentCash : Payment 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/SpecializedDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using DataLayer.SpecialisedEntities.Configurations; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace DataLayer.SpecialisedEntities 8 | { 9 | public class SpecializedDbContext : DbContext 10 | { 11 | public DbSet BookSummaries { get; set; } 12 | public DbSet Orders { get; set; } 13 | public DbSet Payments { get; set; } 14 | public DbSet AllTypesEntities { get; set; } 15 | 16 | public SpecializedDbContext(DbContextOptions options) 17 | : base(options) {} 18 | 19 | protected override void OnModelCreating 20 | (ModelBuilder modelBuilder) 21 | { 22 | modelBuilder.ApplyConfiguration(new BookSummaryConfig()); 23 | modelBuilder.ApplyConfiguration(new BookDetailConfig()); 24 | modelBuilder.ApplyConfiguration(new OrderInfoConfig()); 25 | modelBuilder.ApplyConfiguration(new PaymentConfig()); 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /DataLayer/SpecialisedEntities/User.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | namespace DataLayer.SpecialisedEntities 5 | { 6 | public class User 7 | { 8 | public int UserId { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public string Email { get; set; } 13 | 14 | public Address HomeAddress { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DataLayer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MyString": "This is in the DataLayer" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /EfCoreInAction.Test.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jon P Smith GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | NOTE 24 | 25 | Two classes in this library come from Microsoft's Entity Framework Core 26 | and are therefore Copyright (c) .NET Foundation. All rights reserved. 27 | And licensed under the Apache License, Version 2.0. 28 | The licence is available at https://github.com/dotnet/efcore/blob/main/LICENSE.txt 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EfCore.TestSupport 2 | 3 | This NuGet package containing methods to help test applications that use [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/index) for database access using SQL Server, PostgreSQL, Cosmos DB, and a generic in-memory SQLite approach which works with every EF Core database provider (with limitations). This readme provides links to the documentation in the [EfCore.TestSupport wiki](https://github.com/JonPSmith/EfCore.TestSupport/wiki). Also see [Release Notes](https://github.com/JonPSmith/EfCore.TestSupport/blob/master/ReleaseNotes.md) for information on changes. 4 | 5 | The EfCore.TestSupport library is available on [NuGet as EfCore.TestSupport](https://www.nuget.org/packages/EfCore.TestSupport/) and is an open-source library under the MIT license. See [ReleaseNotes](https://github.com/JonPSmith/EfCore.TestSupport/blob/master/ReleaseNotes.md) for details of the changes in each vesion. 6 | 7 | ## List of versions and which .NET framework they support 8 | 9 | Since .NET 8 this library only supports one .NET. This change makes it easier to update to the next .NET release. 10 | 11 | - Version 9.?.? supports NET 9 only 12 | - Version 8.?.? supports NET 8 only 13 | - Version 6.?.? supports NET 6, 7 and 8 14 | - Version 5.2.? supports NET 5, 6 and 7 15 | 16 | _There are older versions of the EfCore.TestSupport library, but .NET lower than .NET 5 are not supported by Microsoft._ 17 | 18 | ## Documentation 19 | 20 | The NuGet package [EfCore.TestSupport](https://www.nuget.org/packages/EfCore.TestSupport/) containing methods to help you unit test applications that use [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/index) for database access. This readme defines the various groups, with links to the documentation in the [EfCore.TestSupport wiki](https://github.com/JonPSmith/EfCore.TestSupport/wiki). 21 | 22 | *NOTE: The techniques are explained in much more details in chapter 17 of the book [Entity Framework in Action, second edition](https://bit.ly/EfCoreBookEd2).* 23 | 24 | Here is an image covering just a few of the methods available in this library. 25 | 26 | ![Examples of library methods in use](https://github.com/JonPSmith/EfCore.TestSupport/blob/master/UnitTestExample.png) 27 | 28 | The various groups of tools are: 29 | 30 | 1. Helpers to create an in-memory Sqlite database for unit testing. 31 | See [Sqlite in memory test database](https://github.com/JonPSmith/EfCore.TestSupport/wiki/1.-Sqlite-in-memory-test-database). 32 | 2. Helpers to create connection strings with a unique database name. 33 | See [Creating connection strings](https://github.com/JonPSmith/EfCore.TestSupport/wiki/3.-Creating-connection-strings). 34 | 3. Helpers for creating unique SQL Server databases for unit testing. 35 | See [Create SQL Server databases](https://github.com/JonPSmith/EfCore.TestSupport/wiki/4.-Create-SQL-Server-databases). 36 | 4. Helpers to create Cosmos DB databases linked to Azure Cosmos DB Emulator. 37 | See [Create Cosmos DB options](https://github.com/JonPSmith/EfCore.TestSupport/wiki/Create-Cosmos-DB-options). 38 | 6. Helper for wiping all data and resetting the schema a SQL Server database. 39 | See [Quickly wipe and reset schema on SQL Server](#). 40 | 7. Various tools for getting test data, or file paths to test data. 41 | See [Test Data tools](https://github.com/JonPSmith/EfCore.TestSupport/wiki/6.-Test-Data-tools). 42 | 8. A tool for applying a SQL script file to a EF Core database. 43 | See [Run SQL Script](https://github.com/JonPSmith/EfCore.TestSupport/wiki/7.-Run-SQL-Script). 44 | 9. Tools for capturing EF Core logging. 45 | See [Capture EF Core logging](https://github.com/JonPSmith/EfCore.TestSupport/wiki/8.-Capture-EF-Core-logging). 46 | 47 | -------------------------------------------------------------------------------- /SeedFromProductionOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/74ce78237c8887de93a5cc0d2e8cf394463cf3d1/SeedFromProductionOverview.png -------------------------------------------------------------------------------- /Test/AltTestDataDir/Alt dummy file.txt: -------------------------------------------------------------------------------- 1 | This is the content of the dummy file -------------------------------------------------------------------------------- /Test/Helpers/DddEfTestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using DataLayer.DddBookApp; 7 | using DataLayer.DddBookApp.EfCode; 8 | 9 | namespace Tests.Helpers 10 | { 11 | public static class DddEfTestData 12 | { 13 | public const string DummyUserId = "UnitTestUserId"; 14 | public static readonly DateTime DummyBookStartDate = new DateTime(2010, 1, 1); 15 | 16 | public static void SeedDatabaseDummyBooks(this DddBookContext context, int numBooks = 10) 17 | { 18 | context.DddBooks.AddRange(CreateDummyBooks(numBooks)); 19 | context.SaveChanges(); 20 | } 21 | 22 | public static DddBook CreateDummyBookOneAuthor() 23 | { 24 | 25 | var book = DddBook.CreateBook 26 | ( 27 | "Book Title", 28 | "Book Description", 29 | DummyBookStartDate, 30 | "Book Publisher", 31 | 123, 32 | null, 33 | new[] { new DddAuthor { Name = "Test Author"} } 34 | ).Result; 35 | 36 | return book; 37 | } 38 | 39 | public static List CreateDummyBooks(int numBooks = 10, bool stepByYears = false) 40 | { 41 | var result = new List(); 42 | var commonAuthor = new DddAuthor { Name = "CommonAuthor"}; 43 | for (int i = 0; i < numBooks; i++) 44 | { 45 | var book = DddBook.CreateBook 46 | ( 47 | $"Book{i:D4} Title", 48 | $"Book{i:D4} Description", 49 | stepByYears ? DummyBookStartDate.AddYears(i) : DummyBookStartDate.AddDays(i), 50 | "Publisher", 51 | (short)(i + 1), 52 | $"Image{i:D4}", 53 | new[] { new DddAuthor { Name = $"Author{i:D4}"}, commonAuthor} 54 | ).Result; 55 | for (int j = 0; j < i; j++) 56 | { 57 | book.AddReview((j % 5) + 1, null, j.ToString()); 58 | } 59 | 60 | result.Add(book); 61 | } 62 | 63 | return result; 64 | } 65 | 66 | public static void SeedDatabaseFourBooks(this DddBookContext context) 67 | { 68 | context.DddBooks.AddRange(CreateFourBooks()); 69 | context.SaveChanges(); 70 | } 71 | 72 | public static List CreateFourBooks() 73 | { 74 | var martinFowler = new DddAuthor { Name = "Martin Fowler"}; 75 | 76 | var books = new List(); 77 | 78 | var book1 = DddBook.CreateBook 79 | ( 80 | "Refactoring", 81 | "Improving the design of existing code", 82 | new DateTime(1999, 7, 8), 83 | null, 84 | 40, 85 | null, 86 | new[] { martinFowler } 87 | ).Result; 88 | books.Add(book1); 89 | 90 | var book2 = DddBook.CreateBook 91 | ( 92 | "Patterns of Enterprise Application Architecture", 93 | "Written in direct response to the stiff challenges", 94 | new DateTime(2002, 11, 15), 95 | null, 96 | 53, 97 | null, 98 | new []{martinFowler} 99 | ).Result; 100 | books.Add(book2); 101 | 102 | var book3 = DddBook.CreateBook 103 | ( 104 | "Domain-Driven Design", 105 | "Linking business needs to software design", 106 | new DateTime(2003, 8, 30), 107 | null, 108 | 56, 109 | null, 110 | new[] { new DddAuthor { Name = "Eric Evans"}} 111 | ).Result; 112 | books.Add(book3); 113 | 114 | var book4 = DddBook.CreateBook 115 | ( 116 | "Quantum Networking", 117 | "Entangled quantum networking provides faster-than-light data communications", 118 | new DateTime(2057, 1, 1), 119 | "Future Published", 120 | 220, 121 | null, 122 | new[] { new DddAuthor { Name = "Future Person"} } 123 | ).Result; 124 | book4.AddReview(5, 125 | "I look forward to reading this book, if I am still alive!", "Jon P Smith"); 126 | book4.AddReview(5, 127 | "I write this book if I was still alive!", "Albert Einstein"); book4.AddPromotion(219, "Save $1 if you order 40 years ahead!"); 128 | book4.AddPromotion(219, "Save 1$ by buying 40 years ahead"); 129 | 130 | books.Add(book4); 131 | 132 | return books; 133 | } 134 | 135 | } 136 | } -------------------------------------------------------------------------------- /Test/Helpers/EfTestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using DataLayer.BookApp; 7 | using DataLayer.BookApp.EfCode; 8 | 9 | namespace Test.Helpers 10 | { 11 | public static class EfTestData 12 | { 13 | public static readonly DateTime DummyBookStartDate = new DateTime(2010, 1, 1); 14 | 15 | public static void SeedDatabaseDummyBooks(this BookContext context, int numBooks = 10) 16 | { 17 | context.Books.AddRange(CreateDummyBooks(numBooks)); 18 | context.SaveChanges(); 19 | } 20 | 21 | public static Book CreateDummyBookOneAuthor() 22 | { 23 | 24 | var book = new Book 25 | { 26 | Title = "Book Title", 27 | Description = "Book Description", 28 | Price = 123, 29 | PublishedOn = DummyBookStartDate 30 | }; 31 | 32 | var author = new Author {Name = "Test Author"}; 33 | book.AuthorsLink = new List 34 | { 35 | new BookAuthor {Book = book, Author = author}, 36 | }; 37 | 38 | return book; 39 | } 40 | 41 | public static List CreateDummyBooks(int numBooks = 10) 42 | { 43 | var result = new List(); 44 | var commonAuthor = new Author { Name = "CommonAuthor" }; 45 | for (int i = 0; i < numBooks; i++) 46 | { 47 | var reviews = new List(); 48 | for (int j = 0; j < i; j++) 49 | { 50 | reviews.Add(new Review { VoterName = j.ToString(), NumStars = (j % 5) + 1 }); 51 | } 52 | var book = new Book 53 | { 54 | Title = $"Book{i:D4} Title", 55 | Description = $"Book{i:D4} Description", 56 | Price = (short)(i + 1), 57 | ImageUrl = $"Image{i:D4}", 58 | PublishedOn = DummyBookStartDate.AddYears(i), 59 | Reviews = reviews 60 | }; 61 | 62 | var author = new Author { Name = $"Author{i:D4}" }; 63 | book.AuthorsLink = new List 64 | { 65 | new BookAuthor {Book = book, Author = author, Order = 0}, 66 | new BookAuthor {Book = book, Author = commonAuthor, Order = 1} 67 | }; 68 | 69 | result.Add(book); 70 | } 71 | 72 | return result; 73 | } 74 | 75 | public static void SeedDatabaseFourBooks(this BookContext context) 76 | { 77 | context.Books.AddRange(CreateFourBooks()); 78 | context.SaveChanges(); 79 | } 80 | 81 | public static List CreateFourBooks() 82 | { 83 | var martinFowler = new Author 84 | { 85 | Name = "Martin Fowler" 86 | }; 87 | 88 | var books = new List(); 89 | 90 | var book1 = new Book 91 | { 92 | Title = "Refactoring", 93 | Description = "Improving the design of existing code", 94 | PublishedOn = new DateTime(1999, 7, 8), 95 | Price = 40 96 | }; 97 | book1.AuthorsLink = new List { new BookAuthor { Author = martinFowler, Book = book1 } }; 98 | books.Add(book1); 99 | 100 | var book2 = new Book 101 | { 102 | Title = "Patterns of Enterprise Application Architecture", 103 | Description = "Written in direct response to the stiff challenges", 104 | PublishedOn = new DateTime(2002, 11, 15), 105 | Price = 53 106 | }; 107 | book2.AuthorsLink = new List { new BookAuthor { Author = martinFowler, Book = book2 } }; 108 | books.Add(book2); 109 | 110 | var book3 = new Book 111 | { 112 | Title = "Domain-Driven Design", 113 | Description = "Linking business needs to software design", 114 | PublishedOn = new DateTime(2003, 8, 30), 115 | Price = 56 116 | }; 117 | book3.AuthorsLink = new List { new BookAuthor { Author = new Author { Name = "Eric Evans" }, Book = book3 } }; 118 | books.Add(book3); 119 | 120 | var book4 = new Book 121 | { 122 | Title = "Quantum Networking", 123 | Description = "Entangled quantum networking provides faster-than-light data communications", 124 | PublishedOn = new DateTime(2057, 1, 1), 125 | Price = 220 126 | }; 127 | book4.AuthorsLink = new List { new BookAuthor { Author = new Author { Name = "Future Person" }, Book = book4 } }; 128 | book4.Reviews = new List 129 | { 130 | new Review { VoterName = "Jon P Smith", NumStars = 5, Comment = "I look forward to reading this book, if I am still alive!"}, 131 | new Review { VoterName = "Albert Einstein", NumStars = 5, Comment = "I write this book if I was still alive!"} 132 | }; 133 | book4.Promotion = new PriceOffer {NewPrice = 219, PromotionalText = "Save $1 if you order 40 years ahead!"}; 134 | books.Add(book4); 135 | 136 | return books; 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Test/TestData/AddUserDefinedFunctions.sql: -------------------------------------------------------------------------------- 1 | -- SQL script file to add SQL code to improve performance 2 | -- I have built this as an Idempotent Script, that is, it can be applied even if there isn't a change and it will ensure the database is up to date 3 | 4 | IF OBJECT_ID('dbo.AuthorsStringUdf') IS NOT NULL 5 | DROP FUNCTION dbo.AuthorsStringUdf 6 | GO 7 | 8 | CREATE FUNCTION AuthorsStringUdf (@bookId int) 9 | RETURNS NVARCHAR(4000) 10 | AS 11 | BEGIN 12 | -- Thanks to https://stackoverflow.com/a/194887/1434764 13 | DECLARE @Names AS NVARCHAR(4000) 14 | SELECT @Names = COALESCE(@Names + ', ', '') + a.Name 15 | FROM Authors AS a, Books AS b, BookAuthor AS ba 16 | WHERE ba.BookId = @bookId 17 | AND ba.AuthorId = a.AuthorId 18 | AND ba.BookId = b.BookId 19 | ORDER BY ba.[Order] 20 | RETURN @Names 21 | END 22 | GO 23 | -------------------------------------------------------------------------------- /Test/TestData/AlterMyClassesToNotHaveAPrimaryKey.sql: -------------------------------------------------------------------------------- 1 | -- SQL script to create table without key 2 | 3 | DROP TABLE IF EXISTS [MyClasses]; 4 | GO 5 | 6 | CREATE TABLE [dbo].[MyClasses]( 7 | [MyInt] [int] NOT NULL, 8 | [MyString] [nvarchar](max) NULL, 9 | ) 10 | GO 11 | -------------------------------------------------------------------------------- /Test/TestData/DbContextCompareLog01 - MyEntity default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "SubLogs": [ 4 | { 5 | "SubLogs": [ 6 | { 7 | "SubLogs": [], 8 | "Type": "Property", 9 | "State": "Ok", 10 | "Name": "MyEntityId", 11 | "Attribute": "NotSet", 12 | "Expected": "MyEntityId", 13 | "Found": null 14 | }, 15 | { 16 | "SubLogs": [], 17 | "Type": "Property", 18 | "State": "Ok", 19 | "Name": "MyDateTime", 20 | "Attribute": "NotSet", 21 | "Expected": "MyDateTime", 22 | "Found": null 23 | }, 24 | { 25 | "SubLogs": [], 26 | "Type": "Property", 27 | "State": "Ok", 28 | "Name": "MyInt", 29 | "Attribute": "NotSet", 30 | "Expected": "MyInt", 31 | "Found": null 32 | }, 33 | { 34 | "SubLogs": [], 35 | "Type": "Property", 36 | "State": "Ok", 37 | "Name": "MyString", 38 | "Attribute": "NotSet", 39 | "Expected": "MyString", 40 | "Found": null 41 | }, 42 | { 43 | "SubLogs": [], 44 | "Type": "Index", 45 | "State": "Ok", 46 | "Name": "PK_MyEntites", 47 | "Attribute": "NotSet", 48 | "Expected": "PK_MyEntites", 49 | "Found": null 50 | } 51 | ], 52 | "Type": "Entity", 53 | "State": "Ok", 54 | "Name": "MyEntity", 55 | "Attribute": "NotSet", 56 | "Expected": "MyEntites", 57 | "Found": null 58 | } 59 | ], 60 | "Type": "DbContext", 61 | "State": "Ok", 62 | "Name": "MyEntityDbContext", 63 | "Attribute": "NotSet", 64 | "Expected": "MyEntityDbContext", 65 | "Found": null 66 | } 67 | ] -------------------------------------------------------------------------------- /Test/TestData/Dummy file.txt: -------------------------------------------------------------------------------- 1 | This is the content of the dummy file -------------------------------------------------------------------------------- /Test/TestData/Index01 - Create MyEntites with unique constraint.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [MyEntites] ( 2 | [MyEntityId] int NOT NULL IDENTITY, 3 | [MyDateTime] datetime2 NOT NULL, 4 | [MyInt] int NOT NULL, 5 | [MyString] nvarchar(450) NULL, 6 | CONSTRAINT [PK_MyEntites] PRIMARY KEY ([MyEntityId]), 7 | CONSTRAINT [MySpecialName] UNIQUE NONCLUSTERED ([MyInt]) 8 | ); 9 | GO 10 | 11 | CREATE INDEX [IX_MyEntites_MyString] ON [MyEntites] ([MyString]); 12 | GO 13 | -------------------------------------------------------------------------------- /Test/TestData/Logging01 - example logged, no values.txt: -------------------------------------------------------------------------------- 1 | Information: Executed DbCommand (1ms) [Parameters=[@p0='?', @p1='?', @p2='?', @p3='?' (Size = 4000), @p4='?', @p5='?' (Size = 100)], CommandType='Text', CommandTimeout='30'] 2 | SET NOCOUNT ON; 3 | DELETE FROM [Review] 4 | WHERE [ReviewId] = @p0; 5 | SELECT @@ROWCOUNT; 6 | 7 | DELETE FROM [Review] 8 | WHERE [ReviewId] = @p1; 9 | SELECT @@ROWCOUNT; 10 | 11 | INSERT INTO [Review] ([BookId], [Comment], [NumStars], [VoterName]) 12 | VALUES (@p2, @p3, @p4, @p5); 13 | SELECT [ReviewId] 14 | FROM [Review] 15 | WHERE @@ROWCOUNT = 1 AND [ReviewId] = scope_identity(); -------------------------------------------------------------------------------- /Test/TestData/Logging02 - funny name param, no values.txt: -------------------------------------------------------------------------------- 1 | Information: Executed DbCommand (0ms) [Parameters=[@__twoReviewBookId_0='?'], CommandType='Text', CommandTimeout='30'] 2 | SELECT TOP(2) [b].[BookId], [b].[Description], [b].[ImageUrl], [b].[Price], [b].[PublishedOn], [b].[Publisher], [b].[SoftDeleted], [b].[Title] 3 | FROM [Books] AS [b] 4 | WHERE ([b].[SoftDeleted] = 0) AND ([b].[BookId] = @__twoReviewBookId_0) 5 | ORDER BY [b].[BookId] 6 | Information: Executed DbCommand (2ms) [Parameters=[@__twoReviewBookId_0='?'], CommandType='Text', CommandTimeout='30'] 7 | SELECT [p.Reviews].[ReviewId], [p.Reviews].[BookId], [p.Reviews].[Comment], [p.Reviews].[NumStars], [p.Reviews].[VoterName] 8 | FROM [Review] AS [p.Reviews] 9 | INNER JOIN ( 10 | SELECT TOP(1) [b0].[BookId] 11 | FROM [Books] AS [b0] 12 | WHERE ([b0].[SoftDeleted] = 0) AND ([b0].[BookId] = @__twoReviewBookId_0) 13 | ORDER BY [b0].[BookId] 14 | ) AS [t] ON [p.Reviews].[BookId] = [t].[BookId] 15 | ORDER BY [t].[BookId] -------------------------------------------------------------------------------- /Test/TestData/Logging03 - sensitive data with odd string.txt: -------------------------------------------------------------------------------- 1 | Warning,SensitiveDataLoggingEnabledWarning: Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data, this mode should only be enabled during development. 2 | Information,CommandExecuted: Executed DbCommand (10ms) [Parameters=[@p0='' (DbType = String), @p1='' (DbType = String), @p2='0' (DbType = String), @p3='01/01/0001 00:00:00' (DbType = String), @p4='' (DbType = String), @p5='False' (DbType = String), @p6='The person's boss said, "What's that about?"' (Nullable = false)], CommandType='Text', CommandTimeout='30'] 3 | INSERT INTO "Books" ("Description", "ImageUrl", "Price", "PublishedOn", "Publisher", "SoftDeleted", "Title") 4 | VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6); 5 | SELECT "BookId" 6 | FROM "Books" 7 | WHERE changes() = 1 AND "BookId" = last_insert_rowid(); -------------------------------------------------------------------------------- /Test/TestData/Script01 - Add row to Authors table.sql: -------------------------------------------------------------------------------- 1 | -- This adds one Author row to the database 2 | 3 | INSERT INTO Authors (Name) VALUES('Unit test of ApplyScriptToDatabase') 4 | GO 5 | -------------------------------------------------------------------------------- /Test/TestData/Script02 - Add two rows to Authors table.sql: -------------------------------------------------------------------------------- 1 | -- This adds one Author row to the database 2 | 3 | INSERT INTO Authors (Name) VALUES('Row 1') 4 | GO 5 | 6 | INSERT INTO Authors (Name) VALUES('Row 2') 7 | GO 8 | -------------------------------------------------------------------------------- /Test/TestData/SeedData-DddExampleDatabase.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$id": "1", 4 | "BookId": 0, 5 | "Title": "Refactoring", 6 | "Description": "Improving the design of existing code", 7 | "PublishedOn": "1999-07-08T00:00:00", 8 | "Publisher": null, 9 | "OrgPrice": 40.0, 10 | "ActualPrice": 40.0, 11 | "PromotionalText": null, 12 | "ImageUrl": null, 13 | "Reviews": [], 14 | "AuthorsLink": [ 15 | { 16 | "$id": "2", 17 | "BookId": 0, 18 | "AuthorId": 0, 19 | "Order": 0, 20 | "DddBook": { 21 | "$ref": "1" 22 | }, 23 | "DddAuthor": { 24 | "$id": "3", 25 | "AuthorId": 0, 26 | "Name": "Martin Fowler", 27 | "Email": null, 28 | "BooksLink": [ 29 | { 30 | "$ref": "2" 31 | }, 32 | { 33 | "$id": "4", 34 | "BookId": 0, 35 | "AuthorId": 0, 36 | "Order": 0, 37 | "DddBook": { 38 | "$id": "5", 39 | "BookId": 0, 40 | "Title": "Patterns of Enterprise Application Architecture", 41 | "Description": "Written in direct response to the stiff challenges", 42 | "PublishedOn": "2002-11-15T00:00:00", 43 | "Publisher": null, 44 | "OrgPrice": 53.0, 45 | "ActualPrice": 53.0, 46 | "PromotionalText": null, 47 | "ImageUrl": null, 48 | "Reviews": [], 49 | "AuthorsLink": [ 50 | { 51 | "$ref": "4" 52 | } 53 | ] 54 | }, 55 | "DddAuthor": { 56 | "$ref": "3" 57 | } 58 | } 59 | ] 60 | } 61 | } 62 | ] 63 | }, 64 | { 65 | "$ref": "5" 66 | }, 67 | { 68 | "$id": "6", 69 | "BookId": 0, 70 | "Title": "Domain-Driven Design", 71 | "Description": "Linking business needs to software design", 72 | "PublishedOn": "2003-08-30T00:00:00", 73 | "Publisher": null, 74 | "OrgPrice": 56.0, 75 | "ActualPrice": 56.0, 76 | "PromotionalText": null, 77 | "ImageUrl": null, 78 | "Reviews": [], 79 | "AuthorsLink": [ 80 | { 81 | "$id": "7", 82 | "BookId": 0, 83 | "AuthorId": 0, 84 | "Order": 0, 85 | "DddBook": { 86 | "$ref": "6" 87 | }, 88 | "DddAuthor": { 89 | "$id": "8", 90 | "AuthorId": 0, 91 | "Name": "Eric Evans", 92 | "Email": null, 93 | "BooksLink": [ 94 | { 95 | "$ref": "7" 96 | } 97 | ] 98 | } 99 | } 100 | ] 101 | }, 102 | { 103 | "$id": "9", 104 | "BookId": 0, 105 | "Title": "Quantum Networking", 106 | "Description": "Entangled quantum networking provides faster-than-light data communications", 107 | "PublishedOn": "2057-01-01T00:00:00", 108 | "Publisher": "Future Published", 109 | "OrgPrice": 220.0, 110 | "ActualPrice": 219.0, 111 | "PromotionalText": "Save 1$ by buying 40 years ahead", 112 | "ImageUrl": null, 113 | "Reviews": [ 114 | { 115 | "$id": "10", 116 | "ReviewId": 0, 117 | "VoterName": "Jon P Smith", 118 | "NumStars": 5, 119 | "Comment": "I look forward to reading this book, if I am still alive!", 120 | "BookId": 0 121 | }, 122 | { 123 | "$id": "11", 124 | "ReviewId": 0, 125 | "VoterName": "Albert Einstein", 126 | "NumStars": 5, 127 | "Comment": "I write this book if I was still alive!", 128 | "BookId": 0 129 | } 130 | ], 131 | "AuthorsLink": [ 132 | { 133 | "$id": "12", 134 | "BookId": 0, 135 | "AuthorId": 0, 136 | "Order": 0, 137 | "DddBook": { 138 | "$ref": "9" 139 | }, 140 | "DddAuthor": { 141 | "$id": "13", 142 | "AuthorId": 0, 143 | "Name": "Future Person", 144 | "Email": null, 145 | "BooksLink": [ 146 | { 147 | "$ref": "12" 148 | } 149 | ] 150 | } 151 | } 152 | ] 153 | } 154 | ] -------------------------------------------------------------------------------- /Test/TestData/SeedData-DddExampleDatabaseAnonymised.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$id": "1", 4 | "BookId": 0, 5 | "Title": "Refactoring", 6 | "Description": "Improving the design of existing code", 7 | "PublishedOn": "1999-07-08T00:00:00", 8 | "Publisher": null, 9 | "OrgPrice": 40.0, 10 | "ActualPrice": 40.0, 11 | "PromotionalText": null, 12 | "ImageUrl": null, 13 | "Reviews": [], 14 | "AuthorsLink": [ 15 | { 16 | "$id": "2", 17 | "BookId": 0, 18 | "AuthorId": 0, 19 | "Order": 0, 20 | "Book": { 21 | "$ref": "1" 22 | }, 23 | "Author": { 24 | "AuthorId": 0, 25 | "Name": "Alisa Streets", 26 | "Email": null, 27 | "BooksLink": [ 28 | { 29 | "$ref": "2" 30 | }, 31 | { 32 | "$id": "3", 33 | "BookId": 0, 34 | "AuthorId": 0, 35 | "Order": 0, 36 | "Book": { 37 | "BookId": 0, 38 | "Title": "Patterns of Enterprise Application Architecture", 39 | "Description": "Written in direct response to the stiff challenges", 40 | "PublishedOn": "2002-11-15T00:00:00", 41 | "Publisher": null, 42 | "OrgPrice": 53.0, 43 | "ActualPrice": 53.0, 44 | "PromotionalText": null, 45 | "ImageUrl": null, 46 | "Reviews": [], 47 | "AuthorsLink": [ 48 | { 49 | "$ref": "3" 50 | } 51 | ] 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | }, 59 | { 60 | "$id": "4", 61 | "BookId": 0, 62 | "Title": "Patterns of Enterprise Application Architecture", 63 | "Description": "Written in direct response to the stiff challenges", 64 | "PublishedOn": "2002-11-15T00:00:00", 65 | "Publisher": null, 66 | "OrgPrice": 53.0, 67 | "ActualPrice": 53.0, 68 | "PromotionalText": null, 69 | "ImageUrl": null, 70 | "Reviews": [], 71 | "AuthorsLink": [ 72 | { 73 | "$ref": "3" 74 | } 75 | ] 76 | }, 77 | { 78 | "$id": "5", 79 | "BookId": 0, 80 | "Title": "Domain-Driven Design", 81 | "Description": "Linking business needs to software design", 82 | "PublishedOn": "2003-08-30T00:00:00", 83 | "Publisher": null, 84 | "OrgPrice": 56.0, 85 | "ActualPrice": 56.0, 86 | "PromotionalText": null, 87 | "ImageUrl": null, 88 | "Reviews": [], 89 | "AuthorsLink": [ 90 | { 91 | "$id": "6", 92 | "BookId": 0, 93 | "AuthorId": 0, 94 | "Order": 0, 95 | "Book": { 96 | "$ref": "5" 97 | }, 98 | "Author": { 99 | "AuthorId": 0, 100 | "Name": "Rhoda Verhey", 101 | "Email": null, 102 | "BooksLink": [ 103 | { 104 | "$ref": "6" 105 | } 106 | ] 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | "$id": "7", 113 | "BookId": 0, 114 | "Title": "Quantum Networking", 115 | "Description": "Entangled quantum networking provides faster-than-light data communications", 116 | "PublishedOn": "2057-01-01T00:00:00", 117 | "Publisher": "Future Published", 118 | "OrgPrice": 220.0, 119 | "ActualPrice": 219.0, 120 | "PromotionalText": "Save 1$ by buying 40 years ahead", 121 | "ImageUrl": null, 122 | "Reviews": [ 123 | { 124 | "$id": "8", 125 | "ReviewId": 0, 126 | "VoterName": "Jade", 127 | "NumStars": 5, 128 | "Comment": "I look forward to reading this book, if I am still alive!", 129 | "BookId": 0 130 | }, 131 | { 132 | "$id": "9", 133 | "ReviewId": 0, 134 | "VoterName": "Bryon", 135 | "NumStars": 5, 136 | "Comment": "I write this book if I was still alive!", 137 | "BookId": 0 138 | } 139 | ], 140 | "AuthorsLink": [ 141 | { 142 | "$id": "10", 143 | "BookId": 0, 144 | "AuthorId": 0, 145 | "Order": 0, 146 | "Book": { 147 | "$ref": "7" 148 | }, 149 | "Author": { 150 | "AuthorId": 0, 151 | "Name": "Patience Marbury", 152 | "Email": null, 153 | "BooksLink": [ 154 | { 155 | "$ref": "10" 156 | } 157 | ] 158 | } 159 | } 160 | ] 161 | } 162 | ] -------------------------------------------------------------------------------- /Test/TestData/SeedData-ExampleDatabase.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$id": "1", 4 | "BookId": 0, 5 | "Title": "Refactoring", 6 | "Description": "Improving the design of existing code", 7 | "PublishedOn": "1999-07-08T00:00:00", 8 | "Publisher": null, 9 | "Price": 40.0, 10 | "ImageUrl": null, 11 | "SoftDeleted": false, 12 | "Promotion": null, 13 | "Reviews": [], 14 | "AuthorsLink": [ 15 | { 16 | "$id": "2", 17 | "BookId": 0, 18 | "AuthorId": 0, 19 | "Order": 0, 20 | "Book": { 21 | "$ref": "1" 22 | }, 23 | "Author": { 24 | "$id": "3", 25 | "AuthorId": 0, 26 | "Name": "Martin Fowler", 27 | "BooksLink": [ 28 | { 29 | "$ref": "2" 30 | }, 31 | { 32 | "$id": "4", 33 | "BookId": 0, 34 | "AuthorId": 0, 35 | "Order": 0, 36 | "Book": { 37 | "$id": "5", 38 | "BookId": 0, 39 | "Title": "Patterns of Enterprise Application Architecture", 40 | "Description": "Written in direct response to the stiff challenges", 41 | "PublishedOn": "2002-11-15T00:00:00", 42 | "Publisher": null, 43 | "Price": 53.0, 44 | "ImageUrl": null, 45 | "SoftDeleted": false, 46 | "Promotion": null, 47 | "Reviews": [], 48 | "AuthorsLink": [ 49 | { 50 | "$ref": "4" 51 | } 52 | ] 53 | }, 54 | "Author": { 55 | "$ref": "3" 56 | } 57 | } 58 | ] 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | "$ref": "5" 65 | }, 66 | { 67 | "$id": "6", 68 | "BookId": 0, 69 | "Title": "Domain-Driven Design", 70 | "Description": "Linking business needs to software design", 71 | "PublishedOn": "2003-08-30T00:00:00", 72 | "Publisher": null, 73 | "Price": 56.0, 74 | "ImageUrl": null, 75 | "SoftDeleted": false, 76 | "Promotion": null, 77 | "Reviews": [], 78 | "AuthorsLink": [ 79 | { 80 | "$id": "7", 81 | "BookId": 0, 82 | "AuthorId": 0, 83 | "Order": 0, 84 | "Book": { 85 | "$ref": "6" 86 | }, 87 | "Author": { 88 | "$id": "8", 89 | "AuthorId": 0, 90 | "Name": "Eric Evans", 91 | "BooksLink": [ 92 | { 93 | "$ref": "7" 94 | } 95 | ] 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | "$id": "9", 102 | "BookId": 0, 103 | "Title": "Quantum Networking", 104 | "Description": "Entangled quantum networking provides faster-than-light data communications", 105 | "PublishedOn": "2057-01-01T00:00:00", 106 | "Publisher": null, 107 | "Price": 220.0, 108 | "ImageUrl": null, 109 | "SoftDeleted": false, 110 | "Promotion": { 111 | "$id": "10", 112 | "PriceOfferId": 0, 113 | "NewPrice": 219.0, 114 | "PromotionalText": "Save $1 if you order 40 years ahead!", 115 | "BookId": 0 116 | }, 117 | "Reviews": [ 118 | { 119 | "$id": "11", 120 | "ReviewId": 0, 121 | "VoterName": "Jon P Smith", 122 | "NumStars": 5, 123 | "Comment": "I look forward to reading this book, if I am still alive!", 124 | "BookId": 0 125 | }, 126 | { 127 | "$id": "12", 128 | "ReviewId": 0, 129 | "VoterName": "Albert Einstein", 130 | "NumStars": 5, 131 | "Comment": "I write this book if I was still alive!", 132 | "BookId": 0 133 | } 134 | ], 135 | "AuthorsLink": [ 136 | { 137 | "$id": "13", 138 | "BookId": 0, 139 | "AuthorId": 0, 140 | "Order": 0, 141 | "Book": { 142 | "$ref": "9" 143 | }, 144 | "Author": { 145 | "$id": "14", 146 | "AuthorId": 0, 147 | "Name": "Future Person", 148 | "BooksLink": [ 149 | { 150 | "$ref": "13" 151 | } 152 | ] 153 | } 154 | } 155 | ] 156 | } 157 | ] -------------------------------------------------------------------------------- /Test/TestData/SeedData-ExampleDatabaseAnonymised.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$id": "1", 4 | "BookId": 0, 5 | "Title": "Refactoring", 6 | "Description": "Improving the design of existing code", 7 | "PublishedOn": "1999-07-08T00:00:00", 8 | "Publisher": null, 9 | "Price": 40.0, 10 | "ImageUrl": null, 11 | "SoftDeleted": false, 12 | "Promotion": null, 13 | "Reviews": [], 14 | "AuthorsLink": [ 15 | { 16 | "$id": "2", 17 | "BookId": 0, 18 | "AuthorId": 0, 19 | "Order": 0, 20 | "Book": { 21 | "$ref": "1" 22 | }, 23 | "Author": { 24 | "$id": "3", 25 | "AuthorId": 0, 26 | "Name": "Alisa Streets", 27 | "BooksLink": [ 28 | { 29 | "$ref": "2" 30 | }, 31 | { 32 | "$id": "4", 33 | "BookId": 0, 34 | "AuthorId": 0, 35 | "Order": 0, 36 | "Book": { 37 | "$id": "5", 38 | "BookId": 0, 39 | "Title": "Patterns of Enterprise Application Architecture", 40 | "Description": "Written in direct response to the stiff challenges", 41 | "PublishedOn": "2002-11-15T00:00:00", 42 | "Publisher": null, 43 | "Price": 53.0, 44 | "ImageUrl": null, 45 | "SoftDeleted": false, 46 | "Promotion": null, 47 | "Reviews": [], 48 | "AuthorsLink": [ 49 | { 50 | "$ref": "4" 51 | } 52 | ] 53 | }, 54 | "Author": { 55 | "$ref": "3" 56 | } 57 | } 58 | ] 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | "$ref": "5" 65 | }, 66 | { 67 | "$id": "6", 68 | "BookId": 0, 69 | "Title": "Domain-Driven Design", 70 | "Description": "Linking business needs to software design", 71 | "PublishedOn": "2003-08-30T00:00:00", 72 | "Publisher": null, 73 | "Price": 56.0, 74 | "ImageUrl": null, 75 | "SoftDeleted": false, 76 | "Promotion": null, 77 | "Reviews": [], 78 | "AuthorsLink": [ 79 | { 80 | "$id": "7", 81 | "BookId": 0, 82 | "AuthorId": 0, 83 | "Order": 0, 84 | "Book": { 85 | "$ref": "6" 86 | }, 87 | "Author": { 88 | "$id": "8", 89 | "AuthorId": 0, 90 | "Name": "Rhoda Verhey", 91 | "BooksLink": [ 92 | { 93 | "$ref": "7" 94 | } 95 | ] 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | "$id": "9", 102 | "BookId": 0, 103 | "Title": "Quantum Networking", 104 | "Description": "Entangled quantum networking provides faster-than-light data communications", 105 | "PublishedOn": "2057-01-01T00:00:00", 106 | "Publisher": null, 107 | "Price": 220.0, 108 | "ImageUrl": null, 109 | "SoftDeleted": false, 110 | "Promotion": { 111 | "$id": "10", 112 | "PriceOfferId": 0, 113 | "NewPrice": 219.0, 114 | "PromotionalText": "Save $1 if you order 40 years ahead!", 115 | "BookId": 0 116 | }, 117 | "Reviews": [ 118 | { 119 | "$id": "11", 120 | "ReviewId": 0, 121 | "VoterName": "Jade", 122 | "NumStars": 5, 123 | "Comment": "I look forward to reading this book, if I am still alive!", 124 | "BookId": 0 125 | }, 126 | { 127 | "$id": "12", 128 | "ReviewId": 0, 129 | "VoterName": "Bryon", 130 | "NumStars": 5, 131 | "Comment": "I write this book if I was still alive!", 132 | "BookId": 0 133 | } 134 | ], 135 | "AuthorsLink": [ 136 | { 137 | "$id": "13", 138 | "BookId": 0, 139 | "AuthorId": 0, 140 | "Order": 0, 141 | "Book": { 142 | "$ref": "9" 143 | }, 144 | "Author": { 145 | "$id": "14", 146 | "AuthorId": 0, 147 | "Name": "Patience Marbury", 148 | "BooksLink": [ 149 | { 150 | "$ref": "13" 151 | } 152 | ] 153 | } 154 | } 155 | ] 156 | } 157 | ] -------------------------------------------------------------------------------- /Test/TestData/SubDirWithOneFileInIt/One file.txt: -------------------------------------------------------------------------------- 1 | This is the content of the dummy file -------------------------------------------------------------------------------- /Test/TestData/differentAppSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MyString": "This is in the TestData directory in Test" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /Test/UnitCommands/DeleteAllUnitTestDatabases.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using TestSupport.Attributes; 5 | using TestSupport.EfHelpers; 6 | using Xunit.Abstractions; 7 | 8 | namespace Test.UnitCommands 9 | { 10 | public class DeleteAllUnitTestDatabases 11 | { 12 | private readonly ITestOutputHelper _output; 13 | 14 | public DeleteAllUnitTestDatabases(ITestOutputHelper output) 15 | { 16 | _output = output; 17 | } 18 | 19 | //Run this method to wipe ALL the SQL Server test databases using your appsetting.json connection string 20 | //You need to run it in debug mode - that stops it being run when you "run all" unit tests 21 | [RunnableInDebugOnly] //#A 22 | public void DeleteAllSqlServerTestDatabasesOk() //#B 23 | { 24 | var numDeleted = DatabaseTidyHelper //#C 25 | .DeleteAllSqlServerTestDatabases();//#C 26 | _output.WriteLine( //#D 27 | "This deleted {0} SQL Server databases.", numDeleted); //#D 28 | } 29 | 30 | //Run this method to wipe ALL the PostgreSql test databases using your appsetting.json connection string 31 | //You need to run it in debug mode - that stops it being run when you "run all" unit tests 32 | [RunnableInDebugOnly] //#A 33 | public void DeleteAllPostgreSqlTestDatabasesOk() 34 | { 35 | var numDeleted = DatabaseTidyHelper 36 | .DeleteAllPostgreSqlTestDatabases(); 37 | _output.WriteLine( 38 | "This deleted {0} PostgreSql databases.", numDeleted); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Test/UnitTests/TestDataLayer/ExampleTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DataLayer.BookApp; 7 | using DataLayer.BookApp.EfCode; 8 | using TestSupport.EfHelpers; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests.TestDataLayer 14 | { 15 | public class ExampleTest 16 | { 17 | 18 | private readonly ITestOutputHelper _output; 19 | 20 | public ExampleTest(ITestOutputHelper output) 21 | { 22 | _output = output; 23 | } 24 | 25 | [Fact] 26 | public void TestExample() 27 | { 28 | //SETUP 29 | var logs = new List(); 30 | var options = this.CreateUniqueClassOptionsWithLogTo(log => logs.Add(log)); 31 | using (var context = new BookContext(options)) 32 | { 33 | context.Database.EnsureClean(); 34 | logs.Clear(); 35 | 36 | //ATTEMPT 37 | context.Add(new Book {Title = "New Book"}); 38 | context.SaveChanges(); 39 | 40 | //VERIFY 41 | context.Books.Count().ShouldEqual(1); 42 | foreach (var log in logs) 43 | { 44 | _output.WriteLine(log); 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDataLayer/TestApplyScriptExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using DataLayer.BookApp.EfCode; 6 | using TestSupport.EfHelpers; 7 | using TestSupport.Helpers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests.TestDataLayer 13 | { 14 | public class TestApplyScriptExtension 15 | 16 | { 17 | private readonly ITestOutputHelper _output; 18 | 19 | public TestApplyScriptExtension(ITestOutputHelper output) 20 | { 21 | _output = output; 22 | } 23 | 24 | [Fact] 25 | public void TestApplyScriptOneCommandToDatabaseOk() 26 | { 27 | //SETUP 28 | var options = this.CreateUniqueClassOptions(); 29 | var filepath = TestData.GetFilePath("Script01 - Add row to Authors table.sql"); 30 | using (var context = new BookContext(options)) 31 | { 32 | context.Database.EnsureClean(); 33 | 34 | //ATTEMPT 35 | context.ExecuteScriptFileInTransaction(filepath); 36 | 37 | //VERIFY 38 | context.Authors.Count().ShouldEqual(1); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void TestApplyScriptTwoCommandsToDatabaseOk() 44 | { 45 | //SETUP 46 | var options = this.CreateUniqueClassOptions(); 47 | var filepath = TestData.GetFilePath("Script02 - Add two rows to Authors table.sql"); 48 | using (var context = new BookContext(options)) 49 | { 50 | context.Database.EnsureClean(); 51 | 52 | //ATTEMPT 53 | context.ExecuteScriptFileInTransaction(filepath); 54 | 55 | //VERIFY 56 | context.Authors.Count().ShouldEqual(2); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDataLayer/TestDisconnectedState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer.BookApp; 7 | using DataLayer.BookApp.EfCode; 8 | using Test.Helpers; 9 | using TestSupport.EfHelpers; 10 | using Xunit; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests.TestDataLayer 14 | { 15 | public class TestDisconnectedState 16 | { 17 | [Fact] 18 | public void TestSqliteSingleInstanceOk() 19 | { 20 | //SETUP 21 | var options = SqliteInMemory 22 | .CreateOptions(); 23 | using (var context = new BookContext(options)) 24 | { 25 | context.Database.EnsureCreated(); //#A 26 | context.SeedDatabaseFourBooks(); //#A 27 | 28 | //ATTEMPT 29 | var book = context.Books.OrderByDescending(x => x.BookId).First(); //#B 30 | book.Reviews.Add( new Review{NumStars = 5});//#C 31 | context.SaveChanges(); //#D 32 | 33 | //VERIFY 34 | context.Books.OrderByDescending(x => x.BookId).First().Reviews 35 | .Count.ShouldEqual(3); //#E 36 | } 37 | } 38 | /********************************************************* 39 | #A I set up the test database with some test data consisting of four books 40 | #B I read in the last book from my test set, which I know has two reviews 41 | #C I add another review to the book. THIS SHOULDN'T WORK, but it does because the seed data is still being tracked by the DbContext instance 42 | #D And save it to the database 43 | #E I check that I have three reviews, which works, but the unit test should have failed with an exception earlier. 44 | * *******************************************************/ 45 | 46 | [Fact] 47 | public void TestSqliteTwoInstancesOk() 48 | { 49 | //SETUP 50 | var options = SqliteInMemory.CreateOptions(); 51 | options.StopNextDispose(); 52 | using (var context = new BookContext(options))//#B 53 | { 54 | context.Database.EnsureCreated(); 55 | context.SeedDatabaseFourBooks(); //#C 56 | } 57 | using (var context = new BookContext(options))//#D 58 | { 59 | //ATTEMPT 60 | var book = context.Books.OrderByDescending(x => x.BookId).First(); //#E 61 | var ex = Assert.Throws( //#F 62 | () => book.Reviews.Add( //#F 63 | new Review { NumStars = 5 })); //#F 64 | 65 | //VERIFY 66 | ex.Message.ShouldStartWith("Object reference not set to an instance of an object."); 67 | } 68 | } 69 | 70 | /************************************************************* 71 | #A I create the in-memory sqlite options in the same way as the last example 72 | #B I create the first instance of the application's DbContext 73 | #C I set up the test database with some test data consisting of four books, but this time in a separate DbContext instance 74 | #D I close that last instance and open a new instance of the application's DbContext. This means that the new instance does not have any tracked entities which could alter how the test runs 75 | #E I read in the last book from my test set, which I know has two reviews 76 | #F When I try to add the new Review the EF Core will throw a NullReferenceException 77 | * ***********************************************************/ 78 | } 79 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDataLayer/TestPostgreSqlHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DataLayer.BookApp.EfCode; 7 | using Microsoft.EntityFrameworkCore; 8 | using Npgsql; 9 | using Test.Helpers; 10 | using TestSupport.Attributes; 11 | using TestSupport.EfHelpers; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | using Xunit.Extensions.AssertExtensions; 15 | 16 | namespace Test.UnitTests.TestDataLayer 17 | { 18 | public class TestPostgreSqlHelpers 19 | { 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestPostgreSqlHelpers(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | } 26 | 27 | [Fact] 28 | public void TestPostgreSqlUniqueClassOk() 29 | { 30 | //SETUP 31 | //ATTEMPT 32 | var options = this.CreatePostgreSqlUniqueClassOptions(); 33 | using (var context = new BookContext(options)) 34 | { 35 | //VERIFY 36 | var builder = new NpgsqlConnectionStringBuilder(context.Database.GetDbConnection().ConnectionString); 37 | _output.WriteLine(builder.Database); 38 | builder.Database.ShouldEndWith(GetType().Name); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void TestPostgreSqUniqueMethodOk() 44 | { 45 | //SETUP 46 | //ATTEMPT 47 | var options = this.CreatePostgreSqlUniqueMethodOptions(); 48 | using (var context = new BookContext(options)) 49 | { 50 | 51 | //VERIFY 52 | var builder = new NpgsqlConnectionStringBuilder(context.Database.GetDbConnection().ConnectionString); 53 | builder.Database 54 | .ShouldEndWith($"{GetType().Name}_{nameof(TestPostgreSqUniqueMethodOk)}" ); 55 | } 56 | } 57 | 58 | [Fact] 59 | public void TestEnsureDeletedEnsureCreatedOk() 60 | { 61 | //SETUP 62 | var options = this.CreatePostgreSqlUniqueClassOptions(); 63 | using var context = new BookContext(options); 64 | 65 | context.Database.EnsureCreated(); 66 | context.SeedDatabaseFourBooks(); 67 | 68 | //ATTEMPT 69 | using (new TimeThings(_output, "Time to EnsureDeleted and EnsureCreated")) 70 | { 71 | context.Database.EnsureDeleted(); 72 | context.Database.EnsureCreated(); 73 | } 74 | 75 | //VERIFY 76 | context.Books.Count().ShouldEqual(0); 77 | } 78 | 79 | [Fact] 80 | public void TestEnsureCreatedExistingDbOk() 81 | { 82 | //SETUP 83 | var options = this.CreatePostgreSqlUniqueClassOptions(); 84 | using var context = new BookContext(options); 85 | 86 | context.Database.EnsureCreated(); 87 | 88 | //ATTEMPT 89 | using (new TimeThings(_output, "EnsureCreated when database exists")) 90 | { 91 | context.Database.EnsureCreated(); 92 | } 93 | 94 | //VERIFY 95 | } 96 | 97 | [Fact] 98 | public void TestEnsureCleanExistingDatabaseOk() 99 | { 100 | //SETUP 101 | var options = this.CreatePostgreSqlUniqueClassOptions(); 102 | using var context = new BookContext(options); 103 | 104 | context.Database.EnsureCreated(); 105 | context.SeedDatabaseFourBooks(); 106 | 107 | //ATTEMPT 108 | using (new TimeThings(_output, "Time to EnsureClean")) 109 | { 110 | context.Database.EnsureClean(); 111 | } 112 | 113 | //VERIFY 114 | context.Books.Count().ShouldEqual(0); 115 | } 116 | 117 | [RunnableInDebugOnly] 118 | public void TestCreatePostgreSqlUniqueClassOptionsWithLogToOk() 119 | { 120 | //SETUP 121 | var logs = new List(); 122 | var options = this.CreatePostgreSqlUniqueClassOptionsWithLogTo(log => logs.Add(log)); 123 | using (var context = new BookContext(options)) 124 | { 125 | //ATTEMPT 126 | context.Database.EnsureDeleted(); 127 | context.Database.EnsureCreated(); 128 | 129 | //VERIFY 130 | foreach (var log in logs) 131 | { 132 | _output.WriteLine(log); 133 | } 134 | } 135 | } 136 | 137 | [Fact] 138 | public void TestAddExtraBuilderOptions() 139 | { 140 | //SETUP 141 | var options1 = this.CreatePostgreSqlUniqueClassOptions(); 142 | using (var context = new BookContext(options1)) 143 | { 144 | context.Database.EnsureCreated(); 145 | context.SeedDatabaseDummyBooks(100); 146 | 147 | var book = context.Books.First(); 148 | context.Entry(book).State.ShouldEqual(EntityState.Unchanged); 149 | } 150 | //ATTEMPT 151 | var options2 = this.CreatePostgreSqlUniqueClassOptions( 152 | builder => builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); 153 | using (var context = new BookContext(options2)) 154 | { 155 | //VERIFY 156 | var book = context.Books.First(); 157 | context.Entry(book).State.ShouldEqual(EntityState.Detached); 158 | } 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDataLayer/TestSqlServerHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using DataLayer.BookApp.EfCode; 7 | using Microsoft.Data.SqlClient; 8 | using Microsoft.EntityFrameworkCore; 9 | using Test.Helpers; 10 | using TestSupport.Attributes; 11 | using TestSupport.EfHelpers; 12 | using Xunit; 13 | using Xunit.Abstractions; 14 | using Xunit.Extensions.AssertExtensions; 15 | 16 | namespace Test.UnitTests.TestDataLayer 17 | { 18 | public class TestSqlServerHelpers 19 | { 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestSqlServerHelpers(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | } 26 | 27 | [Fact] 28 | public void TestSqlDatabaseEnsureCleanOk() 29 | { 30 | //SETUP 31 | var options = this.CreateUniqueClassOptions(); 32 | using var context = new BookContext(options); 33 | 34 | context.Database.EnsureClean(); 35 | 36 | //ATTEMPT 37 | context.SeedDatabaseFourBooks(); 38 | 39 | //VERIFY 40 | context.Books.Count().ShouldEqual(4); 41 | } 42 | 43 | [Fact] 44 | public void TestEnsureDeletedEnsureCreatedOk() 45 | { 46 | //SETUP 47 | var options = this.CreateUniqueClassOptions(); 48 | using var context = new BookContext(options); 49 | 50 | context.Database.EnsureDeleted(); 51 | context.Database.EnsureCreated(); 52 | 53 | //ATTEMPT 54 | context.SeedDatabaseFourBooks(); 55 | 56 | //VERIFY 57 | context.Books.Count().ShouldEqual(4); 58 | } 59 | 60 | [Fact] 61 | public void TestSqlServerUniqueClassOk() 62 | { 63 | //SETUP 64 | //ATTEMPT 65 | var options = this.CreateUniqueClassOptions(); 66 | using (var context = new BookContext(options)) 67 | { 68 | //VERIFY 69 | var builder = new SqlConnectionStringBuilder(context.Database.GetDbConnection().ConnectionString); 70 | builder.InitialCatalog.ShouldEndWith(GetType().Name); 71 | } 72 | } 73 | 74 | [Fact] 75 | public void TestSqlServerUniqueMethodOk() 76 | { 77 | //SETUP 78 | //ATTEMPT 79 | var options = this.CreateUniqueMethodOptions(); 80 | using (var context = new BookContext(options)) 81 | { 82 | 83 | //VERIFY 84 | var builder = new SqlConnectionStringBuilder(context.Database.GetDbConnection().ConnectionString); 85 | builder.InitialCatalog 86 | .ShouldEndWith($"{GetType().Name}_{nameof(TestSqlServerUniqueMethodOk)}" ); 87 | } 88 | } 89 | 90 | [Fact] 91 | public void TestCreateEmptyViaDeleteOk() 92 | { 93 | //SETUP 94 | var options = this.CreateUniqueMethodOptions(); 95 | using (var context = new BookContext(options)) 96 | { 97 | context.Database.EnsureCreated(); 98 | context.SeedDatabaseFourBooks(); 99 | } 100 | using (var context = new BookContext(options)) 101 | { 102 | //ATTEMPT 103 | using (new TimeThings(_output, "Time to delete and create the database")) 104 | { 105 | context.CreateEmptyViaDelete(); 106 | } 107 | 108 | //VERIFY 109 | context.Books.Count().ShouldEqual(0); 110 | } 111 | } 112 | 113 | [RunnableInDebugOnly] 114 | public void TestCreateDbToGetLogsOk() 115 | { 116 | //SETUP 117 | var logs = new List(); 118 | var options = this.CreateUniqueClassOptionsWithLogTo(log => logs.Add(log)); 119 | using (var context = new BookContext(options)) 120 | { 121 | //ATTEMPT 122 | context.Database.EnsureDeleted(); 123 | context.Database.EnsureCreated(); 124 | 125 | //VERIFY 126 | foreach (var log in logs) 127 | { 128 | _output.WriteLine(log); 129 | } 130 | } 131 | } 132 | 133 | [Fact] 134 | public void TestAddExtraBuilderOptions() 135 | { 136 | //SETUP 137 | var options1 = this.CreateUniqueMethodOptions(); 138 | using (var context = new BookContext(options1)) 139 | { 140 | context.Database.EnsureCreated(); 141 | context.SeedDatabaseDummyBooks(100); 142 | 143 | var book = context.Books.First(); 144 | context.Entry(book).State.ShouldEqual(EntityState.Unchanged); 145 | } 146 | //ATTEMPT 147 | var options2 = this.CreateUniqueMethodOptions( 148 | builder => builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); 149 | using (var context = new BookContext(options2)) 150 | { 151 | //VERIFY 152 | var book = context.Books.First(); 153 | context.Entry(book).State.ShouldEqual(EntityState.Detached); 154 | 155 | } 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDataResetter/TestResetKeysSingleEntity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT licence. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using DataLayer.BookApp; 7 | using DataLayer.BookApp.EfCode; 8 | using DataLayer.SpecialisedEntities; 9 | using Microsoft.EntityFrameworkCore; 10 | using Test.Helpers; 11 | using TestSupport.EfHelpers; 12 | using TestSupport.SeedDatabase; 13 | using Xunit; 14 | using Xunit.Abstractions; 15 | using Xunit.Extensions.AssertExtensions; 16 | 17 | namespace Test.UnitTests.TestDataResetter 18 | { 19 | public class TestResetKeysSingleEntity 20 | { 21 | private readonly ITestOutputHelper _output; 22 | 23 | public TestResetKeysSingleEntity(ITestOutputHelper output) 24 | { 25 | _output = output; 26 | } 27 | 28 | [Fact] 29 | public void TestResetKeysSingleEntityPkOnly() 30 | { 31 | //SETUP 32 | var options = SqliteInMemory.CreateOptions(); 33 | using (var context = new BookContext(options)) 34 | { 35 | context.Database.EnsureCreated(); 36 | context.SeedDatabaseFourBooks(); 37 | var entity = context.Books.First(); 38 | 39 | //ATTEMPT 40 | var resetter = new DataResetter(context); 41 | resetter.ResetKeysSingleEntity(entity); 42 | 43 | //VERIFY 44 | entity.BookId.ShouldEqual(0); 45 | } 46 | } 47 | 48 | [Fact] 49 | public void TestResetKeysSingleEntityPkAndForeignKey() 50 | { 51 | //SETUP 52 | var options = SqliteInMemory.CreateOptions(); 53 | using (var context = new BookContext(options)) 54 | { 55 | context.Database.EnsureCreated(); 56 | context.SeedDatabaseFourBooks(); 57 | var entity = context.Set().First(); 58 | 59 | //ATTEMPT 60 | var resetter = new DataResetter(context); 61 | resetter.ResetKeysSingleEntity(entity); 62 | 63 | //VERIFY 64 | entity.ReviewId.ShouldEqual(0); 65 | entity.BookId.ShouldEqual(0); 66 | } 67 | } 68 | 69 | [Fact] 70 | public void TestResetKeysSingleEntityPrivateSetter() 71 | { 72 | //SETUP 73 | var options = SqliteInMemory.CreateOptions(); 74 | using (var context = new SpecializedDbContext(options)) 75 | { 76 | var entity = new AllTypesEntity(); 77 | entity.SetId(123); 78 | 79 | //ATTEMPT 80 | var resetter = new DataResetter(context); 81 | resetter.ResetKeysSingleEntity(entity); 82 | 83 | //VERIFY 84 | entity.Id.ShouldEqual(0); 85 | } 86 | } 87 | 88 | [Fact] 89 | public void TestResetKeysSingleEntityAlternative() 90 | { 91 | //SETUP 92 | var options = SqliteInMemory.CreateOptions(); 93 | using (var context = new OwnedWithKeyDbContext(options)) 94 | { 95 | var entity = new User 96 | { 97 | UserId = 123, 98 | Email = "Hello" 99 | }; 100 | 101 | //ATTEMPT 102 | var resetter = new DataResetter(context); 103 | resetter.ResetKeysSingleEntity(entity); 104 | 105 | //VERIFY 106 | entity.UserId.ShouldEqual(0); 107 | entity.Email.ShouldBeNull(); 108 | } 109 | } 110 | 111 | [Fact] 112 | public void TestResetKeysSingleEntityAlternativeNotReset() 113 | { 114 | //SETUP 115 | var options = SqliteInMemory.CreateOptions(); 116 | using (var context = new OwnedWithKeyDbContext(options)) 117 | { 118 | var entity = new User 119 | { 120 | UserId = 123, 121 | Email = "Hello" 122 | }; 123 | 124 | //ATTEMPT 125 | var config = new DataResetterConfig {DoNotResetAlternativeKey = true}; 126 | var resetter = new DataResetter(context, config); 127 | resetter.ResetKeysSingleEntity(entity); 128 | 129 | //VERIFY 130 | entity.UserId.ShouldEqual(0); 131 | entity.Email.ShouldEqual("Hello"); 132 | } 133 | } 134 | 135 | [Fact] 136 | public void TestResetKeysSingleEntityNonEntityClassBad() 137 | { 138 | //SETUP 139 | var options = SqliteInMemory.CreateOptions(); 140 | using (var context = new BookContext(options)) 141 | { 142 | var entity = new User(); 143 | 144 | //ATTEMPT 145 | var resetter = new DataResetter(context); 146 | var ex = Assert.Throws(() => resetter.ResetKeysSingleEntity(entity)); 147 | 148 | //VERIFY 149 | ex.Message.ShouldEqual("The class User is not a class that the provided DbContext knows about."); 150 | } 151 | } 152 | 153 | } 154 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestSupport/TestFileData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using TestSupport.Helpers; 6 | using Xunit; 7 | using Xunit.Extensions.AssertExtensions; 8 | 9 | namespace Test.UnitTests.TestSupport 10 | { 11 | public class TestFileData 12 | { 13 | [Fact] 14 | public void TestGetCallerTopLevelDirectory() 15 | { 16 | //SETUP 17 | 18 | //ATTEMPT 19 | var path = TestData.GetCallingAssemblyTopLevelDir(); 20 | 21 | //VERIFY 22 | path.ShouldEndWith(GetType().Namespace.Split('.').First()); 23 | } 24 | 25 | 26 | [Fact] 27 | public void TestGetTestDataFileDirectory() 28 | { 29 | //SETUP 30 | 31 | //ATTEMPT 32 | var path = TestData.GetTestDataDir(); 33 | 34 | //VERIFY 35 | path.ShouldEndWith(GetType().Namespace.Split('.').First() + "\\TestData"); 36 | } 37 | 38 | [Fact] 39 | public void TestGetTestDataDummyFilePath() 40 | { 41 | //SETUP 42 | 43 | //ATTEMPT 44 | var path = TestData.GetFilePath("Dummy*.txt"); 45 | 46 | //VERIFY 47 | path.ShouldEndWith("\\TestData\\Dummy file.txt"); 48 | } 49 | 50 | [Fact] 51 | public void TestGetTestDataDummyFilePathSubDirectory() 52 | { 53 | //SETUP 54 | 55 | //ATTEMPT 56 | var path = TestData.GetFilePath(@"SubDirWithOneFileInIt\One file.txt"); 57 | 58 | //VERIFY 59 | path.ShouldEndWith(@"SubDirWithOneFileInIt\One file.txt"); 60 | } 61 | 62 | [Fact] 63 | public void TestGetTestDataDummyFileAltTestDataDir() 64 | { 65 | //SETUP 66 | 67 | //ATTEMPT 68 | var path = TestData.GetFilePath(@"\AltTestDataDir\Alt dummy file.txt"); 69 | 70 | //VERIFY 71 | path.ShouldEndWith(@"\AltTestDataDir\Alt dummy file.txt"); 72 | } 73 | 74 | 75 | [Fact] 76 | public void TestGetTestDataAllFilesInDir() 77 | { 78 | //SETUP 79 | 80 | //ATTEMPT 81 | var filePaths = TestData.GetFilePaths(@"*.*"); 82 | 83 | //VERIFY 84 | filePaths.Length.ShouldNotEqual(0); 85 | } 86 | 87 | [Fact] 88 | public void TestGetTestDataDummyFileContext() 89 | { 90 | //SETUP 91 | 92 | //ATTEMPT 93 | var content = TestData.GetFileContent("Dummy*.txt"); 94 | 95 | //VERIFY 96 | content.ShouldEqual("This is the content of the dummy file"); 97 | } 98 | 99 | [Fact] 100 | public void TestGetTestDataFileDirectoryWithRedirect() 101 | { 102 | //SETUP 103 | 104 | //ATTEMPT 105 | var path = TestData.GetTestDataDir("..\\TestSupport"); 106 | 107 | //VERIFY 108 | path.ShouldEndWith("\\TestSupport"); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Test/UnitTests/TestSupport/TestTimeThings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading; 5 | using TestSupport.EfHelpers; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | using Xunit.Extensions.AssertExtensions; 9 | 10 | namespace Test.UnitTests.TestSupport; 11 | 12 | public class TestTimeThings(ITestOutputHelper output) 13 | { 14 | private ITestOutputHelper _output = output; 15 | 16 | [Fact] 17 | public void TestNoSettings() 18 | { 19 | //SETUP 20 | var mock = new MockOutput(); 21 | 22 | //ATTEMPT 23 | using (new TimeThings(mock)) 24 | { 25 | Thread.Sleep(10); 26 | } 27 | 28 | //VERIFY 29 | mock.LastWriteLine.ShouldStartWith(" took "); 30 | mock.LastWriteLine.ShouldEndWith("ms."); 31 | } 32 | 33 | [Fact] 34 | public void TestMessage() 35 | { 36 | //SETUP 37 | var mock = new MockOutput(); 38 | 39 | //ATTEMPT 40 | using (new TimeThings(mock, "This message")) 41 | { 42 | Thread.Sleep(10); 43 | } 44 | 45 | //VERIFY 46 | mock.LastWriteLine.ShouldStartWith("This message took "); 47 | mock.LastWriteLine.ShouldEndWith("ms."); 48 | } 49 | 50 | [Theory] 51 | [InlineData(10)] 52 | [InlineData(100)] 53 | [InlineData(1000)] 54 | public void TestTime(int milliseconds) 55 | { 56 | //SETUP 57 | var mock = new MockOutput(); 58 | 59 | //ATTEMPT 60 | using (new TimeThings(mock)) 61 | { 62 | Thread.Sleep(milliseconds); 63 | } 64 | 65 | //VERIFY 66 | _output.WriteLine($"{milliseconds}: {mock.LastWriteLine}"); 67 | } 68 | 69 | [Fact] 70 | public void TestMessageAndNumRuns() 71 | { 72 | //SETUP 73 | var mock = new MockOutput(); 74 | 75 | //ATTEMPT 76 | using (new TimeThings(mock, "This message", 500)) 77 | { 78 | Thread.Sleep(10); 79 | } 80 | 81 | //VERIFY 82 | mock.LastWriteLine.ShouldStartWith("500 x This message took "); 83 | mock.LastWriteLine.ShouldEndWith("us."); 84 | mock.LastWriteLine.ShouldContain(", ave. per run = "); 85 | } 86 | 87 | [Fact] 88 | public void TestTimeThingResultReturn() 89 | { 90 | //SETUP 91 | TimeThingResult result = null; 92 | 93 | //ATTEMPT 94 | using (new TimeThings(x => result = x, "This message", 10)) 95 | { 96 | Thread.Sleep(10); 97 | } 98 | 99 | //VERIFY 100 | result.Message.ShouldEqual("This message"); 101 | result.NumRuns.ShouldEqual(10); 102 | result.TotalTimeMilliseconds.ShouldBeInRange(10, 50); 103 | } 104 | 105 | [Fact] 106 | public void TestHowLongTimeThings() 107 | { 108 | //SETUP 109 | TimeThingResult result = null; 110 | 111 | //ATTEMPT 112 | using (new TimeThings(output, "TimeThings", 2)) 113 | { 114 | using (new TimeThings(x => result = x)) 115 | { 116 | 117 | } 118 | } 119 | 120 | //VERIFY 121 | } 122 | 123 | [Fact] 124 | public void TestTimeThingsMany() 125 | { 126 | //SETUP 127 | TimeThingResult result = null; 128 | //_output.WriteLine("warm up _output"); 129 | 130 | //ATTEMPT 131 | using (new TimeThings(output, "TimeThings direct 1")) 132 | { 133 | Thread.Sleep(10); 134 | } 135 | using (new TimeThings(output, "TimeThings direct 2")) 136 | { 137 | Thread.Sleep(10); 138 | } 139 | using (new TimeThings(x => result = x, "TimeThings redirect 1")) 140 | { 141 | Thread.Sleep(10); 142 | } 143 | _output.WriteLine(result.ToString()); 144 | using (new TimeThings(x => result = x, "TimeThings redirect 2")) 145 | { 146 | Thread.Sleep(10); 147 | } 148 | _output.WriteLine(result.ToString()); 149 | 150 | //VERIFY 151 | } 152 | 153 | 154 | private class MockOutput : ITestOutputHelper 155 | { 156 | public string LastWriteLine { get; private set; } 157 | 158 | public void WriteLine(string message) 159 | { 160 | LastWriteLine = message; 161 | } 162 | 163 | public void WriteLine(string format, params object[] args) 164 | { 165 | throw new System.NotImplementedException(); 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestSupport/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Xunit; 5 | using Xunit.Extensions.AssertExtensions; 6 | 7 | namespace Test.UnitTests.TestSupport 8 | { 9 | public class UnitTest1 10 | { 11 | [Fact] //#A 12 | public void DemoTest() //#B 13 | { 14 | //SETUP 15 | const int someValue = 1; //#C 16 | 17 | //ATTEMPT 18 | var result = someValue * 2; //#D 19 | 20 | //VERIFY 21 | result.ShouldEqual(2); //#E 22 | } 23 | 24 | /***************************************************** 25 | #A The [Fact] attribute tells the unit test runner that this method is an xUnit unit test that should be run 26 | #B The method must be public. It should return void, or if you are running async methods, then it should return "async Task" 27 | #C Typically you put code here that sets up the data and/or environment for the unit test 28 | #D This is where you run the code you want to test 29 | #E And here is where you put the test(s) to check that the result of your test is correct 30 | * ***************************************************/ 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=EfCore.TestSupport-Test;Trusted_Connection=True;MultipleActiveResultSets=true", 4 | "PostgreSqlConnection": "Host=127.0.0.1;Port=5432;Database=Test-Test;Username=postgres;Password=LetMeIn", 5 | "BookOrderConnection": "Data Source=(localdb)\\mssqllocaldb;Initial Catalog=EfCore.TestSupport-Test_ComparerBooksAndOrders;Integrated Security=True;MultipleActiveResultSets=True" 6 | }, 7 | "MyInt": 1, 8 | "MyObject": { 9 | "MyInnerInt": 2 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /TestFromSqlRaw/DbContext1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace TestFromSqlRaw 7 | { 8 | public class MyDbContext : DbContext 9 | { 10 | public MyDbContext(DbContextOptions options) 11 | : base(options) { } 12 | 13 | public DbSet MyEntities { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /TestFromSqlRaw/MyEntity.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace TestFromSqlRaw 3 | { 4 | public class MyEntity 5 | { 6 | public int Id { get; set; } 7 | public string Name { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TestFromSqlRaw/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using TestSupport.EfHelpers; 3 | 4 | namespace TestFromSqlRaw 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var options = SqliteInMemory.CreateOptions(); 11 | var context = new MyDbContext(options); 12 | 13 | //This shows that making the Microsoft.EntityFrameworkCore.Cosmos NuGet package private 14 | //to the TestSupport project removes the compile-time error "The call is ambiguous..." - SEE BELOW 15 | // 16 | //Code CS0121: The call is ambiguous between the following methods or properties: 17 | //'Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.FromSqlRaw(Microsoft.EntityFrameworkCore.DbSet, string, params object[])' and 18 | //'Microsoft.EntityFrameworkCore.CosmosQueryableExtensions.FromSqlRaw(Microsoft.EntityFrameworkCore.DbSet, string, params object[])' 19 | 20 | context.MyEntities.FromSqlRaw("Select * FROM MyEntities"); 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /TestFromSqlRaw/TestFromSqlRaw.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TestSupport/Assert.Extensions/BooleanAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | #pragma warning disable 1574,1584,1581,1580 5 | namespace Xunit.Extensions.AssertExtensions 6 | { 7 | /// 8 | /// Extensions which provide assertions to classes derived from . 9 | /// 10 | public static class BooleanAssertionExtensions 11 | { 12 | /// 13 | /// Verifies that the condition is false. 14 | /// 15 | /// The condition to be tested 16 | /// Thrown if the condition is not false 17 | public static void ShouldBeFalse(this bool condition) 18 | { 19 | Assert.False(condition); 20 | } 21 | 22 | /// 23 | /// Verifies that the condition is false. 24 | /// 25 | /// The condition to be tested 26 | /// The message to show when the condition is not false 27 | /// Thrown if the condition is not false 28 | public static void ShouldBeFalse(this bool condition, 29 | string userMessage) 30 | { 31 | Assert.False(condition, userMessage); 32 | } 33 | 34 | /// 35 | /// Verifies that an expression is true. 36 | /// 37 | /// The condition to be inspected 38 | /// Thrown when the condition is false 39 | public static void ShouldBeTrue(this bool condition) 40 | { 41 | Assert.True(condition); 42 | } 43 | 44 | /// 45 | /// Verifies that an expression is true. 46 | /// 47 | /// The condition to be inspected 48 | /// The message to be shown when the condition is false 49 | /// Thrown when the condition is false 50 | public static void ShouldBeTrue(this bool condition, 51 | string userMessage) 52 | { 53 | Assert.True(condition, userMessage); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /TestSupport/Assert.Extensions/CollectionAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | #pragma warning disable 1574,1584,1581,1580 8 | 9 | namespace Xunit.Extensions.AssertExtensions 10 | { 11 | /// 12 | /// Extensions which provide assertions to classes derived from and . 13 | /// 14 | public static class CollectionAssertExtensions 15 | { 16 | /// 17 | /// Verifies that a collection is empty. 18 | /// 19 | /// The collection to be inspected 20 | /// Thrown when the collection is null 21 | /// Thrown when the collection is not empty 22 | public static void ShouldBeEmpty(this IEnumerable collection) 23 | { 24 | Assert.Empty(collection); 25 | } 26 | 27 | /// 28 | /// Verifies that a collection contains a given object. 29 | /// 30 | /// The type of the object to be verified 31 | /// The collection to be inspected 32 | /// The object expected to be in the collection 33 | /// Thrown when the object is not present in the collection 34 | public static void ShouldContain(this IEnumerable collection, 35 | T expected) 36 | { 37 | Assert.Contains(expected, collection); 38 | } 39 | 40 | /// 41 | /// Verifies that a collection contains a given object, using a comparer. 42 | /// 43 | /// The type of the object to be verified 44 | /// The collection to be inspected 45 | /// The object expected to be in the collection 46 | /// The comparer used to equate objects in the collection with the expected object 47 | /// Thrown when the object is not present in the collection 48 | public static void ShouldContain(this IEnumerable collection, 49 | T expected, 50 | IEqualityComparer comparer) 51 | { 52 | Assert.Contains(expected, collection, comparer); 53 | } 54 | 55 | /// 56 | /// Verifies that a collection is not empty. 57 | /// 58 | /// The collection to be inspected 59 | /// Thrown when a null collection is passed 60 | /// Thrown when the collection is empty 61 | public static void ShouldNotBeEmpty(this IEnumerable collection) 62 | { 63 | Assert.NotEmpty(collection); 64 | } 65 | 66 | /// 67 | /// Verifies that a collection does not contain a given object. 68 | /// 69 | /// The type of the object to be compared 70 | /// The object that is expected not to be in the collection 71 | /// The collection to be inspected 72 | /// Thrown when the object is present inside the container 73 | public static void ShouldNotContain(this IEnumerable collection, 74 | T expected) 75 | { 76 | Assert.DoesNotContain(expected, collection); 77 | } 78 | 79 | /// 80 | /// Verifies that a collection does not contain a given object, using a comparer. 81 | /// 82 | /// The type of the object to be compared 83 | /// The object that is expected not to be in the collection 84 | /// The collection to be inspected 85 | /// The comparer used to equate objects in the collection with the expected object 86 | /// Thrown when the object is present inside the container 87 | public static void ShouldNotContain(this IEnumerable collection, 88 | T expected, 89 | IEqualityComparer comparer) 90 | { 91 | Assert.DoesNotContain(expected, collection, comparer); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /TestSupport/Assert.Extensions/ExtraStringAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | #pragma warning disable 1591 5 | namespace Xunit.Extensions.AssertExtensions 6 | { 7 | /// 8 | /// Extra AssertExtensions that I find useful 9 | /// 10 | public static class ExtraStringAssertionExtensions 11 | { 12 | 13 | public static void ShouldStartWith(this string actualString, 14 | string expectedStartString) 15 | { 16 | Assert.StartsWith(expectedStartString, actualString); 17 | } 18 | 19 | public static void ShouldEndWith(this string actualString, 20 | string expectedEndString) 21 | { 22 | Assert.EndsWith(expectedEndString, actualString); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /TestSupport/Assert.Extensions/StringAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | #pragma warning disable 1574,1584,1581,1580 6 | 7 | namespace Xunit.Extensions.AssertExtensions 8 | { 9 | /// 10 | /// Extensions which provide assertions to classes derived from . 11 | /// 12 | public static class StringAssertionExtensions 13 | { 14 | /// 15 | /// Verifies that a string contains a given sub-string, using the current culture. 16 | /// 17 | /// The string to be inspected 18 | /// The sub-string expected to be in the string 19 | /// Thrown when the sub-string is not present inside the string 20 | public static void ShouldContain(this string actualString, 21 | string expectedSubString) 22 | { 23 | Assert.Contains(expectedSubString, actualString); 24 | } 25 | 26 | /// 27 | /// Verifies that a string contains a given sub-string, using the given comparison type. 28 | /// 29 | /// The string to be inspected 30 | /// The sub-string expected to be in the string 31 | /// The type of string comparison to perform 32 | /// Thrown when the sub-string is not present inside the string 33 | public static void ShouldContain(this string actualString, 34 | string expectedSubString, 35 | StringComparison comparisonType) 36 | { 37 | Assert.Contains(expectedSubString, actualString, comparisonType); 38 | } 39 | 40 | /// 41 | /// Verifies that a string does not contain a given sub-string, using the current culture. 42 | /// 43 | /// The string to be inspected 44 | /// The sub-string which is expected not to be in the string 45 | /// Thrown when the sub-string is present inside the string 46 | public static void ShouldNotContain(this string actualString, 47 | string expectedSubString) 48 | { 49 | Assert.DoesNotContain(expectedSubString, actualString); 50 | } 51 | 52 | /// 53 | /// Verifies that a string does not contain a given sub-string, using the current culture. 54 | /// 55 | /// The string to be inspected 56 | /// The sub-string which is expected not to be in the string 57 | /// The type of string comparison to perform 58 | /// Thrown when the sub-string is present inside the given string 59 | public static void ShouldNotContain(this string actualString, 60 | string expectedSubString, 61 | StringComparison comparisonType) 62 | { 63 | Assert.DoesNotContain(expectedSubString, actualString, comparisonType); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /TestSupport/Attributes/RunnableInDebugOnlyAttribute .cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Diagnostics; 5 | using Xunit; 6 | 7 | namespace TestSupport.Attributes 8 | { 9 | /// 10 | /// Useful attribute for stopping a test from being run 11 | /// see https://lostechies.com/jimmybogard/2013/06/20/run-tests-explicitly-in-xunit-net/ 12 | /// 13 | public class RunnableInDebugOnlyAttribute : FactAttribute 14 | { 15 | /// 16 | /// By putting this attribute on a test instead of the normal [Fact] attribute will mean the 17 | /// test will only run if in debug mode. 18 | /// This is useful for stopping unit tests that should not be run in the normal run of unit tests 19 | /// 20 | public RunnableInDebugOnlyAttribute() 21 | { 22 | if (!Debugger.IsAttached) 23 | { 24 | Skip = "Only running in interactive mode."; 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /TestSupport/EfCoreTestSupportNuGetIcon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/74ce78237c8887de93a5cc0d2e8cf394463cf3d1/TestSupport/EfCoreTestSupportNuGetIcon128.png -------------------------------------------------------------------------------- /TestSupport/EfHelpers/ApplyScriptExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Data; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text.RegularExpressions; 8 | using Microsoft.Data.SqlClient; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace TestSupport.EfHelpers 12 | { 13 | /// 14 | /// Static class holding extension methods for applying SQL scripts to a database 15 | /// 16 | public static class ApplyScriptExtension 17 | { 18 | /// 19 | /// This reads in a SQL script file and executes each command to the database pointed at by the DbContext 20 | /// Each command should have an GO at the start of the line after the command. 21 | /// 22 | /// 23 | /// 24 | public static void ExecuteScriptFileInTransaction(this DbContext context, string filePath) 25 | { 26 | var scriptContent = File.ReadAllText(filePath); 27 | var regex = new Regex("^GO", RegexOptions.IgnoreCase | RegexOptions.Multiline); 28 | var commands = regex.Split(scriptContent).Select(x => x.Trim()); 29 | 30 | using (var transaction = context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) 31 | { 32 | foreach (var command in commands) 33 | { 34 | if (command.Length > 0) 35 | { 36 | try 37 | { 38 | context.Database.ExecuteSqlRaw(command); 39 | } 40 | catch (SqlException) 41 | { 42 | transaction.Rollback(); 43 | throw; 44 | } 45 | } 46 | } 47 | transaction.Commit(); 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/CleanDatabaseExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information.. 3 | 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | 6 | namespace TestSupport.EfHelpers 7 | { 8 | /// 9 | /// This used to internal EF Core code that was a quick reset of a database. 10 | /// In .NET9 the internal code changed, and I decided to go back to the normal 11 | /// EnsureDeleted / EnsureCreated. This means your existing tests will still work. 12 | /// 13 | public static class CleanDatabaseExtensions 14 | { 15 | /// 16 | /// Calling this will call EnsureDeleted and then EnsureCreated. 17 | /// This works with any database supported be EF Core 18 | /// > 19 | /// The Database property of the current DbContext that you want to clean 20 | public static void EnsureClean(this DatabaseFacade databaseFacade) 21 | { 22 | databaseFacade.EnsureDeleted(); 23 | databaseFacade.EnsureCreated(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/DbContextOptionsDisposable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.ObjectModel; 6 | using System.Data.Common; 7 | using System.Linq; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.EntityFrameworkCore.Infrastructure; 10 | 11 | namespace TestSupport.EfHelpers 12 | { 13 | /// 14 | /// This is used to return a class that implements 15 | /// 16 | /// 17 | public class DbContextOptionsDisposable : DbContextOptions, IDisposable where T : DbContext 18 | { 19 | private bool _stopNextDispose; 20 | private bool _turnOffDispose; 21 | private readonly DbConnection _connection; 22 | 23 | /// 24 | /// This creates the class and sets up the part and getting a reference to the connection 25 | /// 26 | /// 27 | public DbContextOptionsDisposable(DbContextOptions baseOptions) 28 | : base(new ReadOnlyDictionary( 29 | baseOptions.Extensions.ToDictionary(x => x.GetType()))) 30 | { 31 | _connection = RelationalOptionsExtension.Extract(baseOptions).Connection; 32 | } 33 | 34 | /// 35 | /// Use this to stop the Dispose if you want to create a second context to the same connection. 36 | /// You should call this BEFORE you create the DbContext 37 | /// 38 | public void StopNextDispose() 39 | { 40 | _stopNextDispose = true; 41 | } 42 | 43 | /// 44 | /// If you have lots of separate application DbContext's then use this to stop the connection from being removed. 45 | /// But remember to call at the end of your unit test 46 | /// 47 | public void TurnOffDispose() 48 | { 49 | _turnOffDispose = true; 50 | } 51 | 52 | /// 53 | /// If you used , then you should call this at the end of your unit test 54 | /// That will dispose the connection. 55 | /// 56 | public void ManualDispose() 57 | { 58 | _turnOffDispose = false; 59 | _stopNextDispose = false; 60 | Dispose(); 61 | } 62 | 63 | /// 64 | /// This disposes the Sqlite connection with holds the in-memory data 65 | /// 66 | public void Dispose() 67 | { 68 | if (!_stopNextDispose && !_turnOffDispose) 69 | _connection.Dispose(); 70 | _stopNextDispose = false; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/Internal/EfCoreLogDecoder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.EntityFrameworkCore.Diagnostics; 7 | 8 | namespace TestSupport.EfHelpers.Internal 9 | { 10 | internal class EfCoreLogDecoder 11 | { 12 | private const string ParameterStart = "[Parameters=["; 13 | 14 | private static readonly Regex ParamRegex = new Regex(@"(@p\d+|@__\w*?_\d+)=('(.*?)'|NULL)(\s\(\w*?\s=\s\w*\))*(?:,\s|\]).*?"); 15 | 16 | private readonly string _paramName; 17 | private readonly string[] _paramTypes; 18 | private readonly string _paramValue; 19 | 20 | private EfCoreLogDecoder(Match matchedParam) 21 | { 22 | _paramName = matchedParam.Groups[1].Value; 23 | _paramValue = matchedParam.Groups[3].Value; 24 | _paramTypes = matchedParam.Groups[4].Captures.Cast().Select(x => x.Value).ToArray(); 25 | } 26 | 27 | private string ValueToInsert() 28 | { 29 | if (_paramValue == string.Empty) 30 | //If no value we assume NULL 31 | //NOTE: this fails with empty string 32 | return "NULL"; 33 | 34 | if (_paramTypes.Any()) 35 | //We assume its something that needs to be a string 36 | //NOTE: This will get byte[] wrong 37 | return $"'{_paramValue.Replace("'","''")}'"; 38 | 39 | if (_paramValue == "True" || _paramValue == "False") 40 | return _paramValue == "True" ? "1" : "0"; 41 | 42 | //NOTE: numbers are presented as a string, but SQL Server handles that OK. 43 | return $"'{_paramValue}'"; 44 | } 45 | 46 | public override string ToString() 47 | { 48 | var paramTypes = string.Join(",", _paramTypes); 49 | return $"{_paramName}={ValueToInsert()}, {paramTypes}"; 50 | } 51 | 52 | /// 53 | /// This will try and decode an EF Core "CommandExecuted" 54 | /// 55 | /// 56 | /// 57 | public static string DecodeMessage(LogOutput log) 58 | { 59 | if (log.EventId.Name != RelationalEventId.CommandError.Name && log.EventId.Name != RelationalEventId.CommandExecuted.Name) 60 | return log.Message; 61 | 62 | var messageLines = log.Message.Split('\n').Select(x => x.Trim()).ToArray(); 63 | var parametersIndex = messageLines[0].IndexOf(ParameterStart); 64 | if (parametersIndex <= 0) 65 | return log.Message; 66 | 67 | var decodedMatches = ParamRegex.Matches(messageLines[0].Substring(parametersIndex + ParameterStart.Length)) 68 | .Cast().Select(x => new EfCoreLogDecoder(x)).ToList(); 69 | //is sensitive logging isn't enabled then all the param values will '?', so we just return the message 70 | if (decodedMatches.All(x => x._paramValue == "?")) 71 | return log.Message; 72 | 73 | decodedMatches.Reverse(); //Need to reverse so that @p10 comes before @p1 74 | 75 | for (int i = 1; i < messageLines.Length; i++) 76 | { 77 | var lineToUpdate = messageLines[i]; 78 | foreach (var param in decodedMatches) 79 | { 80 | lineToUpdate = lineToUpdate.Replace(param._paramName, param.ValueToInsert()); 81 | } 82 | messageLines[i] = lineToUpdate; 83 | } 84 | 85 | return string.Join("\r\n", messageLines); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/Internal/OptionBuilderHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace TestSupport.EfHelpers.Internal 9 | { 10 | 11 | internal static class OptionBuilderHelpers 12 | { 13 | internal static void ApplyOtherOptionSettings(this DbContextOptionsBuilder builder) 14 | where T : DbContext 15 | { 16 | builder 17 | .EnableDetailedErrors() 18 | .EnableSensitiveDataLogging(); 19 | } 20 | 21 | internal static DbContextOptionsBuilder AddLogTo(this DbContextOptionsBuilder builder, Action userAction, LogToOptions logToOptions) 22 | where T : DbContext 23 | { 24 | logToOptions ??= new LogToOptions(); 25 | 26 | Action action = log => 27 | { 28 | if (logToOptions.ShowLog) 29 | userAction(log); 30 | }; 31 | 32 | var usedNames = new List(); 33 | 34 | if (logToOptions.OnlyShowTheseCategories != null) 35 | usedNames.Add(nameof(LogToOptions.OnlyShowTheseCategories)); 36 | if (logToOptions.OnlyShowTheseEvents != null) 37 | usedNames.Add(nameof(LogToOptions.OnlyShowTheseEvents)); 38 | if (logToOptions.FilterFunction != null) 39 | usedNames.Add(nameof(LogToOptions.FilterFunction)); 40 | 41 | if (usedNames.Count > 1) 42 | throw new NotSupportedException($"You can't define {string.Join(" and ", usedNames)} at the same time."); 43 | 44 | if (logToOptions.OnlyShowTheseCategories != null) 45 | return builder.LogTo(action, logToOptions.OnlyShowTheseCategories, logToOptions.LogLevel, logToOptions.LoggerOptions); 46 | if (logToOptions.OnlyShowTheseEvents != null) 47 | return builder.LogTo(action, logToOptions.OnlyShowTheseEvents, logToOptions.LogLevel, logToOptions.LoggerOptions); 48 | if (logToOptions.FilterFunction != null) 49 | return builder.LogTo(action, logToOptions.FilterFunction, logToOptions.LoggerOptions); 50 | 51 | return builder.LogTo(action, logToOptions.LogLevel, logToOptions.LoggerOptions); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/LogOutput.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Linq; 5 | using Microsoft.Extensions.Logging; 6 | using TestSupport.EfHelpers.Internal; 7 | 8 | namespace TestSupport.EfHelpers 9 | { 10 | /// 11 | /// This holds logs produced by the MyLoggerProvider 12 | /// 13 | public class LogOutput 14 | { 15 | private const string EfCoreEventIdStartWith = "Microsoft.EntityFrameworkCore"; 16 | 17 | internal LogOutput(LogLevel logLevel, 18 | EventId eventId, string message) 19 | { 20 | LogLevel = logLevel; 21 | EventId = eventId; 22 | Message = message; 23 | } 24 | 25 | /// 26 | /// The logLevel of this log 27 | /// 28 | public LogLevel LogLevel { get; } 29 | 30 | /// 31 | /// The logging EventId - should be string for EF Core logs 32 | /// 33 | public EventId EventId { get; } 34 | 35 | /// 36 | /// The message in the log 37 | /// 38 | public string Message { get; } 39 | 40 | /// 41 | /// This returns the last part of an EF Core EventId name, or null if the eventId is not an EF Core one 42 | /// 43 | private string EfEventIdLastName => 44 | EventId.Name?.StartsWith( 45 | EfCoreEventIdStartWith) == true 46 | ? EventId.Name.Split('.').Last() 47 | : null; 48 | 49 | /// 50 | /// Summary of the log 51 | /// 52 | /// 53 | public override string ToString() 54 | { 55 | var logType = EfEventIdLastName == null ? "" : "," + EfEventIdLastName; 56 | return $"{LogLevel}{logType}: {Message}"; 57 | } 58 | 59 | /// 60 | /// This tries to build valid SQL commands on CommandExecuted logs, i.e. logs containing the SQL output 61 | /// by taking the values available from EnableSensitiveDataLogging and inserting them in place of the parameter. 62 | /// This makes it easier to copy the SQL produced by EF Core and run in SSMS etc. 63 | /// LIMITATIONS are: 64 | /// - It can't distinguish the different between an empty string and a null string - it default to null 65 | /// - It can't work out if its a byte[] or not, so byte[] is treated as a SQL string, WHICH WILL fail 66 | /// - Numbers are presented as SQL strings, e.g. 123 becomes '123'. SQL Server can handle that 67 | /// 68 | /// 69 | /// 70 | public string DecodeMessage(bool sensitiveLoggingEnabled = true) 71 | { 72 | if (!sensitiveLoggingEnabled) 73 | return Message; 74 | 75 | return EfCoreLogDecoder.DecodeMessage(this); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/LogToOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.EntityFrameworkCore.Diagnostics; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace TestSupport.EfHelpers 9 | { 10 | /// 11 | /// This class allows you to define the various LogTo options 12 | /// NOTE: You can only set a value for one of the three filters: OnlyShow... and FilterFunction 13 | /// 14 | public class LogToOptions 15 | { 16 | /// 17 | /// This controls the action being called. If set to false it will not call the action 18 | /// Defaults to true, i.e. returns all logs 19 | /// 20 | public bool ShowLog { get; set; } = true; 21 | 22 | /// 23 | /// This sets the lowest LogLevel that will be returned 24 | /// Defaults to LogLevel.Information 25 | /// 26 | public LogLevel LogLevel { get; set; } = LogLevel.Information; 27 | 28 | /// 29 | /// This allows you to only show certain DbLoggerCategory, for instance 30 | /// new[] { DbLoggerCategory.Database.Command.Name }) would only show the Database.Command logs 31 | /// Defaults to null, i.e. not used 32 | /// 33 | public string[] OnlyShowTheseCategories { get; set; } 34 | 35 | /// 36 | /// This allows you to only show certain events, for instance 37 | /// new[] { CoreEventId.ContextInitialized } 38 | /// Defaults to null, i.e. not used 39 | /// 40 | public EventId[] OnlyShowTheseEvents { get; set; } 41 | 42 | /// 43 | /// This allows you to provide a method to filter the logs, for instance 44 | /// bool MyFilterFunction(EventId eventId, LogLevel logLevel) {...} 45 | /// Defaults to null, i.e. not used 46 | /// 47 | public Func FilterFunction { get; set; } 48 | 49 | /// 50 | /// This allows you to set format of the log message, for instance 51 | /// DefaultWithUtcTime will use a UTC time instead the local time 52 | /// Defaults to None, which means no extra info is prepended to the message 53 | /// 54 | public DbContextLoggerOptions LoggerOptions { get; set; } = DbContextLoggerOptions.None; 55 | } 56 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/MyLoggerProviderActionOut.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace TestSupport.EfHelpers 8 | { 9 | /// 10 | /// This provides a ILoggerProvider that returns logging output 11 | /// 12 | public class MyLoggerProviderActionOut : ILoggerProvider 13 | { 14 | private readonly Action _efLog; 15 | private readonly LogLevel _logLevel; 16 | 17 | /// 18 | /// This is a logger provider that can be linked into a loggerFactory. 19 | /// It will capture the logs and place them as strings into the provided logs parameter 20 | /// 21 | /// required: a method that will be called when EF Core logs something 22 | /// optional: the level from with you want to capture logs. Defaults to LogLevel.Information 23 | public MyLoggerProviderActionOut(Action efLog, LogLevel logLevel = LogLevel.Information) 24 | { 25 | _efLog = efLog; 26 | _logLevel = logLevel; 27 | } 28 | 29 | /// 30 | /// Create a logger that will return a log when it is called. 31 | /// 32 | /// 33 | /// 34 | public ILogger CreateLogger(string categoryName) 35 | { 36 | return new MyLogger(_efLog, _logLevel); 37 | } 38 | 39 | /// 40 | /// Dispose - not used 41 | /// 42 | public void Dispose() 43 | { 44 | } 45 | 46 | private class MyLogger : ILogger 47 | { 48 | private readonly Action _efLog; 49 | private readonly LogLevel _logLevel; 50 | 51 | public MyLogger(Action efLog, LogLevel logLevel) 52 | { 53 | _efLog = efLog; 54 | _logLevel = logLevel; 55 | } 56 | 57 | public bool IsEnabled(LogLevel logLevel) 58 | { 59 | return logLevel >= _logLevel; 60 | } 61 | 62 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 63 | { 64 | _efLog( new LogOutput(logLevel, eventId, formatter(state, exception))); 65 | Console.WriteLine(formatter(state, exception)); 66 | } 67 | 68 | public IDisposable BeginScope(TState state) 69 | { 70 | return null; 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/PostgreSqlHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using TestSupport.EfHelpers.Internal; 9 | using TestSupport.Helpers; 10 | 11 | namespace TestSupport.EfHelpers 12 | { 13 | /// 14 | /// This static class contains extension methods to use with PostgreSql databases 15 | /// 16 | public static class PostgreSqlHelpers 17 | { 18 | /// 19 | /// This creates the DbContextOptions options for a PostgreSql database, 20 | /// where the database name is formed using the appsetting's PostgreSqlConnection with the class name as a prefix. 21 | /// That is, the database is unique to the object provided 22 | /// 23 | /// 24 | /// this should be this, i.e. the test class you are in 25 | /// Optional: action that allows you to add extra options to the builder 26 | /// 27 | public static DbContextOptions CreatePostgreSqlUniqueClassOptions(this object callingClass, 28 | Action> builder = null) 29 | where T : DbContext 30 | { 31 | return CreatePostgreSqlOptionWithDatabaseName(callingClass, null, builder).Options; 32 | } 33 | 34 | /// 35 | /// This creates the DbContextOptions options for a PostgreSql database while capturing EF Core's logging output. 36 | /// The database name is formed using the appsetting's PostgreSqlConnection with the class name as a prefix. 37 | /// That is, the database is unique to the object provided 38 | /// 39 | /// 40 | /// this should be this, i.e. the class you are in 41 | /// This action is called with each log output 42 | /// Optional: This allows you to define what logs you want and what format. Defaults to LogLevel.Information 43 | /// Optional: action that allows you to add extra options to the builder 44 | /// 45 | public static DbContextOptions CreatePostgreSqlUniqueClassOptionsWithLogTo(this object callingClass, 46 | Action logAction, 47 | LogToOptions logToOptions = null, Action> builder = null) 48 | where T : DbContext 49 | { 50 | if (logAction == null) throw new ArgumentNullException(nameof(logAction)); 51 | 52 | return CreatePostgreSqlOptionWithDatabaseName(callingClass, null, builder) 53 | .AddLogTo(logAction, logToOptions) 54 | .Options; 55 | } 56 | 57 | /// 58 | /// This creates the DbContextOptions options for a PostgreSql database, 59 | /// where the database name is formed using the appsetting's PostgreSqlConnection 60 | /// with the class name and the calling method's name as as a prefix. 61 | /// That is, the database is unique to the calling method. 62 | /// 63 | /// 64 | /// this should be this, i.e. the class you are in 65 | /// Optional: action that allows you to add extra options to the builder 66 | /// Do not use: this is filled in by compiler 67 | /// 68 | public static DbContextOptions CreatePostgreSqlUniqueMethodOptions(this object callingClass, 69 | Action> builder = null, 70 | [CallerMemberName] string callingMember = "") where T : DbContext 71 | { 72 | return CreatePostgreSqlOptionWithDatabaseName(callingClass, callingMember, builder).Options; 73 | } 74 | 75 | //------------------------------------------------ 76 | //private methods 77 | 78 | 79 | private static DbContextOptionsBuilder CreatePostgreSqlOptionWithDatabaseName(object callingClass, 80 | string callingMember, Action> extraOptions) 81 | where T : DbContext 82 | { 83 | var connectionString = callingClass.GetUniquePostgreSqlConnectionString(callingMember); 84 | var builder = new DbContextOptionsBuilder(); 85 | builder.UseNpgsql(connectionString); 86 | builder.ApplyOtherOptionSettings(); 87 | extraOptions?.Invoke(builder); 88 | 89 | return builder; 90 | } 91 | 92 | 93 | } 94 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/SqlAdoNetHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace TestSupport.EfHelpers 7 | { 8 | /// 9 | /// Contains extension methods to help with base SQL commands 10 | /// 11 | public static class SqlAdoNetHelpers 12 | { 13 | /// 14 | /// Execute a count of the rows in a table, with optional where clause, using ADO.NET 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public static int ExecuteRowCount(this string connectionString, string tableName, string whereClause = "") 21 | { 22 | using (var myConn = new SqlConnection(connectionString)) 23 | { 24 | var command = "SELECT COUNT(*) FROM " + tableName + " " + whereClause; 25 | var myCommand = new SqlCommand(command, myConn); 26 | myConn.Open(); 27 | return (int) myCommand.ExecuteScalar(); 28 | } 29 | } 30 | 31 | /// 32 | /// Execute a non-query SQL using ADO.NET 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | public static int ExecuteNonQuery(this string connectionString, string command, int commandTimeout = 10) 39 | { 40 | using (var myConn = new SqlConnection(connectionString)) 41 | { 42 | var myCommand = new SqlCommand(command, myConn) {CommandTimeout = commandTimeout}; 43 | myConn.Open(); 44 | return myCommand.ExecuteNonQuery(); 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/SqliteInMemory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Data.Sqlite; 6 | using Microsoft.EntityFrameworkCore; 7 | using TestSupport.EfHelpers.Internal; 8 | 9 | namespace TestSupport.EfHelpers 10 | { 11 | /// 12 | /// This static class contains extension methods to use with in-memory Sqlite databases 13 | /// 14 | public static class SqliteInMemory 15 | { 16 | /// 17 | /// Created a Sqlite Options for in-memory database. 18 | /// 19 | /// 20 | /// Optional: action that allows you to add extra options to the builder 21 | /// 22 | public static DbContextOptionsDisposable CreateOptions(Action> builder = null) 23 | where T : DbContext 24 | { 25 | return new DbContextOptionsDisposable(SetupConnectionAndBuilderOptions(builder) 26 | .Options); 27 | } 28 | 29 | /// 30 | /// Created a Sqlite Options for in-memory database while using LogTo to get the EF Core logging output. 31 | /// 32 | /// 33 | /// This action is called with each log output 34 | /// Optional: This allows you to define what logs you want and what format. Defaults to LogLevel.Information 35 | /// Optional: action that allows you to add extra options to the builder 36 | /// 37 | public static DbContextOptionsDisposable CreateOptionsWithLogTo(Action logAction, 38 | LogToOptions logToOptions = null , Action> builder = null) 39 | where T : DbContext 40 | { 41 | if (logAction == null) throw new ArgumentNullException(nameof(logAction)); 42 | 43 | return new DbContextOptionsDisposable( 44 | SetupConnectionAndBuilderOptions(builder) 45 | .AddLogTo(logAction, logToOptions) 46 | .Options); 47 | } 48 | 49 | 50 | /// 51 | /// Created a Sqlite Options for in-memory database. 52 | /// 53 | /// 54 | /// 55 | private static DbContextOptionsBuilder 56 | SetupConnectionAndBuilderOptions //#D 57 | (Action> applyExtraOption) //#E 58 | where T : DbContext 59 | { 60 | //Thanks to https://www.scottbrady91.com/Entity-Framework/Entity-Framework-Core-In-Memory-Testing 61 | var connectionStringBuilder = //#F 62 | new SqliteConnectionStringBuilder //#F 63 | { DataSource = ":memory:" }; //#F 64 | var connectionString = connectionStringBuilder.ToString(); //#G 65 | var connection = new SqliteConnection(connectionString); //#H 66 | connection.Open(); //#I //see https://github.com/aspnet/EntityFramework/issues/6968 67 | 68 | // create in-memory context 69 | var builder = new DbContextOptionsBuilder(); 70 | builder.UseSqlite(connection); //#J 71 | builder.ApplyOtherOptionSettings(); //#K 72 | applyExtraOption?.Invoke(builder); //#L 73 | 74 | return builder; //#M 75 | } 76 | 77 | /**************************************************************** 78 | #A A class containing the SQLite in-memory options which is also disposable 79 | #B This parameter allows you at add more option methods while building of the options 80 | #C Gets the DbContextOptions and returns a disposable version 81 | #D This method builds the SQLite in-memory options 82 | #E This contains any extra option methods the user provided 83 | #F Creates a SQLite connection string with the DataSource set to ":memory:" 84 | #G Turns the SQLiteConnectionStringBuilder into a connection string 85 | #H Forms a SQLite connection using the connection string 86 | #I You must open the SQLite connection. If you don't, the in-memory database doesn't work. 87 | #J Builds a DbContextOptions with the SQLite database provider and the open connection 88 | #K Calls a general method used on all your option builders. This enables sensitive logging and better error messages 89 | #L Add any extra options the user added 90 | #M Returns the DbContextOptions to use in the creation of your application's DbContext 91 | * **************************************************************/ 92 | } 93 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/TimeThingResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace TestSupport.EfHelpers 5 | { 6 | /// 7 | /// Result from a TimeThings instance once it is disposed 8 | /// 9 | public class TimeThingResult 10 | { 11 | /// 12 | /// Creates the TimeThingResult 13 | /// 14 | /// 15 | /// 16 | /// 17 | public TimeThingResult(double totalTimeMilliseconds, int numRuns, string message) 18 | { 19 | TotalTimeMilliseconds = totalTimeMilliseconds; 20 | NumRuns = numRuns; 21 | Message = message; 22 | } 23 | 24 | /// 25 | /// Total time in milliseconds, with fractions 26 | /// 27 | public double TotalTimeMilliseconds { get; private set; } 28 | 29 | /// 30 | /// Optional number of runs. zero if not set. 31 | /// 32 | public int NumRuns { get; private set; } 33 | 34 | /// 35 | /// Optional string to identify this usage of the TimeThings 36 | /// 37 | public string Message { get; private set; } 38 | 39 | /// 40 | /// Provides a detailed report of the timed event 41 | /// 42 | /// 43 | public override string ToString() 44 | { 45 | var prefix = NumRuns > 1 ? $"{NumRuns:#,###} x " : ""; 46 | var suffix = NumRuns > 1 ? $", ave. per run = {TimeScaled(TotalTimeMilliseconds / NumRuns)}" : ""; 47 | return $"{prefix}{Message} took {TotalTimeMilliseconds:#,###.00} ms.{suffix}"; 48 | } 49 | 50 | private string TimeScaled(double timeMilliseconds) 51 | { 52 | if (timeMilliseconds > 5 * 1000) 53 | return $"{timeMilliseconds / 1000:F3} sec."; //Seconds 54 | if (timeMilliseconds > 5) 55 | return $"{timeMilliseconds:#,###.00} ms."; //Milliseconds 56 | if (timeMilliseconds > 5 / 1000.0) 57 | return $"{timeMilliseconds * 1000:#,###.00} us."; //Microseconds 58 | return $"{timeMilliseconds * 1000_000:#,###.0} ns."; //Nanoseconds 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /TestSupport/EfHelpers/TimeThings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using Xunit.Abstractions; 7 | 8 | namespace TestSupport.EfHelpers 9 | { 10 | /// 11 | /// Use this in a using statement for timing things - time output to test output when class is disposes 12 | /// 13 | public class TimeThings : IDisposable 14 | { 15 | private readonly Action _funcToCall; 16 | private readonly string _message; 17 | private readonly int _numRuns; 18 | private readonly ITestOutputHelper _output; 19 | private readonly Stopwatch _stopwatch ; 20 | 21 | /// 22 | /// This will measure the time it took from this class being created to it being disposed 23 | /// 24 | /// This action returns the TimeThingResult on dispose 25 | /// Optional: a string to show in the result. Useful if you have multiple timing in a row 26 | /// Optional: if the timing covers multiple runs of something, then set numRuns to the number of runs and it will give you the average per run 27 | public TimeThings(Action result, string message = "", int numRuns = 0) 28 | : this(message, numRuns) 29 | { 30 | _funcToCall = result; 31 | } 32 | 33 | /// 34 | /// This will measure the time it took from this class being created to it being disposed and writes out to xUnit ITestOutputHelper 35 | /// 36 | /// On dispose it will write the result to the output 37 | /// Optional: a string to show in the result. Useful if you have multiple timing in a row 38 | /// Optional: if the timing covers multiple runs of something, then set numRuns to the number of runs and it will give you the average per run 39 | public TimeThings(ITestOutputHelper output, string message = "", int numRuns = 0) 40 | : this(message, numRuns) 41 | { 42 | _output = output; 43 | } 44 | 45 | private TimeThings(string message = "", int numRuns = 0) 46 | { 47 | _message = message; 48 | _numRuns = numRuns; 49 | _stopwatch = new Stopwatch(); 50 | _stopwatch.Start(); 51 | } 52 | 53 | /// 54 | /// When disposed it will return the result, either via a action or by an output 55 | /// 56 | public void Dispose() 57 | { 58 | _stopwatch.Stop(); 59 | var timeMilliseconds = _stopwatch.ElapsedTicks * 1000.0 / Stopwatch.Frequency; 60 | var result = new TimeThingResult(timeMilliseconds, _numRuns, _message); 61 | _funcToCall?.Invoke(result); 62 | _output?.WriteLine(result.ToString()); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /TestSupport/SeedDatabase/AnonymiserData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | 8 | namespace TestSupport.SeedDatabase 9 | { 10 | /// 11 | /// This is used to provide the AnonymiserFunc with extra information on how to create the anonymised string 12 | /// 13 | public class AnonymiserData 14 | { 15 | /// 16 | /// This is the first part of the replacementRequest, e.g. "Name:Max=4" would set this to "Name" 17 | /// 18 | public string ReplacementType { get; private set; } 19 | 20 | /// 21 | /// This contains all the options provided after the first part, separated by :, e.g "Max=4:Min=100" 22 | /// You can add more commands that are specific to your your own Anonymiser function, e.g. "Case=Pascal" 23 | /// 24 | public IImmutableList ReplaceOptions { get; private set; } 25 | 26 | /// 27 | /// This holds the max length, or -1 if not set 28 | /// 29 | public int MaxLength { get; private set; } = -1; 30 | 31 | /// 32 | /// This holds the min length, or -1 if not set 33 | /// 34 | public int MinLength { get; private set; } = -1; 35 | 36 | /// 37 | /// This decodes the replacement string into it component parts 38 | /// 39 | /// 40 | internal AnonymiserData(string replaceRequest) 41 | { 42 | var parts = replaceRequest.Split(':'); 43 | ReplacementType = parts[0]; 44 | var options = new List(); 45 | for (int i = 1; i < parts.Length; i++) 46 | { 47 | options.Add(parts[i]); 48 | var config = parts[i].Split('='); 49 | if (config[0].Equals("max", StringComparison.InvariantCultureIgnoreCase)) 50 | MaxLength = int.Parse(config[1]); 51 | else if (config[0].Equals("min", StringComparison.InvariantCultureIgnoreCase)) 52 | MinLength = int.Parse(config[1]); 53 | //we don't error on other options as the caller might want to add other options. 54 | } 55 | ReplaceOptions = options.ToImmutableList(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /TestSupport/SeedDatabase/DataResetterConfig.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | using TestSupport.SeedDatabase.Internal; 8 | 9 | namespace TestSupport.SeedDatabase 10 | { 11 | /// 12 | /// This provides configuration information for the DataResetter 13 | /// 14 | public class DataResetterConfig 15 | { 16 | internal const string EmailSuffix = "@gmail.com"; 17 | 18 | internal List AnonymiseRequests { get; private set; } = new List(); 19 | 20 | /// 21 | /// If true any Alternative keys will not be reset 22 | /// 23 | public bool DoNotResetAlternativeKey { get; set; } 24 | 25 | /// 26 | /// This function is called on whenever a property you have added via the AnonymiseThisMember config method 27 | /// 28 | public Func AnonymiserFunc { get; set; } = DefaultAnonymiser; 29 | 30 | /// 31 | /// This allows you to add a property in an class to be registered to be anonymised 32 | /// 33 | /// The class the field or property is in 34 | /// An expression such as "p => p.PropertyInYourClass" 35 | /// Provide usage and config of the replacement string, e.g. "Email" or "FirstName:Max=10:Min=5" 36 | /// - First part is the name says what you want, e.g. FirstName, Email, Address1, Country, etc. 37 | /// - You can then add properties like :Max=10,:Min=2 38 | /// NOTE: The default anonymiser uses guids for everything, but add @ana.com if "Email". It also applies the Max=nn if guid is longer 39 | /// 40 | public void AddToAnonymiseList(Expression> expression, string replaceRequest) 41 | { 42 | var member = MemberAnonymiseData.GetPropertyViaLambda(expression); 43 | AnonymiseRequests.Add(new MemberAnonymiseData(typeof(TEntity), member, replaceRequest )); 44 | } 45 | 46 | /// 47 | /// This is a simple Anonymiser using guids 48 | /// It adds "@ana.com" to end of guid if "Email" 49 | /// It applies both "Min=nn" and "Max=nn", but max is applied only if the guid string is longer than than the max 50 | /// 51 | /// This is the AnonymiserData produced by when you called 52 | /// This is the instance of the class it is updating. Useful if you want to use matching data in the same instance. 53 | /// 54 | public static string DefaultAnonymiser(AnonymiserData data, object classInstance) 55 | { 56 | var anoString = Guid.NewGuid().ToString("N"); 57 | while (data.MinLength > 0 && anoString.Length < data.MinLength) 58 | anoString += anoString; 59 | if(data.ReplacementType.Equals("Email", StringComparison.InvariantCultureIgnoreCase)) 60 | anoString += EmailSuffix; 61 | if (data.MaxLength > 0 && data.MaxLength < anoString.Length) 62 | //we trim from the end so that an email will still end in @ano.com 63 | return anoString.Substring(anoString.Length - data.MaxLength); 64 | return anoString; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /TestSupport/SeedDatabase/Internal/MemberAnonymiseData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | 8 | namespace TestSupport.SeedDatabase.Internal 9 | { 10 | internal class MemberAnonymiseData 11 | { 12 | public MemberAnonymiseData(Type classType, PropertyInfo propertyToAnonymise, string replaceRequest) 13 | { 14 | ClassType = classType; 15 | PropertyToAnonymise = propertyToAnonymise; 16 | AnonymiserData = new AnonymiserData(replaceRequest); 17 | } 18 | 19 | public Type ClassType { get; private set; } 20 | public PropertyInfo PropertyToAnonymise { get; private set; } 21 | 22 | public AnonymiserData AnonymiserData { get; private set; } 23 | 24 | public void AnonymiseMember(object entityToUpdate, DataResetterConfig config) 25 | { 26 | PropertyToAnonymise.SetValue(entityToUpdate, config.AnonymiserFunc(AnonymiserData, entityToUpdate)); 27 | } 28 | 29 | //thanks to https://www.codeproject.com/Tips/301274/How-to-get-property-name-using-Expression-2 30 | public static PropertyInfo GetPropertyViaLambda(Expression> expression) 31 | { 32 | var body = expression.Body as MemberExpression ?? ((UnaryExpression)expression.Body).Operand as MemberExpression; 33 | 34 | return (PropertyInfo)body?.Member ?? throw new ArgumentException("You must call this with ...(p => p.PropertyInMyEntity)"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /TestSupport/SeedDatabase/SeedJsonHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Serialization; 9 | using TestSupport.Helpers; 10 | 11 | [assembly: InternalsVisibleTo("Test")] 12 | namespace TestSupport.SeedDatabase 13 | { 14 | /// 15 | /// Set of extensions that help with serialize and save data to JSON, and read+deserialise data back from the JSON files 16 | /// 17 | public static class SeedJsonHelpers 18 | { 19 | /// 20 | /// This serialises the data you provide into a JSON string. 21 | /// You may want to build your own if you have specific requirements 22 | /// 23 | /// 24 | /// The class or collection you want to save 25 | /// Defaults to true - make bigger, but more readable JSON files. 26 | /// 27 | public static string DefaultSerializeToJson(this T data, bool moreReadableJsonFile = true) 28 | { 29 | var setting = new JsonSerializerSettings() 30 | { 31 | ContractResolver = new ResolvePrivateSetters(), //Needed for DDD-styled classes (JSON.NET needs to know it can set the value which serializing) 32 | PreserveReferencesHandling = PreserveReferencesHandling.Objects 33 | }; 34 | if (moreReadableJsonFile) 35 | setting.Formatting = Formatting.Indented; 36 | return JsonConvert.SerializeObject(data, setting); 37 | } 38 | 39 | /// 40 | /// This will read the data from the JSON file using the fileSuffix as a discriminator 41 | /// You may want to build your own if you have specific requirements 42 | /// 43 | /// This is the type of the data you expect to get back, e.g. List{Book} 44 | /// This is the name of the seed data, typically the name of the database that the JSON came from 45 | /// optional: provide the calling assembly. default is to use the current calling assembly 46 | /// 47 | public static T ReadSeedDataFromJsonFile(this string fileSuffix, Assembly callingAssembly = null) 48 | { 49 | var filePath = FormJsonFilePath(fileSuffix, callingAssembly ?? Assembly.GetCallingAssembly()); 50 | var json = File.ReadAllText(filePath); 51 | var settings = new JsonSerializerSettings() 52 | { 53 | ContractResolver = new ResolvePrivateSetters() 54 | }; 55 | return JsonConvert.DeserializeObject(json, settings); 56 | } 57 | 58 | /// 59 | /// This writes the JSON string to a JSON file using the fileSuffix as part of the file name 60 | /// 61 | /// This should be different for each seed data. Suggest using the name of the database that produced it. 62 | /// The json string to save 63 | /// optional: provide the calling assembly. default is to use the current calling assembly 64 | public static void WriteJsonToJsonFile(this string fileSuffix, string json, Assembly callingAssembly = null) 65 | { 66 | var filePath = FormJsonFilePath(fileSuffix, callingAssembly ?? Assembly.GetCallingAssembly()); 67 | File.WriteAllText(filePath, json); 68 | } 69 | 70 | /// 71 | /// This forms the name of the json file using the fileSuffix 72 | /// This is of the form $"SeedData-{fileSuffix}.json" 73 | /// 74 | /// This is the name of the seed data, typically the name of the database that the JSON came from 75 | /// optional: provide the calling assembly. default is to use the current calling assembly 76 | /// 77 | private static string FormJsonFilePath(string fileSuffix, Assembly callingAssembly) 78 | { 79 | return Path.Combine(TestData.GetTestDataDir(callingAssembly: callingAssembly), $"SeedData-{fileSuffix}.json"); 80 | } 81 | 82 | //----------------------------------------------------------------- 83 | //private 84 | 85 | //Thanks to https://bartwullems.blogspot.com/2018/02/jsonnetresolve-private-setters.html 86 | internal class ResolvePrivateSetters : DefaultContractResolver 87 | { 88 | protected override JsonProperty CreateProperty( 89 | MemberInfo member, 90 | MemberSerialization memberSerialization) 91 | { 92 | var prop = base.CreateProperty(member, memberSerialization); 93 | 94 | if (!prop.Writable) 95 | { 96 | var property = member as PropertyInfo; 97 | if (property != null) 98 | { 99 | var hasPrivateSetter = property.GetSetMethod(true) != null; 100 | prop.Writable = hasPrivateSetter; 101 | } 102 | } 103 | 104 | return prop; 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /TestSupport/SeedDatabase/SqlServerProductionSetup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Reflection; 5 | using Microsoft.Data.SqlClient; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using TestSupport.Helpers; 9 | 10 | namespace TestSupport.SeedDatabase 11 | { 12 | /// 13 | /// This provides a way to set up the options for opening a SQL production database 14 | /// 15 | /// 16 | public class SqlServerProductionSetup where YourDbContext : DbContext 17 | { 18 | /// 19 | /// This provides the options 20 | /// 21 | public DbContextOptions Options { get; } 22 | 23 | /// 24 | /// This provides the name of the database that was opened. Useful if you want to save the data using the database name 25 | /// 26 | public string DatabaseName { get; } 27 | 28 | /// 29 | /// This sets up the Options and DatabaseName properties ready for you to open the SQL database 30 | /// 31 | /// This is either the name of a connection in the appsetting.json file or the actual connection string 32 | public SqlServerProductionSetup(string connectionNameOrConnectionString) 33 | { 34 | var connection = GetConfigurationOrActualString(connectionNameOrConnectionString, Assembly.GetCallingAssembly()); 35 | var builder = new SqlConnectionStringBuilder(connection); 36 | DatabaseName = builder.InitialCatalog; 37 | var optionsBuilder = new DbContextOptionsBuilder(); 38 | optionsBuilder.UseSqlServer(connection); 39 | Options = optionsBuilder.Options; 40 | } 41 | 42 | //-------------------------------------------- 43 | //private methods 44 | 45 | private string GetConfigurationOrActualString(string configOrConnectionString, Assembly callingAssembly) 46 | { 47 | var config = AppSettings.GetConfiguration(callingAssembly); 48 | var connectionFromConfigFile = config.GetConnectionString(configOrConnectionString); 49 | return connectionFromConfigFile ?? configOrConnectionString; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /TestSupport/TestSupport.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | EfCore.TestSupport 29 | 9.0.0 30 | 9.0.0 31 | Jon P Smith 32 | Useful tools when unit testing applications that use Entity Framework Core. See readme file on github. 33 | false 34 | 35 | - .NET 9 version 36 | - NOTE: EnsureClean code now uses EnsuredDeleted, then EnsuredDeleted ro provide a new, empty database 37 | 38 | Copyright (c) 2020 Jon P Smith. Licenced under MIT licence 39 | Entity Framework Core, xUnit 40 | true 41 | true 42 | https://github.com/JonPSmith/EfCore.TestSupport 43 | https://github.com/JonPSmith/EfCore.TestSupport 44 | EfCoreTestSupportNuGetIcon128.png 45 | MIT 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /UnitTestExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/74ce78237c8887de93a5cc0d2e8cf394463cf3d1/UnitTestExample.png --------------------------------------------------------------------------------