├── Test
├── TestData
│ ├── Dummy file.txt
│ ├── SubDirWithOneFileInIt
│ │ └── One file.txt
│ ├── differentAppSettings.json
│ ├── Script01 - Add row to Authors table.sql
│ ├── Script02 - Add two rows to Authors table.sql
│ ├── AlterMyClassesToNotHaveAPrimaryKey.sql
│ ├── Index01 - Create MyEntites with unique constraint.sql
│ ├── Logging01 - example logged, no values.txt
│ ├── AddUserDefinedFunctions.sql
│ ├── Logging03 - sensitive data with odd string.txt
│ ├── Logging02 - funny name param, no values.txt
│ ├── DbContextCompareLog01 - MyEntity default.json
│ ├── SeedData-ExampleDatabaseAnonymised.json
│ ├── SeedData-ExampleDatabase.json
│ ├── SeedData-DddExampleDatabase.json
│ └── SeedData-DddExampleDatabaseAnonymised.json
├── AltTestDataDir
│ └── Alt dummy file.txt
├── appsettings.json
├── Test.csproj
├── UnitTests
│ ├── TestSupport
│ │ ├── UnitTest1.cs
│ │ ├── TestFileData.cs
│ │ └── TestTimeThings.cs
│ ├── TestDataLayer
│ │ ├── ExampleTest.cs
│ │ ├── TestApplyScriptExtension.cs
│ │ ├── TestDisconnectedState.cs
│ │ ├── TestSqlServerHelpers.cs
│ │ └── TestPostgreSqlHelpers.cs
│ └── TestDataResetter
│ │ └── TestResetKeysSingleEntity.cs
├── UnitCommands
│ └── DeleteAllUnitTestDatabases.cs
└── Helpers
│ ├── DddEfTestData.cs
│ └── EfTestData.cs
├── .vscode
├── settings.json
├── tasks.json.old
├── tasks.json
└── launch.json
├── DataLayer
├── appsettings.json
├── MutipleSchema
│ ├── Class1.cs
│ ├── Class2.cs
│ ├── Class3.cs
│ ├── Class4.cs
│ └── ManySchemaDbContext.cs
├── SpecialisedEntities
│ ├── PaymentCash.cs
│ ├── PaymentCard.cs
│ ├── MyEntityReadOnly.cs
│ ├── BookDetail.cs
│ ├── Address.cs
│ ├── User.cs
│ ├── Payment.cs
│ ├── BookSummary.cs
│ ├── OrderInfo.cs
│ ├── Configurations
│ │ ├── BookDetailConfig.cs
│ │ ├── UserConfig.cs
│ │ ├── OrderInfoConfig.cs
│ │ ├── PaymentConfig.cs
│ │ └── BookSummaryConfig.cs
│ ├── OwnedWithKeyDbContext.cs
│ ├── SpecializedDbContext.cs
│ └── AllTypesEntity.cs
├── Database2
│ ├── Dependent2.cs
│ ├── TopClass2.cs
│ └── DbContext2.cs
├── Database1
│ ├── Dependent1.cs
│ ├── TopClass1.cs
│ └── DbContext1.cs
├── BookApp
│ ├── EfCode
│ │ ├── Configurations
│ │ │ ├── PriceOfferConfig.cs
│ │ │ ├── LineItemConfig.cs
│ │ │ ├── BookAuthorConfig.cs
│ │ │ └── BookConfig.cs
│ │ ├── OrderContext.cs
│ │ ├── BookOrderContext.cs
│ │ ├── BookOrderSchemaContext.cs
│ │ └── BookContext.cs
│ ├── Author.cs
│ ├── Review.cs
│ ├── PriceOffer.cs
│ ├── BookAuthor.cs
│ ├── Order.cs
│ ├── Book.cs
│ └── LineItem.cs
├── DecodedParts
│ ├── AllTypesDbContext.cs
│ └── AllTypesEntity.cs
├── DataLayer.csproj
└── DddBookApp
│ ├── DddBookAuthor.cs
│ ├── EfCode
│ ├── DddBookContext.cs
│ └── Configurations
│ │ ├── BookAuthorConfig.cs
│ │ └── BookConfig.cs
│ ├── DddReview.cs
│ └── DddAuthor.cs
├── UnitTestExample.png
├── SeedFromProductionOverview.png
├── TestSupport
├── EfCoreTestSupportNuGetIcon128.png
├── Assert.Extensions
│ ├── ExtraStringAssertionExtensions.cs
│ ├── BooleanAssertionExtensions.cs
│ ├── StringAssertionExtensions.cs
│ └── CollectionAssertionExtensions.cs
├── Attributes
│ └── RunnableInDebugOnlyAttribute .cs
├── EfHelpers
│ ├── CleanDatabaseExtensions.cs
│ ├── SqlAdoNetHelpers.cs
│ ├── ApplyScriptExtension.cs
│ ├── Internal
│ │ ├── OptionBuilderHelpers.cs
│ │ └── EfCoreLogDecoder.cs
│ ├── TimeThingResult.cs
│ ├── LogToOptions.cs
│ ├── MyLoggerProviderActionOut.cs
│ ├── DbContextOptionsDisposable.cs
│ ├── TimeThings.cs
│ ├── LogOutput.cs
│ ├── SqliteInMemory.cs
│ └── PostgreSqlHelpers.cs
├── SeedDatabase
│ ├── Internal
│ │ └── MemberAnonymiseData.cs
│ ├── SqlServerProductionSetup.cs
│ ├── AnonymiserData.cs
│ ├── DataResetterConfig.cs
│ └── SeedJsonHelpers.cs
└── TestSupport.csproj
├── TestFromSqlRaw
├── MyEntity.cs
├── TestFromSqlRaw.csproj
├── DbContext1.cs
└── Program.cs
├── EfCoreInAction.Test.sln.DotSettings
├── LICENSE
├── .gitattributes
├── README.md
└── .gitignore
/Test/TestData/Dummy file.txt:
--------------------------------------------------------------------------------
1 | This is the content of the dummy file
--------------------------------------------------------------------------------
/Test/AltTestDataDir/Alt dummy file.txt:
--------------------------------------------------------------------------------
1 | This is the content of the dummy file
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dotnet-test-explorer.testProjectPath": "Test"
3 | }
--------------------------------------------------------------------------------
/DataLayer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "MyString": "This is in the DataLayer"
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/Test/TestData/SubDirWithOneFileInIt/One file.txt:
--------------------------------------------------------------------------------
1 | This is the content of the dummy file
--------------------------------------------------------------------------------
/UnitTestExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/HEAD/UnitTestExample.png
--------------------------------------------------------------------------------
/Test/TestData/differentAppSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "MyString": "This is in the TestData directory in Test"
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/SeedFromProductionOverview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/HEAD/SeedFromProductionOverview.png
--------------------------------------------------------------------------------
/TestSupport/EfCoreTestSupportNuGetIcon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JonPSmith/EfCore.TestSupport/HEAD/TestSupport/EfCoreTestSupportNuGetIcon128.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/EfCoreInAction.Test.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
--------------------------------------------------------------------------------
/TestFromSqlRaw/TestFromSqlRaw.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net10.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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();
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/.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/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/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/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/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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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();
--------------------------------------------------------------------------------
/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/DataLayer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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]
--------------------------------------------------------------------------------
/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Test/Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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/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 | ]
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/TestSupport/TestSupport.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 | all
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | EfCore.TestSupport
30 | 10.0.0
31 | 10.0.0
32 | Jon P Smith
33 | Useful tools when unit testing applications that use Entity Framework Core. See readme file on github.
34 | false
35 |
36 | - .NET 10 version
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 version.
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 10.?.? supports NET 10 only
12 | - Version 9.?.? supports NET 9 only
13 | - Version 8.?.? supports NET 8 only
14 | - Version 6.?.? supports NET 6, 7 and 8
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 | 
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 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/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/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/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-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/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 | }
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 connectionString = context.Database.GetDbConnection().ConnectionString;
37 | var builder = new NpgsqlConnectionStringBuilder(connectionString);
38 | _output.WriteLine(builder.Database);
39 | builder.Database.ShouldEndWith(GetType().Name);
40 | }
41 | }
42 |
43 | [Fact]
44 | public void TestPostgreSqUniqueMethodOk()
45 | {
46 | //SETUP
47 | //ATTEMPT
48 | var options = this.CreatePostgreSqlUniqueMethodOptions();
49 | using (var context = new BookContext(options))
50 | {
51 |
52 | //VERIFY
53 | var builder = new NpgsqlConnectionStringBuilder(context.Database.GetDbConnection().ConnectionString);
54 | builder.Database
55 | .ShouldEndWith($"{GetType().Name}_{nameof(TestPostgreSqUniqueMethodOk)}" );
56 | }
57 | }
58 |
59 | [Fact]
60 | public void TestEnsureDeletedEnsureCreatedOk()
61 | {
62 | //SETUP
63 | var options = this.CreatePostgreSqlUniqueClassOptions();
64 | using var context = new BookContext(options);
65 |
66 | context.Database.EnsureCreated();
67 | context.SeedDatabaseFourBooks();
68 |
69 | //ATTEMPT
70 | using (new TimeThings(_output, "Time to EnsureDeleted and EnsureCreated"))
71 | {
72 | context.Database.EnsureDeleted();
73 | context.Database.EnsureCreated();
74 | }
75 |
76 | //VERIFY
77 | context.Books.Count().ShouldEqual(0);
78 | }
79 |
80 | [Fact]
81 | public void TestEnsureCreatedExistingDbOk()
82 | {
83 | //SETUP
84 | var options = this.CreatePostgreSqlUniqueClassOptions();
85 | using var context = new BookContext(options);
86 |
87 | context.Database.EnsureCreated();
88 |
89 | //ATTEMPT
90 | using (new TimeThings(_output, "EnsureCreated when database exists"))
91 | {
92 | context.Database.EnsureCreated();
93 | }
94 |
95 | //VERIFY
96 | }
97 |
98 | [Fact]
99 | public void TestEnsureCleanExistingDatabaseOk()
100 | {
101 | //SETUP
102 | var options = this.CreatePostgreSqlUniqueClassOptions();
103 | using var context = new BookContext(options);
104 |
105 | context.Database.EnsureCreated();
106 | context.SeedDatabaseFourBooks();
107 |
108 | //ATTEMPT
109 | using (new TimeThings(_output, "Time to EnsureClean"))
110 | {
111 | context.Database.EnsureClean();
112 | }
113 |
114 | //VERIFY
115 | context.Books.Count().ShouldEqual(0);
116 | }
117 |
118 | [RunnableInDebugOnly]
119 | public void TestCreatePostgreSqlUniqueClassOptionsWithLogToOk()
120 | {
121 | //SETUP
122 | var logs = new List();
123 | var options = this.CreatePostgreSqlUniqueClassOptionsWithLogTo(log => logs.Add(log));
124 | using (var context = new BookContext(options))
125 | {
126 | //ATTEMPT
127 | context.Database.EnsureDeleted();
128 | context.Database.EnsureCreated();
129 |
130 | //VERIFY
131 | foreach (var log in logs)
132 | {
133 | _output.WriteLine(log);
134 | }
135 | }
136 | }
137 |
138 | [Fact]
139 | public void TestAddExtraBuilderOptions()
140 | {
141 | //SETUP
142 | var options1 = this.CreatePostgreSqlUniqueClassOptions();
143 | using (var context = new BookContext(options1))
144 | {
145 | context.Database.EnsureCreated();
146 | context.SeedDatabaseDummyBooks(100);
147 |
148 | var book = context.Books.First();
149 | context.Entry(book).State.ShouldEqual(EntityState.Unchanged);
150 | }
151 | //ATTEMPT
152 | var options2 = this.CreatePostgreSqlUniqueClassOptions(
153 | builder => builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
154 | using (var context = new BookContext(options2))
155 | {
156 | //VERIFY
157 | var book = context.Books.First();
158 | context.Entry(book).State.ShouldEqual(EntityState.Detached);
159 | }
160 | }
161 | }
162 | }
--------------------------------------------------------------------------------