From 91e3c30b87a2bf36043a4cb10c949a8a18f58e51 Mon Sep 17 00:00:00 2001 From: Nicolas Lenz Date: Wed, 10 Jun 2026 16:34:45 +0200 Subject: [PATCH 1/5] Use own type name for container name This makes it possible to use multiple containers with different settings using the same base DBMS --- .../DbContainer/TestDbContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs index 8c396cd..deefaeb 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs @@ -24,7 +24,7 @@ public abstract class TestDbContainer(IMessage protected abstract TBuilderEntity CreateBuilder(); - protected virtual string DbmsName => typeof(TContainerEntity).Name.Replace("Container", ""); + protected virtual string DbmsName => GetType().Name.Replace("TestDbContainer", ""); protected override TBuilderEntity Configure() { From 96de772522b81dc1a8d50c1337d3108bd0ceee68 Mon Sep 17 00:00:00 2001 From: Nicolas Lenz Date: Wed, 10 Jun 2026 16:36:48 +0200 Subject: [PATCH 2/5] Disable Testcontainer reuse --- .../DbContainer/TestDbContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs index deefaeb..a05a838 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs @@ -30,7 +30,7 @@ protected override TBuilderEntity Configure() { var targetFramework = GetType().Assembly.GetCustomAttributes().FirstOrDefault(e => e.Key == "TargetFramework")?.Value ?? "NA"; return CreateBuilder() - .WithReuse(true) + .WithReuse(false) .WithName($"PhenX.EntityFrameworkCore.BulkInsert.Tests.{DbmsName}-{targetFramework}") .WithWaitStrategy(Wait.ForUnixContainer().UntilDatabaseIsAvailable(DbProviderFactory)); } From 3f655548aafc417970f3c21074ae4d39283bd254 Mon Sep 17 00:00:00 2001 From: Nicolas Lenz Date: Wed, 10 Jun 2026 16:41:14 +0200 Subject: [PATCH 3/5] Expand gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index bdba052..982abde 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,8 @@ fabric.properties # Vitepress docs/.vitepress/dist docs/.vitepress/cache + +.direnv +.envrc +.launch +PhenX.EntityFrameworkCore.BulkInsert.sln.DotSettings.user From 6c19b915ad594dbc6c8a2db03b13f4aa9c86166f Mon Sep 17 00:00:00 2001 From: Nicolas Lenz Date: Wed, 10 Jun 2026 16:40:16 +0200 Subject: [PATCH 4/5] Add Testcontainer with SnakeCaseNamingConvention --- .../TestDbContainerPostgreSqlSnakeCase.cs | 39 +++++++++++++++++++ ...ntityFrameworkCore.BulkInsert.Tests.csproj | 3 ++ .../Arrays/ArrayTestsPostgreSqlSnakeCase.cs | 12 ++++++ 3 files changed, 54 insertions(+) create mode 100644 tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSqlSnakeCase.cs create mode 100644 tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Arrays/ArrayTestsPostgreSqlSnakeCase.cs diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSqlSnakeCase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSqlSnakeCase.cs new file mode 100644 index 0000000..2455c39 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSqlSnakeCase.cs @@ -0,0 +1,39 @@ +using System.Data.Common; + +using Microsoft.EntityFrameworkCore; + +using Npgsql; + +using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; + +using Testcontainers.PostgreSql; + +using Xunit; +using Xunit.Abstractions; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; + +[CollectionDefinition(Name)] +public class TestDbContainerPostgreSqlSnakeCaseCollection : ICollectionFixture +{ + public const string Name = "PostgreSqlSnakeCase"; +} + +public class TestDbContainerPostgreSqlSnakeCase(IMessageSink messageSink) : TestDbContainer(messageSink) +{ + public override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance; + + // GeoSpatial support, using imresamu/postgis instead of postgis/postgis for arm64 support, see https://github.com/postgis/docker-postgis/issues/216#issuecomment-2936824962 + protected override PostgreSqlBuilder CreateBuilder() => new("imresamu/postgis:17-3.5"); + + protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) + { + optionsBuilder + .UseNpgsql(GetConnectionString(databaseName), o => + { + o.UseNetTopologySuite(); + }) + .UseSnakeCaseNamingConvention() + .UseBulkInsertPostgreSql(); + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj index 8672007..8adfc56 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj @@ -38,18 +38,21 @@ + + + diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Arrays/ArrayTestsPostgreSqlSnakeCase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Arrays/ArrayTestsPostgreSqlSnakeCase.cs new file mode 100644 index 0000000..ad18ecc --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Arrays/ArrayTestsPostgreSqlSnakeCase.cs @@ -0,0 +1,12 @@ +using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; +using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +using Xunit; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Arrays; + +[Trait("Category", "PostgreSqlSnakeCase")] +[Collection(TestDbContainerPostgreSqlSnakeCaseCollection.Name)] +public class ArrayTestsPostgreSqlSnakeCase(TestDbContainerPostgreSqlSnakeCase dbContainer) : ArrayTestsBase(dbContainer) +{ +} From 5bd964421577d131837db8476bc398800f61483d Mon Sep 17 00:00:00 2001 From: Nicolas Lenz Date: Wed, 10 Jun 2026 16:42:13 +0200 Subject: [PATCH 5/5] Temporarily add "HasDefaultValueSql", WIP --- .../DbContext/TestDbContext.cs | 356 +++++++++--------- 1 file changed, 179 insertions(+), 177 deletions(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs index a895781..475163c 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs @@ -1,177 +1,179 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -using SmartEnum.EFCore; - -namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; - -public class TestDbContext : TestDbContextBase -{ - public DbSet TestEntities { get; set; } = null!; - public DbSet TestEntitiesWithSimpleTypes { get; set; } = null!; - public DbSet TestEntitiesWithJson { get; set; } = null!; - public DbSet TestEntitiesWithGuidId { get; set; } = null!; - public DbSet TestEntitiesWithConverter { get; set; } = null!; - public DbSet TestEntitiesWithComplexType { get; set; } = null!; - public DbSet TestEntitiesWithSmartEnum { get; set; } = null!; - public DbSet TestEntitiesWithSpecialColumnNames { get; set; } = null!; - public DbSet Students { get; set; } = null!; - public DbSet Courses { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureSmartEnum(); - - modelBuilder.Entity(builder => - { - builder.Property(e => e.CreatedAt) - .HasConversion(new DateTimeToBinaryConverter()); - }); - - modelBuilder.Entity(builder => - { - builder - .ComplexProperty(e => e.OwnedComplexType) - .IsRequired(); - }); - - // Many-to-many with shadow property - modelBuilder.Entity() - .HasMany(s => s.Courses) - .WithMany(c => c.Students) - .UsingEntity>( - "StudentCourse", - j => j.HasOne().WithMany().HasForeignKey("CourseId"), - j => j.HasOne().WithMany().HasForeignKey("StudentId"), - j => - { - j.Property("EnrolledAt"); - j.HasKey("StudentId", "CourseId"); - } - ); - - // Keyless entity type - modelBuilder.Entity(builder => - { - builder.HasNoKey(); - // ToView will use the given table name read-only, it doesn't have to actually be a database view. - // We just reuse the table for the standard TestEntity. - builder.ToView("test_entity"); - }); - } -} - -public class TestDbContextPostgreSql : TestDbContext -{ - public DbSet TestEntitiesWithArrays { get; set; } = null!; - public DbSet TestEntitiesWithEnumList { get; set; } = null!; - public DbSet TestEntitiesWithEnumArray { get; set; } = null!; - public DbSet TestEntitiesWithIntList { get; set; } = null!; - public DbSet TestEntitiesWithEnumListExplicitType { get; set; } = null!; - public DbSet TestRecordsWithEnumList { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString("jsonb"); - b.Property(x => x.JsonObject).AsJsonString("jsonb"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.EnumList).HasColumnType("integer[]"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.TestRun).HasColumnName("test_run"); - b.Property(x => x.Values).HasColumnName("values"); - }); - } -} - -public class TestDbContextMySql : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString("json"); - b.Property(x => x.JsonObject).AsJsonString("json"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextSqlServer : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextSqlite : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextOracle : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("nvarchar2(255)"); - }); - } -} +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +using SmartEnum.EFCore; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +public class TestDbContext : TestDbContextBase +{ + public DbSet TestEntities { get; set; } = null!; + public DbSet TestEntitiesWithSimpleTypes { get; set; } = null!; + public DbSet TestEntitiesWithJson { get; set; } = null!; + public DbSet TestEntitiesWithGuidId { get; set; } = null!; + public DbSet TestEntitiesWithConverter { get; set; } = null!; + public DbSet TestEntitiesWithComplexType { get; set; } = null!; + public DbSet TestEntitiesWithSmartEnum { get; set; } = null!; + public DbSet TestEntitiesWithSpecialColumnNames { get; set; } = null!; + public DbSet Students { get; set; } = null!; + public DbSet Courses { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureSmartEnum(); + + modelBuilder.Entity(builder => + { + builder.Property(e => e.CreatedAt) + .HasConversion(new DateTimeToBinaryConverter()); + }); + + modelBuilder.Entity(builder => + { + builder + .ComplexProperty(e => e.OwnedComplexType) + .IsRequired(); + }); + + // Many-to-many with shadow property + modelBuilder.Entity() + .HasMany(s => s.Courses) + .WithMany(c => c.Students) + .UsingEntity>( + "StudentCourse", + j => j.HasOne().WithMany().HasForeignKey("CourseId"), + j => j.HasOne().WithMany().HasForeignKey("StudentId"), + j => + { + j.Property("EnrolledAt"); + j.HasKey("StudentId", "CourseId"); + } + ); + + // Keyless entity type + modelBuilder.Entity(builder => + { + builder.HasNoKey(); + // ToView will use the given table name read-only, it doesn't have to actually be a database view. + // We just reuse the table for the standard TestEntity. + builder.ToView("test_entity"); + }); + } +} + +public class TestDbContextPostgreSql : TestDbContext +{ + public DbSet TestEntitiesWithArrays { get; set; } = null!; + public DbSet TestEntitiesWithEnumList { get; set; } = null!; + public DbSet TestEntitiesWithEnumArray { get; set; } = null!; + public DbSet TestEntitiesWithIntList { get; set; } = null!; + public DbSet TestEntitiesWithEnumListExplicitType { get; set; } = null!; + public DbSet TestRecordsWithEnumList { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().Property(e => e.IntArray).HasDefaultValueSql("'{}'"); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString("jsonb"); + b.Property(x => x.JsonObject).AsJsonString("jsonb"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.EnumList).HasColumnType("integer[]"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.TestRun).HasColumnName("test_run"); + b.Property(x => x.Values).HasColumnName("values"); + }); + } +} + +public class TestDbContextMySql : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString("json"); + b.Property(x => x.JsonObject).AsJsonString("json"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextSqlServer : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextSqlite : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextOracle : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("nvarchar2(255)"); + }); + } +}