From 9545c7622726d9a2e69fd6ed170c6870d21a90a5 Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Mon, 16 Sep 2024 12:51:18 +0700 Subject: [PATCH 1/9] Allow to adjust NpgsqlDataSource --- .config/dotnet-tools.json | 4 +- .../ExampleWeb/Database/ExampleDbContext.cs | 10 ++-- examples/ExampleWeb/Database/User.cs | 2 +- examples/ExampleWeb/ExampleWeb.csproj | 14 +++-- .../ExampleDbContextModelSnapshot.cs | 5 +- examples/ExampleWeb/Program.cs | 17 +++--- .../MccSoft.IntegreSql.EF.csproj | 12 ++-- .../NpgsqlDatabaseInitializer.cs | 17 +++++- .../ExampleWeb.IntegrationTests.csproj | 15 +++-- .../IntegrationTestAdvancedSeedingExample.cs | 26 ++++----- ...IntegrationTestBaseWithoutEnsureCreated.cs | 36 ++++++------ .../IntegrationTestSimplified.cs | 50 ++++++++--------- .../ExampleWeb.UnitTests.csproj | 13 +++-- .../SeedingFunctionThrowsTests.cs | 56 +++++++++---------- 14 files changed, 143 insertions(+), 134 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6b2a01c..d72cc69 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,13 +9,13 @@ ] }, "dotnet-outdated-tool": { - "version": "3.2.1", + "version": "4.6.0", "commands": [ "dotnet-outdated" ] }, "dotnet-ef": { - "version": "8.0.0", + "version": "8.0.8", "commands": [ "dotnet-ef" ] diff --git a/examples/ExampleWeb/Database/ExampleDbContext.cs b/examples/ExampleWeb/Database/ExampleDbContext.cs index fe54a8e..afece78 100644 --- a/examples/ExampleWeb/Database/ExampleDbContext.cs +++ b/examples/ExampleWeb/Database/ExampleDbContext.cs @@ -6,13 +6,15 @@ public class ExampleDbContext : DbContext { public DbSet Users { get; set; } - public ExampleDbContext(DbContextOptions options) : base(options) { } + public ExampleDbContext(DbContextOptions options) + : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder - .Entity() - .HasData(new() { Id = 1, Name = "John" }, new() { Id = 2, Name = "Bill" }); + modelBuilder.Entity(e => + { + e.HasData(new() { Id = 1, Name = "John" }, new() { Id = 2, Name = "Bill" }); + }); } } diff --git a/examples/ExampleWeb/Database/User.cs b/examples/ExampleWeb/Database/User.cs index fa094fc..d7bab06 100644 --- a/examples/ExampleWeb/Database/User.cs +++ b/examples/ExampleWeb/Database/User.cs @@ -4,4 +4,4 @@ public class User { public int Id { get; set; } public string Name { get; set; } -} \ No newline at end of file +} diff --git a/examples/ExampleWeb/ExampleWeb.csproj b/examples/ExampleWeb/ExampleWeb.csproj index 4c4500d..9c8371d 100644 --- a/examples/ExampleWeb/ExampleWeb.csproj +++ b/examples/ExampleWeb/ExampleWeb.csproj @@ -1,17 +1,21 @@ - net6.0 + net8.0 enable enable true - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/examples/ExampleWeb/Migrations/ExampleDbContextModelSnapshot.cs b/examples/ExampleWeb/Migrations/ExampleDbContextModelSnapshot.cs index e0aae3b..de459c1 100644 --- a/examples/ExampleWeb/Migrations/ExampleDbContextModelSnapshot.cs +++ b/examples/ExampleWeb/Migrations/ExampleDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System.Collections.Generic; using ExampleWeb; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -16,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -35,7 +36,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Users"); + b.ToTable("Users", (string)null); b.HasData( new diff --git a/examples/ExampleWeb/Program.cs b/examples/ExampleWeb/Program.cs index 67c22b2..9aca34d 100644 --- a/examples/ExampleWeb/Program.cs +++ b/examples/ExampleWeb/Program.cs @@ -1,12 +1,11 @@ using ExampleWeb; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient(); -builder.Services.AddDbContext( - options => options.UseNpgsql(builder.Configuration.GetValue("Postgres")) +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetValue("Postgres")) ); // builder.Services.AddDbContext( @@ -21,8 +20,8 @@ // if you use CreateDatabaseGetConnectionString). if (app.Configuration.GetValue("DisableSeed") != true) { - await app.Services - .CreateScope() + await app + .Services.CreateScope() .ServiceProvider.GetRequiredService() .Database.MigrateAsync(); } @@ -32,10 +31,10 @@ await app.Services "/database-type", (ExampleDbContext dbContext) => dbContext.Database.IsNpgsql() - ? "postgres" - : dbContext.Database.IsSqlite() - ? "sqlite" - : "unknown" + ? "postgres" + : dbContext.Database.IsSqlite() + ? "sqlite" + : "unknown" ); app.MapGet( "/users", diff --git a/src/MccSoft.IntegreSql.EF/MccSoft.IntegreSql.EF.csproj b/src/MccSoft.IntegreSql.EF/MccSoft.IntegreSql.EF.csproj index 69cc206..3e45b5b 100644 --- a/src/MccSoft.IntegreSql.EF/MccSoft.IntegreSql.EF.csproj +++ b/src/MccSoft.IntegreSql.EF/MccSoft.IntegreSql.EF.csproj @@ -12,7 +12,7 @@ GitHub - net6.0 + net8.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 1591,CLASS0001;ASYNC0001;ASYNC0002;ASYNC0003;ASYNC0004;RETURN0001;VpRoslynConfigureAwaitAnalayzer Helper classes to use IntegreSql (and Sqlite) with EntityFramework in .Net tests. @@ -22,11 +22,11 @@ - - - - - + + + + + diff --git a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs index 6a265be..a712c9c 100644 --- a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs @@ -4,6 +4,7 @@ using MccSoft.IntegreSql.EF.DatabaseInitialization; using MccSoft.IntegreSql.EF.Dto; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Npgsql; namespace MccSoft.IntegreSql.EF; @@ -14,6 +15,7 @@ namespace MccSoft.IntegreSql.EF; public class NpgsqlDatabaseInitializer : BaseDatabaseInitializer { private readonly IntegreSqlClient _integreSqlClient; + private readonly Action _adjustNpgsqlDataSource; /// /// When is called, IntegreSQL returns a connection string it uses to connect to PostgreSQL. @@ -57,13 +59,18 @@ private record ConnectionStringInfo(string Hash, int Id); /// So you could override some connection string by defining /// and setting some properties to non-null values. /// + /// + /// Action that allows to adjust NpgsqlDataSourceBuilder before creating Npgsql connection. + /// public NpgsqlDatabaseInitializer( Uri integreSqlUri = null, - ConnectionStringOverride connectionStringOverride = null + ConnectionStringOverride connectionStringOverride = null, + Action adjustNpgsqlDataSource = null ) { integreSqlUri ??= new Uri("http://localhost:5000/api/v1/"); _integreSqlClient = new IntegreSqlClient(integreSqlUri); + _adjustNpgsqlDataSource = adjustNpgsqlDataSource; ConnectionStringOverride = connectionStringOverride; } @@ -210,7 +217,13 @@ await _integreSqlClient.RecreateTestDatabase( public override void UseProvider(DbContextOptionsBuilder options, string connectionString) { - options.UseNpgsql(connectionString); + options.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); + _adjustNpgsqlDataSource?.Invoke(dataSourceBuilder); + var dataSource = dataSourceBuilder.Build(); + + options.UseNpgsql(dataSource); } protected override void PerformBasicSeedingOperations(DbContext dbContext) diff --git a/tests/ExampleWeb.IntegrationTests/ExampleWeb.IntegrationTests.csproj b/tests/ExampleWeb.IntegrationTests/ExampleWeb.IntegrationTests.csproj index 4897f8e..a46f038 100644 --- a/tests/ExampleWeb.IntegrationTests/ExampleWeb.IntegrationTests.csproj +++ b/tests/ExampleWeb.IntegrationTests/ExampleWeb.IntegrationTests.csproj @@ -1,20 +1,23 @@ - net6.0 + net8.0 enable false - - + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/ExampleWeb.IntegrationTests/IntegrationTestAdvancedSeedingExample.cs b/tests/ExampleWeb.IntegrationTests/IntegrationTestAdvancedSeedingExample.cs index e02fc94..410b306 100644 --- a/tests/ExampleWeb.IntegrationTests/IntegrationTestAdvancedSeedingExample.cs +++ b/tests/ExampleWeb.IntegrationTests/IntegrationTestAdvancedSeedingExample.cs @@ -48,24 +48,20 @@ private async Task SeedData() private WebApplicationFactory CreateWebApplication(string connectionString) { - var webAppFactory = new WebApplicationFactory().WithWebHostBuilder( - builder => + var webAppFactory = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => { - builder.ConfigureServices( - services => - { - var descriptor = services.Single( - d => d.ServiceType == typeof(DbContextOptions) - ); - services.Remove(descriptor); + var descriptor = services.Single(d => + d.ServiceType == typeof(DbContextOptions) + ); + services.Remove(descriptor); - services.AddDbContext( - options => _databaseInitializer.UseProvider(options, connectionString) - ); - } + services.AddDbContext(options => + _databaseInitializer.UseProvider(options, connectionString) ); - } - ); + }); + }); _httpClient = webAppFactory.CreateDefaultClient(); return webAppFactory; diff --git a/tests/ExampleWeb.IntegrationTests/IntegrationTestBaseWithoutEnsureCreated.cs b/tests/ExampleWeb.IntegrationTests/IntegrationTestBaseWithoutEnsureCreated.cs index 3c3c961..0475b7e 100644 --- a/tests/ExampleWeb.IntegrationTests/IntegrationTestBaseWithoutEnsureCreated.cs +++ b/tests/ExampleWeb.IntegrationTests/IntegrationTestBaseWithoutEnsureCreated.cs @@ -25,24 +25,20 @@ protected IntegrationTestBaseWithoutEnsureCreated(DatabaseType databaseType) ) ); - var webAppFactory = new WebApplicationFactory().WithWebHostBuilder( - builder => + var webAppFactory = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => { - builder.ConfigureServices( - services => - { - var descriptor = services.Single( - d => d.ServiceType == typeof(DbContextOptions) - ); - services.Remove(descriptor); + var descriptor = services.Single(d => + d.ServiceType == typeof(DbContextOptions) + ); + services.Remove(descriptor); - services.AddDbContext( - options => _databaseInitializer.UseProvider(options, _connectionString) - ); - } + services.AddDbContext(options => + _databaseInitializer.UseProvider(options, _connectionString) ); - } - ); + }); + }); _httpClient = webAppFactory.CreateDefaultClient(); } @@ -52,11 +48,11 @@ private IDatabaseInitializer CreateDatabaseInitializer(DatabaseType databaseType return databaseType switch { DatabaseType.Postgres - => new NpgsqlDatabaseInitializer( - // This is needed if you run tests NOT inside the container. - // 5434 is the public port number of Postgresql instance - connectionStringOverride: new() { Host = "localhost", Port = 5434 } - ), + => new NpgsqlDatabaseInitializer( + // This is needed if you run tests NOT inside the container. + // 5434 is the public port number of Postgresql instance + connectionStringOverride: new() { Host = "localhost", Port = 5434 } + ), DatabaseType.Sqlite => new SqliteDatabaseInitializer(), _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, null) }; diff --git a/tests/ExampleWeb.IntegrationTests/IntegrationTestSimplified.cs b/tests/ExampleWeb.IntegrationTests/IntegrationTestSimplified.cs index 5cbc20c..9f9adde 100644 --- a/tests/ExampleWeb.IntegrationTests/IntegrationTestSimplified.cs +++ b/tests/ExampleWeb.IntegrationTests/IntegrationTestSimplified.cs @@ -35,37 +35,33 @@ public IntegrationTestSimplified() ); // Create a standard WebApplicationFactory to set up web app in tests - var webAppFactory = new WebApplicationFactory().WithWebHostBuilder( - builder => + var webAppFactory = new WebApplicationFactory().WithWebHostBuilder(builder => + { + // Inject 'DisableSeed' configuration variable to disable running Migrations in Startup + builder.ConfigureAppConfiguration( + (context, configuration) => + { + configuration.AddInMemoryCollection( + new KeyValuePair[] { new("DisableSeed", "true") } + ); + } + ); + + // Adjust DI configurations with test-specifics + builder.ConfigureServices(services => { - // Inject 'DisableSeed' configuration variable to disable running Migrations in Startup - builder.ConfigureAppConfiguration( - (context, configuration) => - { - configuration.AddInMemoryCollection( - new KeyValuePair[] { new("DisableSeed", "true") } - ); - } + // Remove default DbContext registration from DI + var descriptor = services.Single(d => + d.ServiceType == typeof(DbContextOptions) ); + services.Remove(descriptor); - // Adjust DI configurations with test-specifics - builder.ConfigureServices( - services => - { - // Remove default DbContext registration from DI - var descriptor = services.Single( - d => d.ServiceType == typeof(DbContextOptions) - ); - services.Remove(descriptor); - - // Add new DbContext registration - services.AddDbContext( - options => _databaseInitializer.UseProvider(options, _connectionString) - ); - } + // Add new DbContext registration + services.AddDbContext(options => + _databaseInitializer.UseProvider(options, _connectionString) ); - } - ); + }); + }); // Create http client to connect to our TestServer within test _httpClient = webAppFactory.CreateDefaultClient(); diff --git a/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj b/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj index 308db73..9cc9b46 100644 --- a/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj +++ b/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj @@ -1,17 +1,20 @@ - net6.0 + net8.0 enable false - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/ExampleWeb.UnitTests/SeedingFunctionThrowsTests.cs b/tests/ExampleWeb.UnitTests/SeedingFunctionThrowsTests.cs index 1b24b7f..4ed5306 100644 --- a/tests/ExampleWeb.UnitTests/SeedingFunctionThrowsTests.cs +++ b/tests/ExampleWeb.UnitTests/SeedingFunctionThrowsTests.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using MccSoft.IntegreSql.EF; using MccSoft.IntegreSql.EF.DatabaseInitialization; using Xunit; @@ -10,7 +9,8 @@ public class SeedingFunctionThrowsTests : UnitTestBase { private readonly UserService _service; - public SeedingFunctionThrowsTests() : base(DatabaseType.Sqlite) + public SeedingFunctionThrowsTests() + : base(DatabaseType.Sqlite) { _service = new UserService(CreateDbContext()); } @@ -21,35 +21,31 @@ public void Test() string hash = nameof(SeedingFunctionThrowsTests) + Guid.NewGuid(); SqliteDatabaseInitializer.ClearInitializationTasks(); - Assert.ThrowsAny( - () => - { - _databaseInitializer?.CreateDatabaseGetDbContextOptionsBuilderSync( - new DatabaseSeedingOptions( - Name: hash, - async context => - { - throw new InvalidOperationException("zxc"); - } - ) - ); - } - ); + Assert.ThrowsAny(() => + { + _databaseInitializer?.CreateDatabaseGetDbContextOptionsBuilderSync( + new DatabaseSeedingOptions( + Name: hash, + async context => + { + throw new InvalidOperationException("zxc"); + } + ) + ); + }); SqliteDatabaseInitializer.ClearInitializationTasks(); - Assert.ThrowsAny( - () => - { - _databaseInitializer?.CreateDatabaseGetDbContextOptionsBuilderSync( - new DatabaseSeedingOptions( - Name: hash, - async context => - { - throw new InvalidOperationException("zxc"); - } - ) - ); - } - ); + Assert.ThrowsAny(() => + { + _databaseInitializer?.CreateDatabaseGetDbContextOptionsBuilderSync( + new DatabaseSeedingOptions( + Name: hash, + async context => + { + throw new InvalidOperationException("zxc"); + } + ) + ); + }); } } From 886131ddb36ed63b2083480875147551b7991b5f Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 11:48:19 +0700 Subject: [PATCH 2/9] Add tests for postgres-specific behaviour (jsonb columns) --- IntegreSql.EF.sln | 14 +++++ .../Database/Document.cs | 12 ++++ .../ExamplePostgresSpecificDbContext.cs | 23 +++++++ .../Database/User.cs | 8 +++ .../ExampleWebPostgresSpecific.csproj | 20 ++++++ .../20240917040854_Initial.Designer.cs | 62 +++++++++++++++++++ .../Migrations/20240917040854_Initial.cs | 54 ++++++++++++++++ ...ePostgresSpecificDbContextModelSnapshot.cs | 59 ++++++++++++++++++ .../ExampleWebPostgresSpecific/Program.cs | 58 +++++++++++++++++ .../Properties/launchSettings.json | 41 ++++++++++++ .../ExampleWebPostgresSpecific/UserService.cs | 35 +++++++++++ .../appsettings.Development.json | 8 +++ .../appsettings.json | 11 ++++ .../BaseDatabaseInitializer.cs | 22 ++++--- ...xampleWebPostgresSpecific.UnitTests.csproj | 28 +++++++++ .../PostgresTests.cs | 54 ++++++++++++++++ .../UnitTestBase.cs | 58 +++++++++++++++++ .../UnitTestSimplified.cs | 45 ++++++++++++++ .../UnitTestWithCustomSeedData.cs | 51 +++++++++++++++ 19 files changed, 656 insertions(+), 7 deletions(-) create mode 100644 examples/ExampleWebPostgresSpecific/Database/Document.cs create mode 100644 examples/ExampleWebPostgresSpecific/Database/ExamplePostgresSpecificDbContext.cs create mode 100644 examples/ExampleWebPostgresSpecific/Database/User.cs create mode 100644 examples/ExampleWebPostgresSpecific/ExampleWebPostgresSpecific.csproj create mode 100644 examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.Designer.cs create mode 100644 examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.cs create mode 100644 examples/ExampleWebPostgresSpecific/Migrations/ExamplePostgresSpecificDbContextModelSnapshot.cs create mode 100644 examples/ExampleWebPostgresSpecific/Program.cs create mode 100644 examples/ExampleWebPostgresSpecific/Properties/launchSettings.json create mode 100644 examples/ExampleWebPostgresSpecific/UserService.cs create mode 100644 examples/ExampleWebPostgresSpecific/appsettings.Development.json create mode 100644 examples/ExampleWebPostgresSpecific/appsettings.json create mode 100644 tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj create mode 100644 tests/ExampleWebPostgresSpecific.UnitTests/PostgresTests.cs create mode 100644 tests/ExampleWebPostgresSpecific.UnitTests/UnitTestBase.cs create mode 100644 tests/ExampleWebPostgresSpecific.UnitTests/UnitTestSimplified.cs create mode 100644 tests/ExampleWebPostgresSpecific.UnitTests/UnitTestWithCustomSeedData.cs diff --git a/IntegreSql.EF.sln b/IntegreSql.EF.sln index 653fd51..2b364fa 100644 --- a/IntegreSql.EF.sln +++ b/IntegreSql.EF.sln @@ -12,6 +12,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWeb.IntegrationTests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWeb.UnitTests", "tests\ExampleWeb.UnitTests\ExampleWeb.UnitTests.csproj", "{AB078860-06D1-4611-BBBA-1D7E96C9B421}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWebPostgresSpecific", "examples\ExampleWebPostgresSpecific\ExampleWebPostgresSpecific.csproj", "{3AD86800-36E2-48B9-A3A0-428B6C0EEBFE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWebPostgresSpecific.UnitTests", "tests\ExampleWebPostgresSpecific.UnitTests\ExampleWebPostgresSpecific.UnitTests.csproj", "{8538E23A-C48D-4229-91CE-3767198CB583}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,10 +38,20 @@ Global {AB078860-06D1-4611-BBBA-1D7E96C9B421}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB078860-06D1-4611-BBBA-1D7E96C9B421}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB078860-06D1-4611-BBBA-1D7E96C9B421}.Release|Any CPU.Build.0 = Release|Any CPU + {3AD86800-36E2-48B9-A3A0-428B6C0EEBFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AD86800-36E2-48B9-A3A0-428B6C0EEBFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AD86800-36E2-48B9-A3A0-428B6C0EEBFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AD86800-36E2-48B9-A3A0-428B6C0EEBFE}.Release|Any CPU.Build.0 = Release|Any CPU + {8538E23A-C48D-4229-91CE-3767198CB583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8538E23A-C48D-4229-91CE-3767198CB583}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8538E23A-C48D-4229-91CE-3767198CB583}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8538E23A-C48D-4229-91CE-3767198CB583}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B8D87F8F-F622-48A1-BEEA-4C8BA6B1F6E9} = {0F0A5C71-7CAD-46EB-9A89-21CDDA43815D} {802044F5-BD36-4ECD-8ABD-96E742EE6F36} = {6A62837E-674A-444E-AEF4-177681A1D816} {AB078860-06D1-4611-BBBA-1D7E96C9B421} = {6A62837E-674A-444E-AEF4-177681A1D816} + {3AD86800-36E2-48B9-A3A0-428B6C0EEBFE} = {0F0A5C71-7CAD-46EB-9A89-21CDDA43815D} + {8538E23A-C48D-4229-91CE-3767198CB583} = {6A62837E-674A-444E-AEF4-177681A1D816} EndGlobalSection EndGlobal diff --git a/examples/ExampleWebPostgresSpecific/Database/Document.cs b/examples/ExampleWebPostgresSpecific/Database/Document.cs new file mode 100644 index 0000000..eb11b22 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Database/Document.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace ExampleWebPostgresSpecific.Database; + +public class Document +{ + public int Id { get; set; } + public string Name { get; set; } + + [Column(TypeName = "jsonb")] + public List? SubDocuments { get; set; } +} diff --git a/examples/ExampleWebPostgresSpecific/Database/ExamplePostgresSpecificDbContext.cs b/examples/ExampleWebPostgresSpecific/Database/ExamplePostgresSpecificDbContext.cs new file mode 100644 index 0000000..f8a7126 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Database/ExamplePostgresSpecificDbContext.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; + +namespace ExampleWebPostgresSpecific.Database; + +public class ExamplePostgresSpecificDbContext : DbContext +{ + public DbSet Users { get; set; } + + public ExamplePostgresSpecificDbContext( + DbContextOptions options + ) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(e => + { + e.Property(x => x.Documents).HasColumnType("jsonb"); + e.HasData(new() { Id = 1, Name = "John" }, new() { Id = 2, Name = "Bill" }); + }); + } +} diff --git a/examples/ExampleWebPostgresSpecific/Database/User.cs b/examples/ExampleWebPostgresSpecific/Database/User.cs new file mode 100644 index 0000000..d323363 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Database/User.cs @@ -0,0 +1,8 @@ +namespace ExampleWebPostgresSpecific.Database; + +public class User +{ + public int Id { get; set; } + public string Name { get; set; } + public List? Documents { get; set; } +} diff --git a/examples/ExampleWebPostgresSpecific/ExampleWebPostgresSpecific.csproj b/examples/ExampleWebPostgresSpecific/ExampleWebPostgresSpecific.csproj new file mode 100644 index 0000000..56dfc1b --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/ExampleWebPostgresSpecific.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.Designer.cs b/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.Designer.cs new file mode 100644 index 0000000..893859d --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.Designer.cs @@ -0,0 +1,62 @@ +// +using System.Collections.Generic; +using ExampleWebPostgresSpecific.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ExampleWebPostgresSpecific.Migrations +{ + [DbContext(typeof(ExamplePostgresSpecificDbContext))] + [Migration("20240917040854_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExampleWebPostgresSpecific.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("Documents") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = 1, + Name = "John" + }, + new + { + Id = 2, + Name = "Bill" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.cs b/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.cs new file mode 100644 index 0000000..d198189 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Migrations/20240917040854_Initial.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using ExampleWebPostgresSpecific.Database; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ExampleWebPostgresSpecific.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table + .Column(type: "integer", nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), + Name = table.Column(type: "text", nullable: false), + Documents = table.Column>(type: "jsonb", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + } + ); + + migrationBuilder.InsertData( + table: "Users", + columns: new[] { "Id", "Documents", "Name" }, + values: new object[,] + { + { 1, null, "John" }, + { 2, null, "Bill" } + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "Users"); + } + } +} diff --git a/examples/ExampleWebPostgresSpecific/Migrations/ExamplePostgresSpecificDbContextModelSnapshot.cs b/examples/ExampleWebPostgresSpecific/Migrations/ExamplePostgresSpecificDbContextModelSnapshot.cs new file mode 100644 index 0000000..4cf08e0 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Migrations/ExamplePostgresSpecificDbContextModelSnapshot.cs @@ -0,0 +1,59 @@ +// +using System.Collections.Generic; +using ExampleWebPostgresSpecific.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ExampleWebPostgresSpecific.Migrations +{ + [DbContext(typeof(ExamplePostgresSpecificDbContext))] + partial class ExamplePostgresSpecificDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ExampleWebPostgresSpecific.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("Documents") + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = 1, + Name = "John" + }, + new + { + Id = 2, + Name = "Bill" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/examples/ExampleWebPostgresSpecific/Program.cs b/examples/ExampleWebPostgresSpecific/Program.cs new file mode 100644 index 0000000..42afe3a --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Program.cs @@ -0,0 +1,58 @@ +using ExampleWebPostgresSpecific; +using ExampleWebPostgresSpecific.Database; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddTransient(); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetValue("Postgres")) +); + +// builder.Services.AddDbContext( +// options => options.UseSqlite(builder.Configuration.GetValue("Sqlite")) +// ); + +var app = builder.Build(); + +// Not using migration in every test greatly speeds up the execution. +// So you need to make sure that database schema and seed data is cached within template database. +// (by default DB schema is created using dbContext.Database.EnsureCreated() +// if you use CreateDatabaseGetConnectionString). +if (app.Configuration.GetValue("DisableSeed") != true) +{ + await app + .Services.CreateScope() + .ServiceProvider.GetRequiredService() + .Database.MigrateAsync(); +} + +app.MapGet("/", () => "Hello World!"); +app.MapGet( + "/database-type", + (ExamplePostgresSpecificDbContext dbContext) => + dbContext.Database.IsNpgsql() + ? "postgres" + : dbContext.Database.IsSqlite() + ? "sqlite" + : "unknown" +); +app.MapGet( + "/users", + async (ExamplePostgresSpecificDbContext dbContext) => + await dbContext.Users.OrderBy(x => x.Id).Select(x => new { x.Id, x.Name }).ToListAsync() +); +app.MapPost( + "/users", + async (context) => + { + var user = await context.Request.ReadFromJsonAsync(); + var dbContext = + context.RequestServices.GetRequiredService(); + dbContext.Users.Add(user); + await dbContext.SaveChangesAsync(); + } +); +app.MapGet("/users-from-service", async (UserService userService) => await userService.GetUsers()); + +app.Run(); diff --git a/examples/ExampleWebPostgresSpecific/Properties/launchSettings.json b/examples/ExampleWebPostgresSpecific/Properties/launchSettings.json new file mode 100644 index 0000000..2405fdd --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59697", + "sslPort": 44398 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5027", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7124;http://localhost:5027", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/ExampleWebPostgresSpecific/UserService.cs b/examples/ExampleWebPostgresSpecific/UserService.cs new file mode 100644 index 0000000..b5571f2 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/UserService.cs @@ -0,0 +1,35 @@ +using ExampleWebPostgresSpecific.Database; +using Microsoft.EntityFrameworkCore; + +namespace ExampleWebPostgresSpecific; + +public class UserService +{ + private readonly ExamplePostgresSpecificDbContext _postgresSpecificDbContext; + + public UserService(ExamplePostgresSpecificDbContext postgresSpecificDbContext) + { + _postgresSpecificDbContext = postgresSpecificDbContext; + } + + public async Task> GetUsers() + { + return await _postgresSpecificDbContext + .Users.OrderBy(x => x.Id) + .Select(x => x.Name) + .ToListAsync(); + } + + public async Task AddUserWithDocuments(string name, List documents) + { + _postgresSpecificDbContext.Users.Add(new User() { Name = name, Documents = documents }); + await _postgresSpecificDbContext.SaveChangesAsync(); + } + + public async Task> GetUsersWithDocuments() + { + return await _postgresSpecificDbContext + .Users.Where(u => u.Documents != null && u.Documents.Count > 0) + .ToListAsync(); + } +} diff --git a/examples/ExampleWebPostgresSpecific/appsettings.Development.json b/examples/ExampleWebPostgresSpecific/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/ExampleWebPostgresSpecific/appsettings.json b/examples/ExampleWebPostgresSpecific/appsettings.json new file mode 100644 index 0000000..2a5a7f3 --- /dev/null +++ b/examples/ExampleWebPostgresSpecific/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Postgres": "Server=localhost;Database=integresql_example_web;Port=5432;Username=postgres;Password=postgres;Pooling=true;Keepalive=5;Command Timeout=60;Include Error Detail=true", + "Sqlite": "Data Source=db.sqlite;Version=3;", + "AllowedHosts": "*" +} diff --git a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs index 99517f5..6de3f63 100644 --- a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs @@ -49,7 +49,8 @@ public virtual void RemoveDatabaseSync(string connectionString) public void UseProvider( DbContextOptionsBuilder options, DatabaseSeedingOptions databaseSeedingOptions - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { string connectionString = CreateDatabaseGetConnectionString(databaseSeedingOptions) .ConfigureAwait(false) @@ -61,7 +62,8 @@ DatabaseSeedingOptions databaseSeedingOptions /// public Task CreateDatabaseGetConnectionString( DatabaseSeedingOptions databaseSeeding - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { string lastMigrationName = ContextHelper.GetLastMigrationName() ?? ""; @@ -72,9 +74,11 @@ DatabaseSeedingOptions databaseSeeding + typeof(TDbContext).Assembly.FullName, async (connectionString) => { + // Required to be able to get password from DbContext.Database.GetConnectionString() + var newConnectionString = "Persist Security Info=true;" + connectionString; await using var dbContext = ContextHelper.CreateDbContext( useProvider: this, - connectionString: connectionString, + connectionString: newConnectionString, factoryMethod: databaseSeeding?.DbContextFactory ); if (databaseSeeding?.DisableEnsureCreated != true) @@ -92,7 +96,8 @@ DatabaseSeedingOptions databaseSeeding /// public string CreateDatabaseGetConnectionStringSync( DatabaseSeedingOptions databaseSeeding = null - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { return TaskUtils.RunSynchronously(() => CreateDatabaseGetConnectionString(databaseSeeding)); } @@ -100,7 +105,8 @@ public string CreateDatabaseGetConnectionStringSync( /// public virtual DbContextOptionsBuilder CreateDbContextOptionsBuilder( string connectionString - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { var builder = new DbContextOptionsBuilder(); UseProvider(builder, connectionString); @@ -114,7 +120,8 @@ public virtual async Task< DbContextOptionsBuilder > CreateDatabaseGetDbContextOptionsBuilder( DatabaseSeedingOptions seedingOptions - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { var connectionString = await CreateDatabaseGetConnectionString(seedingOptions); return CreateDbContextOptionsBuilder(connectionString); @@ -123,7 +130,8 @@ DatabaseSeedingOptions seedingOptions /// public virtual DbContextOptionsBuilder CreateDatabaseGetDbContextOptionsBuilderSync( DatabaseSeedingOptions seedingOptions = null - ) where TDbContext : DbContext + ) + where TDbContext : DbContext { return TaskUtils.RunSynchronously( () => CreateDatabaseGetDbContextOptionsBuilder(seedingOptions) diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj b/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj new file mode 100644 index 0000000..29e332a --- /dev/null +++ b/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/PostgresTests.cs b/tests/ExampleWebPostgresSpecific.UnitTests/PostgresTests.cs new file mode 100644 index 0000000..94b8cff --- /dev/null +++ b/tests/ExampleWebPostgresSpecific.UnitTests/PostgresTests.cs @@ -0,0 +1,54 @@ +using ExampleWebPostgresSpecific.Database; +using MccSoft.IntegreSql.EF.DatabaseInitialization; + +namespace ExampleWebPostgresSpecific.UnitTests; + +public class PostgresTests : UnitTestBase +{ + private readonly UserService _service; + + public PostgresTests() + : base(DatabaseType.Postgres) + { + _service = new UserService(CreateDbContext()); + } + + /// + /// We run the test several times just to show how fast the subsequent runs are + /// (the very first test is usually not that fast) + /// + [Theory()] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] +#pragma warning disable xUnit1026 + public async Task Test1(int iteration) +#pragma warning restore xUnit1026 + { + var users = await _service.GetUsers(); + Assert.Equal(new[] { "John", "Bill" }, users); + } + + [Fact] + public async Task Test2() + { + var name = "Igor"; + var documents = new List + { + new() + { + Name = "Document1", + SubDocuments = [new() { Name = "SubDocument1" }, new() { Name = "SubDocument2" }] + }, + new() { Name = "Document2" } + }; + await _service.AddUserWithDocuments(name, documents); + var users = await _service.GetUsersWithDocuments(); + Assert.NotNull(users); + Assert.Single(users); + Assert.Contains(users, x => x.Name == name); + + Assert.Equal(documents, users.First().Documents); + } +} diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestBase.cs b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestBase.cs new file mode 100644 index 0000000..17d84b2 --- /dev/null +++ b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestBase.cs @@ -0,0 +1,58 @@ +using ExampleWebPostgresSpecific.Database; +using MccSoft.IntegreSql.EF; +using MccSoft.IntegreSql.EF.DatabaseInitialization; +using Microsoft.EntityFrameworkCore; + +namespace ExampleWebPostgresSpecific.UnitTests; + +public class UnitTestBase : IDisposable +{ + protected readonly IDatabaseInitializer? _databaseInitializer; + private DbContextOptions? _dbContextOptions; + + public UnitTestBase( + DatabaseType? databaseType, + DatabaseSeedingOptions? seedingOptions = null + ) + { + _databaseInitializer = CreateDatabaseInitializer(databaseType); + + _dbContextOptions = _databaseInitializer + ?.CreateDatabaseGetDbContextOptionsBuilderSync( + seedingOptions: seedingOptions + ) + ?.Options; + } + + private IDatabaseInitializer? CreateDatabaseInitializer(DatabaseType? databaseType) + { + return databaseType switch + { + null => null, + DatabaseType.Postgres + => new NpgsqlDatabaseInitializer( + // This is needed if you run tests NOT inside the container. + // 5434 is the public port number of Postgresql instance + connectionStringOverride: new() { Host = "localhost", Port = 5434 }, + adjustNpgsqlDataSource: builder => builder.EnableDynamicJson() + ) + { + DropDatabaseOnRemove = true, + }, + DatabaseType.Sqlite => new SqliteDatabaseInitializer(), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, null) + }; + } + + public ExamplePostgresSpecificDbContext CreateDbContext() + { + if (_dbContextOptions == null) + return null!; + return new ExamplePostgresSpecificDbContext(_dbContextOptions); + } + + public void Dispose() + { + _databaseInitializer?.RemoveDatabase(CreateDbContext().Database.GetConnectionString()); + } +} diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestSimplified.cs b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestSimplified.cs new file mode 100644 index 0000000..ec60450 --- /dev/null +++ b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestSimplified.cs @@ -0,0 +1,45 @@ +using ExampleWebPostgresSpecific.Database; +using MccSoft.IntegreSql.EF; +using Microsoft.EntityFrameworkCore; + +namespace ExampleWebPostgresSpecific.UnitTests; + +public class UnitTestSimplified : IDisposable +{ + private readonly DbContextOptions _dbContextOptions; + private readonly NpgsqlDatabaseInitializer _databaseInitializer; + + public UnitTestSimplified() + { + _databaseInitializer = new NpgsqlDatabaseInitializer( + // This is needed if you run tests NOT inside the container. + // 5434 is the public port number of Postgresql instance + connectionStringOverride: new() { Host = "localhost", Port = 5434, } + ) + { + DropDatabaseOnRemove = true, + }; + _dbContextOptions = _databaseInitializer + .CreateDatabaseGetDbContextOptionsBuilderSync() + .Options; + } + + public ExamplePostgresSpecificDbContext CreateDbContext() + { + return new ExamplePostgresSpecificDbContext(_dbContextOptions); + } + + [Fact] + public async Task Test1() + { + var connectionString = CreateDbContext().Database.GetConnectionString(); + var service = new UserService(CreateDbContext()); + var users = await service.GetUsers(); + Assert.Equal(new[] { "John", "Bill" }, users); + } + + public void Dispose() + { + _databaseInitializer?.RemoveDatabase(CreateDbContext().Database.GetConnectionString()); + } +} diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestWithCustomSeedData.cs b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestWithCustomSeedData.cs new file mode 100644 index 0000000..564dda0 --- /dev/null +++ b/tests/ExampleWebPostgresSpecific.UnitTests/UnitTestWithCustomSeedData.cs @@ -0,0 +1,51 @@ +using ExampleWebPostgresSpecific.Database; +using MccSoft.IntegreSql.EF; +using MccSoft.IntegreSql.EF.DatabaseInitialization; +using Microsoft.EntityFrameworkCore; + +namespace ExampleWebPostgresSpecific.UnitTests; + +public class UnitTestWithCustomSeedData : IDisposable +{ + private readonly DbContextOptions _dbContextOptions; + private readonly NpgsqlDatabaseInitializer _databaseInitializer; + + public UnitTestWithCustomSeedData() + { + _databaseInitializer = new NpgsqlDatabaseInitializer( + // This is needed if you run tests NOT inside the container. + // 5434 is the public port number of Postgresql instance + connectionStringOverride: new() { Host = "localhost", Port = 5434, } + ); + _dbContextOptions = _databaseInitializer + .CreateDatabaseGetDbContextOptionsBuilderSync( + new DatabaseSeedingOptions( + "DatabaseWithSeedData", + async context => + { + context.Users.Add(new User() { Id = 5, Name = "Eugene" }); + await context.SaveChangesAsync(); + } + ) + ) + .Options; + } + + public ExamplePostgresSpecificDbContext CreateDbContext() + { + return new ExamplePostgresSpecificDbContext(_dbContextOptions); + } + + [Fact] + public async Task Test1() + { + var service = new UserService(CreateDbContext()); + var users = await service.GetUsers(); + Assert.Equal(new[] { "John", "Bill", "Eugene" }, users); + } + + public void Dispose() + { + _databaseInitializer?.RemoveDatabase(CreateDbContext().Database.GetConnectionString()); + } +} From cd391afda7ec0de1f8483b72c785ce5deb3e534d Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 11:54:24 +0700 Subject: [PATCH 3/9] Clear --- examples/ExampleWeb/Database/ExampleDbContext.cs | 7 +++---- examples/ExampleWeb/ExampleWeb.csproj | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/ExampleWeb/Database/ExampleDbContext.cs b/examples/ExampleWeb/Database/ExampleDbContext.cs index afece78..5b3f30f 100644 --- a/examples/ExampleWeb/Database/ExampleDbContext.cs +++ b/examples/ExampleWeb/Database/ExampleDbContext.cs @@ -12,9 +12,8 @@ public ExampleDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity(e => - { - e.HasData(new() { Id = 1, Name = "John" }, new() { Id = 2, Name = "Bill" }); - }); + modelBuilder + .Entity() + .HasData(new() { Id = 1, Name = "John" }, new() { Id = 2, Name = "Bill" }); } } diff --git a/examples/ExampleWeb/ExampleWeb.csproj b/examples/ExampleWeb/ExampleWeb.csproj index 9c8371d..a879054 100644 --- a/examples/ExampleWeb/ExampleWeb.csproj +++ b/examples/ExampleWeb/ExampleWeb.csproj @@ -15,7 +15,6 @@ - From 7af85527db61e37ce7634443219786f8271e3462 Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 11:56:28 +0700 Subject: [PATCH 4/9] Adjust pipeline --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 13c6484..9f7859e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore From f283c7506630d6089e1671763e43375e661d47fd Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 12:00:00 +0700 Subject: [PATCH 5/9] Fix docker-compose --- .github/workflows/dotnet.yml | 2 +- scripts/run-compose.bat | 4 ++-- .../Exceptions/IntegreSqlNotRunningException.cs | 2 +- .../Exceptions/IntegreSqlPostgresNotAvailableException.cs | 2 +- tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj | 4 ---- .../ExampleWebPostgresSpecific.UnitTests.csproj | 5 ++++- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9f7859e..4c5dbd1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,7 +26,7 @@ jobs: run: dotnet build --no-restore - name: run IntegreSQL docker - run: cd scripts; docker-compose up -d + run: cd scripts; docker compose up -d - name: Test run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" diff --git a/scripts/run-compose.bat b/scripts/run-compose.bat index 27e26e9..66fa6ca 100644 --- a/scripts/run-compose.bat +++ b/scripts/run-compose.bat @@ -1,2 +1,2 @@ -docker-compose rm -fsv -docker-compose up -d +docker compose rm -fsv +docker compose up -d diff --git a/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlNotRunningException.cs b/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlNotRunningException.cs index d9cf5a9..0765a81 100644 --- a/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlNotRunningException.cs +++ b/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlNotRunningException.cs @@ -7,7 +7,7 @@ public class IntegreSqlNotRunningException : IntegreSqlException public IntegreSqlNotRunningException(string uri, Exception innerException = null) : base( $"IntegreSQL not available. Make sure IntegreSQL is running at '{uri}'." - + $"Maybe running docker-compose from https://github.com/mcctomsk/IntegreSql.EF/tree/main/scripts could help", + + $"Maybe running docker compose from https://github.com/mcctomsk/IntegreSql.EF/tree/main/scripts could help", innerException ) { } } diff --git a/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlPostgresNotAvailableException.cs b/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlPostgresNotAvailableException.cs index 4f16cdf..ed05fac 100644 --- a/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlPostgresNotAvailableException.cs +++ b/src/MccSoft.IntegreSql.EF/Exceptions/IntegreSqlPostgresNotAvailableException.cs @@ -7,6 +7,6 @@ public class IntegreSqlPostgresNotAvailableException : IntegreSqlException public IntegreSqlPostgresNotAvailableException(string uri) : base( $"IntegreSQL at '{uri}' can't connect to PostgreSQL. Examine IntegreSQL docker logs and make sure you set up IntegreSQL and PostgreSQL corectly." - + $"Maybe running docker-compose from https://github.com/mcctomsk/IntegreSql.EF/tree/main/scripts could help." + + $"Maybe running docker compose from https://github.com/mcctomsk/IntegreSql.EF/tree/main/scripts could help." ) { } } diff --git a/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj b/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj index 9cc9b46..3e79316 100644 --- a/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj +++ b/tests/ExampleWeb.UnitTests/ExampleWeb.UnitTests.csproj @@ -18,10 +18,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj b/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj index 29e332a..edf989c 100644 --- a/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj +++ b/tests/ExampleWebPostgresSpecific.UnitTests/ExampleWebPostgresSpecific.UnitTests.csproj @@ -10,7 +10,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From c2bab1c9254603f256f17a2822c9cda9cbfd435b Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 12:06:10 +0700 Subject: [PATCH 6/9] Fix: Connection string keyword 'persist security info' is not supported by SQLite --- .../DatabaseInitialization/BaseDatabaseInitializer.cs | 9 ++++++--- src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs index 6de3f63..7753933 100644 --- a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs @@ -59,6 +59,11 @@ DatabaseSeedingOptions databaseSeedingOptions UseProvider(options, connectionString); } + protected virtual string AdjustConnectionStringOnSeeding(string connectionString) + { + return connectionString; + } + /// public Task CreateDatabaseGetConnectionString( DatabaseSeedingOptions databaseSeeding @@ -74,11 +79,9 @@ DatabaseSeedingOptions databaseSeeding + typeof(TDbContext).Assembly.FullName, async (connectionString) => { - // Required to be able to get password from DbContext.Database.GetConnectionString() - var newConnectionString = "Persist Security Info=true;" + connectionString; await using var dbContext = ContextHelper.CreateDbContext( useProvider: this, - connectionString: newConnectionString, + connectionString: AdjustConnectionStringOnSeeding(connectionString), factoryMethod: databaseSeeding?.DbContextFactory ); if (databaseSeeding?.DisableEnsureCreated != true) diff --git a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs index a712c9c..51cb3dc 100644 --- a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs @@ -87,6 +87,12 @@ static NpgsqlDatabaseInitializer() /// private static readonly LazyConcurrentDictionary InitializationTasks; + protected override string AdjustConnectionStringOnSeeding(string connectionString) + { + // Required to be able to get password from DbContext.Database.GetConnectionString() + return "Persist Security Info=true;" + connectionString; + } + /// /// Returns a PostgreSQL connection string to be used in the test. /// Runs function for the first test in a sequence. From f36115d4fcbbb641689949338ee0abf7474e4914 Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 12:14:52 +0700 Subject: [PATCH 7/9] Fix: Resource not accessible by integration --- .github/workflows/dotnet.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 4c5dbd1..2d9098e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -6,6 +6,11 @@ on: pull_request: branches: [ main ] +permissions: + id-token: write + contents: read + checks: write + jobs: build: From a9cc0382be706580405cc86757a16cb9aad5bd76 Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 12:19:24 +0700 Subject: [PATCH 8/9] Fix tests --- .github/workflows/dotnet.yml | 5 ----- .../DatabaseInitialization/BaseDatabaseInitializer.cs | 2 ++ src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs | 2 -- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2d9098e..4c5dbd1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -6,11 +6,6 @@ on: pull_request: branches: [ main ] -permissions: - id-token: write - contents: read - checks: write - jobs: build: diff --git a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs index 7753933..8f04fe3 100644 --- a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace MccSoft.IntegreSql.EF.DatabaseInitialization; @@ -56,6 +57,7 @@ DatabaseSeedingOptions databaseSeedingOptions .ConfigureAwait(false) .GetAwaiter() .GetResult(); + options.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); UseProvider(options, connectionString); } diff --git a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs index 51cb3dc..048fbac 100644 --- a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs @@ -223,8 +223,6 @@ await _integreSqlClient.RecreateTestDatabase( public override void UseProvider(DbContextOptionsBuilder options, string connectionString) { - options.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); - var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); _adjustNpgsqlDataSource?.Invoke(dataSourceBuilder); var dataSource = dataSourceBuilder.Build(); From f27de071e0cf15e988223551d32dc32e0e4a44a9 Mon Sep 17 00:00:00 2001 From: Stepan Ustimenko <50939552+Apochromat@users.noreply.github.com.> Date: Tue, 17 Sep 2024 12:30:40 +0700 Subject: [PATCH 9/9] Fix tests --- .../DatabaseInitialization/BaseDatabaseInitializer.cs | 6 ++++-- src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs | 1 + src/MccSoft.IntegreSql.EF/SqliteDatabaseInitializer.cs | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs index 8f04fe3..5c2d242 100644 --- a/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/DatabaseInitialization/BaseDatabaseInitializer.cs @@ -44,7 +44,10 @@ public virtual void RemoveDatabaseSync(string connectionString) } /// - public abstract void UseProvider(DbContextOptionsBuilder options, string connectionString); + public virtual void UseProvider(DbContextOptionsBuilder options, string connectionString) + { + options.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); + } /// public void UseProvider( @@ -57,7 +60,6 @@ DatabaseSeedingOptions databaseSeedingOptions .ConfigureAwait(false) .GetAwaiter() .GetResult(); - options.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)); UseProvider(options, connectionString); } diff --git a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs index 048fbac..fa4d52a 100644 --- a/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/NpgsqlDatabaseInitializer.cs @@ -223,6 +223,7 @@ await _integreSqlClient.RecreateTestDatabase( public override void UseProvider(DbContextOptionsBuilder options, string connectionString) { + base.UseProvider(options, connectionString); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); _adjustNpgsqlDataSource?.Invoke(dataSourceBuilder); var dataSource = dataSourceBuilder.Build(); diff --git a/src/MccSoft.IntegreSql.EF/SqliteDatabaseInitializer.cs b/src/MccSoft.IntegreSql.EF/SqliteDatabaseInitializer.cs index 4f17a24..64a121f 100644 --- a/src/MccSoft.IntegreSql.EF/SqliteDatabaseInitializer.cs +++ b/src/MccSoft.IntegreSql.EF/SqliteDatabaseInitializer.cs @@ -84,6 +84,7 @@ public override Task RemoveDatabase(string connectionString) public override void UseProvider(DbContextOptionsBuilder options, string connectionString) { + base.UseProvider(options, connectionString); options.UseSqlite(connectionString); }