From ad0395dcc66f1795a065e866023fc9f315c91b5b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 25 May 2025 22:33:06 +0200 Subject: [PATCH 1/6] Fix tests. --- .../Dialect/SqlDialectBuilder.cs | 2 +- .../DbContext/TestEntity.cs | 5 +- .../DbContext/TestEntityBase.cs | 9 + .../DbContext/TestEntityWithConverters.cs | 5 +- .../DbContext/TestEntityWithGeo.cs | 7 +- .../DbContext/TestEntityWithGuidId.cs | 5 +- .../DbContext/TestEntityWithJson.cs | 5 +- ...ntityFrameworkCore.BulkInsert.Tests.csproj | 4 +- .../TestHelpers.cs | 65 +++ .../Tests/Basic/BasicTestsBase.cs | 369 +++++++----------- .../Tests/Geo/GeoTestsBase.cs | 23 +- 11 files changed, 229 insertions(+), 270 deletions(-) create mode 100644 tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityBase.cs create mode 100644 tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs index 801d044..4fcf0ce 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs @@ -84,7 +84,7 @@ public virtual string BuildMoveDataSql( if (returnedColumns.Count != 0) { - q.Append("RETURNING "); + q.Append(" RETURNING "); q.AppendJoin(", ", returnedColumns.Select(p => p.QuotedColumName)); q.AppendLine(); } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs index 361e3d2..89dfab1 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs @@ -8,7 +8,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; [PrimaryKey(nameof(Id))] [Index(nameof(Name), IsUnique = true)] [Table("test_entity")] -public class TestEntity +public class TestEntity : TestEntityBase { public int Id { get; set; } @@ -19,9 +19,6 @@ public class TestEntity [Column("some_price")] public decimal Price { get; set; } - [Column("test_run")] - public Guid TestRun { get; set; } - [Column("the_identifier")] public Guid Identifier { get; set; } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityBase.cs new file mode 100644 index 0000000..8f063ef --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityBase.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +public abstract class TestEntityBase +{ + [Column("test_run")] + public Guid TestRun { get; set; } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithConverters.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithConverters.cs index ecfc945..041a6be 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithConverters.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithConverters.cs @@ -4,7 +4,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; [Table("test_entity_with_converters")] -public class TestEntityWithConverters +public class TestEntityWithConverters : TestEntityBase { public int Id { get; set; } @@ -14,8 +14,5 @@ public class TestEntityWithConverters [Column("created_at")] public DateTime CreatedAt { get; set; } - - [Column("test_run")] - public Guid TestRun { get; set; } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs index 667645f..705f74a 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using NetTopologySuite.Geometries; @@ -6,13 +6,10 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; [Table("test_entity_geo")] -public class TestEntityWithGeo +public class TestEntityWithGeo : TestEntityBase { [Key] public int Id { get; set; } public Geometry GeoObject { get; set; } = null!; - - [Column("test_run")] - public Guid TestRun { get; set; } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGuidId.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGuidId.cs index e15a28e..2e5e00e 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGuidId.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGuidId.cs @@ -4,7 +4,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; [Table("test_entity_guids")] -public class TestEntityWithGuidId +public class TestEntityWithGuidId : TestEntityBase { [Key] public Guid Id { get; set; } @@ -12,7 +12,4 @@ public class TestEntityWithGuidId [Column("name")] [MaxLength(100)] public string Name { get; set; } = string.Empty; - - [Column("test_run")] - public Guid TestRun { get; set; } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithJson.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithJson.cs index d9915fd..dbde0b1 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithJson.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithJson.cs @@ -4,13 +4,10 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; [Table("test_entity_json")] -public class TestEntityWithJson +public class TestEntityWithJson : TestEntityBase { [Key] public int Id { get; set; } public List Json { get; set; } = []; - - [Column("test_run")] - public Guid TestRun { get; set; } } 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 b701dba..12f938b 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 @@ -13,8 +13,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs new file mode 100644 index 0000000..5857bc5 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; + +using PhenX.EntityFrameworkCore.BulkInsert.Enums; +using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; +using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +using Xunit; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests; + +public enum InsertStrategy +{ + Insert, + InsertReturn, + InsertAsync, + InsertReturnAsync +} + +public static class TestHelpers +{ + public static async Task> InsertWithStrategyAsync( + this TestDbContextBase dbContext, + InsertStrategy strategy, + List entities, + Action? configure = null, + OnConflictOptions? onConflict = null) + where T : TestEntityBase + { + Skip.If(strategy is InsertStrategy.InsertReturn or InsertStrategy.InsertReturnAsync && dbContext.IsProvider(ProviderType.MySql)); + + var runId = Guid.NewGuid(); + if (entities.Any(x => x.TestRun == default)) + { + foreach (var entity in entities) + { + if (entity.TestRun == default) + { + entity.TestRun = runId; + } + } + } + else if (entities.Count > 0) + { + runId = entities[0].TestRun; + } + + var actualConfigure = configure ?? (_ => { }); + switch (strategy) + { + case InsertStrategy.InsertReturn: + return dbContext.ExecuteBulkInsertReturnEntities(entities, actualConfigure, onConflict); + case InsertStrategy.InsertReturnAsync: + return await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities, actualConfigure, onConflict); + case InsertStrategy.Insert: + dbContext.ExecuteBulkInsert(entities, actualConfigure, onConflict); + return dbContext.Set().Where(x => x.TestRun == runId).ToList(); + case InsertStrategy.InsertAsync: + await dbContext.ExecuteBulkInsertAsync(entities, actualConfigure, onConflict); + return await dbContext.Set().Where(x => x.TestRun == runId).ToListAsync(); + default: + throw new NotImplementedException(); + } + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index dba9848..982dfcf 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -1,3 +1,5 @@ +using FluentAssertions; + using PhenX.EntityFrameworkCore.BulkInsert.Enums; using PhenX.EntityFrameworkCore.BulkInsert.Extensions; using PhenX.EntityFrameworkCore.BulkInsert.MySql; @@ -27,91 +29,48 @@ public Task DisposeAsync() return Task.CompletedTask; } - [Fact] - public async Task InsertsEntities() - { - // Arrange - var entities = new List - { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } - }; - - // Act - await _context.ExecuteBulkInsertAsync(entities); - - // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); - } - - [Fact] - public void InsertEntities_Sync() - { - // Arrange - var entities = new List - { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } - }; - - // Act - _context.ExecuteBulkInsert(entities); - - // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); - } - - [SkippableFact] - public async Task InsertEntities_AndReturn() + [SkippableTheory] + [CombinatorialData] + public async Task InsertsEntities(InsertStrategy strategy) { - Skip.If(_context.IsProvider(ProviderType.MySql)); - // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntity { Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity2" } }; // Act - var insertedEntities = await _context.ExecuteBulkInsertReturnEntitiesAsync(entities); + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); // Assert - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [Fact] - public async Task InsertEntities_WithJson() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithJson(InsertStrategy strategy) { // Arrange var entities = new List { - new TestEntityWithJson { TestRun = _run, Json = [1] }, - new TestEntityWithJson { TestRun = _run, Json = [2] } + new TestEntityWithJson { Json = [1] }, + new TestEntityWithJson { Json = [2] } }; // Act - await _context.ExecuteBulkInsertAsync(entities); + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); // Assert - var insertedEntities = _context.TestEntitiesWithJson.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Json[0] == 1); - Assert.Contains(insertedEntities, e => e.Json[0] == 2); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntityWithJson e) => e.Id)); } [SkippableFact] public async Task InsertEntities_AndReturn_AsyncEnumerable() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); // Arrange var entities = new List @@ -122,6 +81,7 @@ public async Task InsertEntities_AndReturn_AsyncEnumerable() // Act var enumerable = _context.ExecuteBulkInsertReturnEnumerableAsync(entities); + var insertedEntities = new List(); await foreach (var item in enumerable) { @@ -129,34 +89,13 @@ public async Task InsertEntities_AndReturn_AsyncEnumerable() } // Assert - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); - } - - [SkippableFact] - public void InsertEntities_AndReturn_Sync() - { - Skip.If(_context.IsProvider(ProviderType.MySql)); - - // Arrange - var entities = new List - { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } - }; - - // Act - var insertedEntities = _context.ExecuteBulkInsertReturnEntities(entities); - - // Assert - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [SkippableFact] - public async Task InsertEntities_MultipleTimes() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_MultipleTimes(InsertStrategy strategy) { Skip.If(_context.IsProvider(ProviderType.PostgreSql)); Skip.If(_context.IsProvider(ProviderType.SqlServer)); @@ -164,8 +103,8 @@ public async Task InsertEntities_MultipleTimes() // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntity { Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity2" } }; // Act @@ -176,27 +115,26 @@ public async Task InsertEntities_MultipleTimes() entity.NumericEnumValue = NumericEnum.Second; } - await _context.ExecuteBulkInsertAsync(entities, + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, onConflict: new OnConflictOptions { Update = e => e, }); // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.NumericEnumValue == NumericEnum.Second); - Assert.Contains(insertedEntities, e => e.NumericEnumValue == NumericEnum.Second); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [SkippableFact] - public async Task InsertEntities_MultipleTimes_WithGuidId() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_MultipleTimes_WithGuidId(InsertStrategy strategy) { // Arrange var entities = new List { - new TestEntityWithGuidId { Id = Guid.NewGuid(), TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntityWithGuidId { Id = Guid.NewGuid(), TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntityWithGuidId { Id = Guid.NewGuid(), Name = $"{_run}_Entity1" }, + new TestEntityWithGuidId { Id = Guid.NewGuid(), Name = $"{_run}_Entity2" } }; // Act @@ -207,39 +145,36 @@ public async Task InsertEntities_MultipleTimes_WithGuidId() entity.Name = $"Updated_{entity.Name}"; } - await _context.ExecuteBulkInsertAsync(entities, + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, onConflict: new OnConflictOptions { Update = e => e, }); // Assert - var insertedEntities = _context.TestEntitiesWithGuidId.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"Updated_{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"Updated_{_run}_Entity2"); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGuidId e) => e.Id)); } - [SkippableFact] - public async Task InsertEntities_MultipleTimes_With_Conflict_On_Id() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_MultipleTimes_With_Conflict_On_Id(InsertStrategy strategy) { // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntity { Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity2" } }; // Act - await _context.ExecuteBulkInsertAsync(entities); - - var insertedEntities0 = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); + var insertedEntities0 = await _context.InsertWithStrategyAsync(strategy, entities); foreach (var entity in insertedEntities0) { entity.Name = $"Updated_{entity.Name}"; } - await _context.ExecuteBulkInsertAsync(insertedEntities0, + var insertedEntities1 = await _context.InsertWithStrategyAsync(strategy, insertedEntities0, o => o.CopyGeneratedColumns = true, onConflict: new OnConflictOptions { @@ -247,53 +182,52 @@ await _context.ExecuteBulkInsertAsync(insertedEntities0, }); // Assert - var insertedEntities1 = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities1.Count); - Assert.Contains(insertedEntities1, e => e.Name == $"Updated_{_run}_Entity1"); - Assert.Contains(insertedEntities1, e => e.Name == $"Updated_{_run}_Entity2"); + insertedEntities1.Should().BeEquivalentTo(insertedEntities0, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [Fact] - public async Task InsertEntities_MoveRows() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_MoveRows(InsertStrategy strategy) { // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntity { Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity2" } }; // Act - await _context.ExecuteBulkInsertAsync(entities, o => + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = true; }); // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [SkippableFact] - public async Task InsertEntities_WithConflict_SingleColumn() + [SkippableTheory] + [InlineData(InsertStrategy.InsertReturn)] + [InlineData(InsertStrategy.InsertReturnAsync)] + public async Task InsertEntities_WithConflict_SingleColumn(InsertStrategy strategy) { Skip.If(_context.IsProvider(ProviderType.MySql)); - _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }); - await _context.SaveChangesAsync(); + // Arrange + _context.TestEntities.Add(new TestEntity { Name = $"{_run}_Entity1" }); + _context.SaveChanges(); _context.ChangeTracker.Clear(); - // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity1" }, new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" }, }; // Act - await _context.ExecuteBulkInsertAsync(entities, o => + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = true; }, new OnConflictOptions @@ -309,19 +243,21 @@ await _context.ExecuteBulkInsertAsync(entities, o => }); // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); Assert.Equal(2, insertedEntities.Count); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1 - Conflict"); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); } - [SkippableFact] - public async Task InsertEntities_WithConflict_DoNothing() + [SkippableTheory] + [InlineData(InsertStrategy.Insert)] + [InlineData(InsertStrategy.InsertAsync)] + public async Task InsertEntities_WithConflict_DoNothing(InsertStrategy strategy) { Skip.If(_context.IsProvider(ProviderType.MySql)); + // Arrange _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }); - await _context.SaveChangesAsync(); + _context.SaveChanges(); _context.ChangeTracker.Clear(); var entities = new List @@ -330,28 +266,31 @@ public async Task InsertEntities_WithConflict_DoNothing() new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" }, }; - await _context.ExecuteBulkInsertAsync(entities, o => + // Act + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = true; }, new OnConflictOptions { Match = e => new { e.Name } - // Pas de Update => DO NOTHING }); - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); + // Assert Assert.Equal(2, insertedEntities.Count); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); } - [SkippableFact] - public async Task InsertEntities_WithConflict_Condition() + [SkippableTheory] + [InlineData(InsertStrategy.InsertReturn)] + [InlineData(InsertStrategy.InsertReturnAsync)] + public async Task InsertEntities_WithConflict_Condition(InsertStrategy strategy) { Skip.If(_context.IsProvider(ProviderType.MySql)); + // Arrange _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 }); - await _context.SaveChangesAsync(); + _context.SaveChanges(); _context.ChangeTracker.Clear(); var entities = new List @@ -360,7 +299,8 @@ public async Task InsertEntities_WithConflict_Condition() new TestEntity { TestRun = _run, Name = $"{_run}_Entity2", Price = 30 }, }; - await _context.ExecuteBulkInsertAsync(entities, o => + // Act + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = true; }, new OnConflictOptions @@ -370,19 +310,22 @@ await _context.ExecuteBulkInsertAsync(entities, o => Condition = "EXCLUDED.some_price > test_entity.some_price" }); - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); + // Assert Assert.Equal(2, insertedEntities.Count); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1" && e.Price == 20); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2" && e.Price == 30); } - [SkippableFact] - public async Task InsertEntities_WithConflict_MultipleColumns() + [SkippableTheory] + [InlineData(InsertStrategy.InsertReturn)] + [InlineData(InsertStrategy.InsertReturnAsync)] + public async Task InsertEntities_WithConflict_MultipleColumns(InsertStrategy strategy) { Skip.If(_context.IsProvider(ProviderType.MySql)); + // Arrange _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 }); - await _context.SaveChangesAsync(); + _context.SaveChanges(); _context.ChangeTracker.Clear(); var entities = new List @@ -391,92 +334,89 @@ public async Task InsertEntities_WithConflict_MultipleColumns() new TestEntity { TestRun = _run, Name = $"{_run}_Entity2", Price = 30, Identifier = Guid.NewGuid() }, }; - await _context.ExecuteBulkInsertAsync(entities, o => + // Act + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = true; }, new OnConflictOptions { Match = e => new { e.Name }, - Update = e => new TestEntity { - Name = e.Name + " - Conflict", - Price = 0, - } + Update = e => new TestEntity { Name = e.Name + " - Conflict", Price = 0 } }); - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); + // Assert Assert.Equal(2, insertedEntities.Count); - Assert.Equal(1, insertedEntities.Count(e => e.Name == $"{_run}_Entity1 - Conflict")); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); - - var entity1 = insertedEntities.First(e => e.Name == $"{_run}_Entity1 - Conflict"); - Assert.Equal(0, entity1.Price); + Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1 - Conflict" && e.Price == 0); } - [Fact] - public async Task InsertEntities_DoesNothing_WhenEntitiesAreEmpty() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_DoesNothing_WhenEntitiesAreEmpty(InsertStrategy strategy) { // Arrange var entities = new List(); // Act - await Assert.ThrowsAsync(async () => await _context.ExecuteBulkInsertAsync(entities)); + await Assert.ThrowsAsync(async () => await _context.InsertWithStrategyAsync(strategy, entities)); // Assert var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); Assert.Empty(insertedEntities); } - [Fact] - public async Task InsertEntities_Many() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_Many(InsertStrategy strategy) { // Arrange const int count = 156055; + var entities = Enumerable.Range(1, count).Select(i => new TestEntity { + Identifier = Guid.NewGuid(), Name = $"{_run}_Entity{i}", + NumericEnumValue = (NumericEnum)(i % 2), Price = (decimal)(i * 0.1), - Identifier = Guid.NewGuid(), StringEnumValue = (StringEnum)(i % 2), - NumericEnumValue = (NumericEnum)(i % 2), - TestRun = _run, }).ToList(); // Act - await _context.ExecuteBulkInsertAsync(entities, o => + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities, o => { o.MoveRows = false; }); // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); Assert.Equal(count, insertedEntities.Count); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity1"); Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity" + count); } - [Fact] - public async Task InsertEntities_AndReturn_WithEntityWithValueConverters() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_AndReturn_WithEntityWithValueConverters(InsertStrategy strategy) { // Arrange var now = DateTime.UtcNow; + var entities = new List { - new() { TestRun = _run, Name = $"{_run}_Entity1", CreatedAt = now }, - new() { TestRun = _run, Name = $"{_run}_Entity2", CreatedAt = now.AddDays(-1) } + new TestEntityWithConverters() { Name = $"{_run}_Entity1", CreatedAt = now }, + new TestEntityWithConverters() { Name = $"{_run}_Entity2", CreatedAt = now.AddDays(-1) } }; // Act - await _context.ExecuteBulkInsertAsync(entities); - var inserted = _context.TestEntitiesWithConverter.Where(x => x.TestRun == _run).ToList(); + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); // Assert - Assert.Equal(2, inserted.Count); - Assert.Contains(inserted, e => e.Name == $"{_run}_Entity1" && e.CreatedAt == now); - Assert.Contains(inserted, e => e.Name == $"{_run}_Entity2" && e.CreatedAt == now.AddDays(-1)); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntityWithConverters e) => e.Id)); } - [Fact] - public async Task InsertEntities_WithOpenTransaction_CommitsSuccessfully() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithOpenTransaction_CommitsSuccessfully(InsertStrategy strategy) { // Arrange var entities = new List @@ -485,42 +425,19 @@ public async Task InsertEntities_WithOpenTransaction_CommitsSuccessfully() new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx2" } }; + // Act await using var transaction = await _context.Database.BeginTransactionAsync(); - - await _context.ExecuteBulkInsertAsync(entities); - + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); await transaction.CommitAsync(); // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_EntityWithTx1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_EntityWithTx2"); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntity e) => e.Id)); } - [Fact] - public void InsertEntities_WithOpenTransaction_CommitsSuccessfully_Sync() - { - // Arrange - var entities = new List - { - new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx2" } - }; - - var transaction = _context.Database.BeginTransaction(); - - _context.ExecuteBulkInsert(entities); - - transaction.Commit(); - - // Assert - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_EntityWithTx1"); - Assert.Contains(insertedEntities, e => e.Name == $"{_run}_EntityWithTx2"); - } - - [Fact] - public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertStrategy strategy) { // Arrange var entities = new List @@ -530,46 +447,20 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure() }; await using var transaction = await _context.Database.BeginTransactionAsync(); - - await _context.ExecuteBulkInsertAsync(entities); - + await _context.InsertWithStrategyAsync(strategy, entities); await transaction.RollbackAsync(); // Assert _context.ChangeTracker.Clear(); - var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail1"); - Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail2"); - } - [Fact] - public void InsertEntities_WithOpenTransaction_RollsBackOnFailure_Sync() - { - // Arrange - var entities = new List - { - new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTxFail1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTxFail2" } - }; - - using var transaction = _context.Database.BeginTransaction(); - - _context.ExecuteBulkInsert(entities); - - transaction.Rollback(); - - // Assert - _context.ChangeTracker.Clear(); var insertedEntities = _context.TestEntities.Where(x => x.TestRun == _run).ToList(); - Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail1"); - Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail2"); + Assert.Empty(insertedEntities); } - + [SkippableFact] public async Task ThrowsWhenUsingWrongConfigurationType() { // Skip for providers that don't support this feature - Skip.If(_context.IsProvider(ProviderType.PostgreSql)); Skip.If(_context.IsProvider(ProviderType.Sqlite)); // Arrange @@ -595,5 +486,13 @@ await _context.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) = { })); } + + if (_context.IsProvider(ProviderType.PostgreSql)) + { + await Assert.ThrowsAsync(async () => + await _context.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) => + { + })); + } } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs index 890d0ba..26b1677 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs @@ -1,6 +1,7 @@ -using NetTopologySuite.Geometries; +using FluentAssertions; + +using NetTopologySuite.Geometries; -using PhenX.EntityFrameworkCore.BulkInsert.Extensions; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; @@ -11,7 +12,6 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Geo; public abstract class GeoTestsBase(TestDbContainer dbContainer) : IAsyncLifetime where TDbContext : TestDbContextGeo, new() { - private readonly Guid _run = Guid.NewGuid(); private TDbContext _context = null!; public async Task InitializeAsync() @@ -25,8 +25,9 @@ public Task DisposeAsync() return Task.CompletedTask; } - [Fact] - public async Task InsertEntities_WithGeo() + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithGeo(InsertStrategy strategy) { // Arrange var geo1 = new Point(1, 2) { SRID = 4326 }; @@ -34,17 +35,15 @@ public async Task InsertEntities_WithGeo() var entities = new List { - new TestEntityWithGeo { TestRun = _run, GeoObject = geo1 }, - new TestEntityWithGeo { TestRun = _run, GeoObject = geo2 } + new TestEntityWithGeo { GeoObject = geo1 }, + new TestEntityWithGeo { GeoObject = geo2 } }; // Act - await _context.ExecuteBulkInsertAsync(entities); + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); // Assert - var insertedEntities = _context.TestEntitiesWithGeo.Where(x => x.TestRun == _run).ToList(); - Assert.Equal(2, insertedEntities.Count); - Assert.Contains(insertedEntities, e => e.GeoObject == geo1); - Assert.Contains(insertedEntities, e => e.GeoObject == geo2); + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); } } From 9d169449e85e261ea2588e248b5a98a5971c4c8c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 25 May 2025 22:58:09 +0200 Subject: [PATCH 2/6] Drop table. --- .../SqliteBulkInsertProvider.cs | 10 ++- .../BulkInsertProviderBase.cs | 67 +++++++++++++++---- .../BulkInsertProviderUntyped.cs | 10 +++ .../Log.cs | 6 ++ 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index f607225..489927c 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -20,10 +20,12 @@ internal class SqliteBulkInsertProvider(ILogger? logge /// protected override string BulkInsertId => "rowid"; - //language=sql /// protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite + /// + protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_test_entity_{Guid.NewGuid():N}"; + /// protected override BulkInsertOptions CreateDefaultOptions() => new() { @@ -116,6 +118,12 @@ private static DbCommand GetInsertCommand( return command; } + /// + protected override Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName) + { + return ExecuteAsync(sync, dbContext, $"DROP TABLE IF EXISTS {tableName}", default); + } + /// protected override async Task BulkInsert( bool sync, diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs index bf9ea0f..8103304 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -43,14 +44,20 @@ protected override async IAsyncEnumerable BulkInsertReturnEntities( } var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, options, tempTableRequired: true, ctk: ctk); + try + { + var result = + await CopyFromTempTableAsync(sync, context, tableInfo, tableName, true, options, onConflict, ctk: ctk) + ?? throw new InvalidOperationException("Copy returns null enumerable."); - var result = - await CopyFromTempTableAsync(sync, context, tableInfo, tableName, true, options, onConflict, ctk: ctk) - ?? throw new InvalidOperationException("Copy returns null enumerable."); - - await foreach (var item in result.WithCancellation(ctk)) + await foreach (var item in result.WithCancellation(ctk)) + { + yield return item; + } + } + finally { - yield return item; + await PerformDropTempTableAsync(sync, context, tableName); } // Commit the transaction if we own them. @@ -71,6 +78,11 @@ protected override async Task BulkInsert( OnConflictOptions? onConflict, CancellationToken ctk) where T : class { + if (entities.TryGetNonEnumeratedCount(out var count) && count == 0) + { + throw new InvalidOperationException("No entities to insert."); + } + using var activity = Telemetry.ActivitySource.StartActivity("BulkInsert"); activity?.AddTag("tableName", tableInfo.TableName); activity?.AddTag("synchronous", sync); @@ -86,8 +98,14 @@ protected override async Task BulkInsert( } var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, options, tempTableRequired: true, ctk: ctk); - - await CopyFromTempTableAsync(sync, context, tableInfo, tableName, false, options, onConflict, ctk); + try + { + await CopyFromTempTableAsync(sync, context, tableInfo, tableName, false, options, onConflict, ctk); + } + finally + { + await PerformDropTempTableAsync(sync, context, tableName); + } } else { @@ -107,7 +125,6 @@ protected override async Task BulkInsert( await connection.Close(sync, ctk); } } - private async Task PerformBulkInsertAsync( bool sync, DbContext context, @@ -117,11 +134,6 @@ private async Task PerformBulkInsertAsync( bool tempTableRequired, CancellationToken ctk) where T : class { - if (entities.TryGetNonEnumeratedCount(out var count) && count == 0) - { - throw new InvalidOperationException("No entities to insert."); - } - var tableName = tempTableRequired ? await CreateTableCopyAsync(sync, context, options, tableInfo, ctk) : tableInfo.QuotedTableName; @@ -208,6 +220,33 @@ protected virtual async Task AddBulkInsertIdColumn( return null; } + private async Task PerformDropTempTableAsync(bool sync, DbContext dbContext, string tableName) + { + try + { + await DropTempTableAsync(sync, dbContext, tableName); + } + catch (Exception ex) + { + // The drop operation is not mandatory, therefore never fail the actual operation. + if (logger != null) + { + Log.DropTemporaryTableFailed(logger, ex); + } + } + } + + /// + /// Drops the temporary table manually if needed. + /// + /// Indicates if the operation is synchronous. + /// The context. + /// The table name. + protected virtual Task DropTempTableAsync(bool sync, DbContext dbContext, string tableName) + { + return Task.CompletedTask; + } + protected static async Task ExecuteAsync( bool sync, DbContext context, diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderUntyped.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderUntyped.cs index baab9b2..28a545a 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderUntyped.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderUntyped.cs @@ -36,6 +36,11 @@ public IAsyncEnumerable BulkInsertReturnEntities( throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}"); } + if (entities.TryGetNonEnumeratedCount(out var count) && count == 0) + { + throw new InvalidOperationException("No entities to insert."); + } + return BulkInsertReturnEntities(sync, context, tableInfo, entities, providerOptions, onConflict, ctk); } @@ -62,6 +67,11 @@ public Task BulkInsert( throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}"); } + if (entities.TryGetNonEnumeratedCount(out var count) && count == 0) + { + throw new InvalidOperationException("No entities to insert."); + } + return BulkInsert(sync, context, tableInfo, entities, providerOptions, onConflict, ctk); } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Log.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Log.cs index d8271e3..b66ebf3 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Log.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Log.cs @@ -21,4 +21,10 @@ internal static partial class Log Level = LogLevel.Trace, Message = "Insert to table directly")] public static partial void UsingDirectInsert(ILogger logger); + + [LoggerMessage( + EventId = 1003, + Level = LogLevel.Error, + Message = "Failed to drop temporary table.")] + public static partial void DropTemporaryTableFailed(ILogger logger, Exception exception); } From 61fbbb938d85c3597f6d9519090a94092cf994b3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 25 May 2025 23:25:18 +0200 Subject: [PATCH 3/6] another fix. --- .../TestHelpers.cs | 33 +++++++++++-------- .../Tests/Basic/BasicTestsBase.cs | 12 +++---- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs index 5857bc5..dac6711 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/TestHelpers.cs @@ -46,20 +46,27 @@ public static async Task> InsertWithStrategyAsync( } var actualConfigure = configure ?? (_ => { }); - switch (strategy) + try { - case InsertStrategy.InsertReturn: - return dbContext.ExecuteBulkInsertReturnEntities(entities, actualConfigure, onConflict); - case InsertStrategy.InsertReturnAsync: - return await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities, actualConfigure, onConflict); - case InsertStrategy.Insert: - dbContext.ExecuteBulkInsert(entities, actualConfigure, onConflict); - return dbContext.Set().Where(x => x.TestRun == runId).ToList(); - case InsertStrategy.InsertAsync: - await dbContext.ExecuteBulkInsertAsync(entities, actualConfigure, onConflict); - return await dbContext.Set().Where(x => x.TestRun == runId).ToListAsync(); - default: - throw new NotImplementedException(); + switch (strategy) + { + case InsertStrategy.InsertReturn: + return dbContext.ExecuteBulkInsertReturnEntities(entities, actualConfigure, onConflict); + case InsertStrategy.InsertReturnAsync: + return await dbContext.ExecuteBulkInsertReturnEntitiesAsync(entities, actualConfigure, onConflict); + case InsertStrategy.Insert: + dbContext.ExecuteBulkInsert(entities, actualConfigure, onConflict); + return dbContext.Set().Where(x => x.TestRun == runId).ToList(); + case InsertStrategy.InsertAsync: + await dbContext.ExecuteBulkInsertAsync(entities, actualConfigure, onConflict); + return await dbContext.Set().Where(x => x.TestRun == runId).ToListAsync(); + default: + throw new NotImplementedException(); + } + } + finally + { + dbContext.ChangeTracker.Clear(); } } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index 982dfcf..be5b93d 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -103,12 +103,12 @@ public async Task InsertEntities_MultipleTimes(InsertStrategy strategy) // Arrange var entities = new List { - new TestEntity { Name = $"{_run}_Entity1" }, - new TestEntity { Name = $"{_run}_Entity2" } + new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, + new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } }; // Act - await _context.ExecuteBulkInsertAsync(entities); + await _context.InsertWithStrategyAsync(strategy, entities); foreach (var entity in entities) { @@ -457,7 +457,7 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertSt Assert.Empty(insertedEntities); } - [SkippableFact] + [Fact] public async Task ThrowsWhenUsingWrongConfigurationType() { // Skip for providers that don't support this feature @@ -466,8 +466,8 @@ public async Task ThrowsWhenUsingWrongConfigurationType() // Arrange var entities = new List { - new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, - new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + new TestEntity { Name = $"{_run}_Entity1" }, + new TestEntity { Name = $"{_run}_Entity2" } }; // Act & Assert From b1685517c7396428161168ef78e4778df7597784 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 25 May 2025 23:29:21 +0200 Subject: [PATCH 4/6] Fix tests --- .../Tests/Basic/BasicTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index be5b93d..0607f81 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -457,7 +457,7 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertSt Assert.Empty(insertedEntities); } - [Fact] + [SkippableFact] public async Task ThrowsWhenUsingWrongConfigurationType() { // Skip for providers that don't support this feature From e6c659f41640fac1706702901a917bcfb824c2d2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 26 May 2025 16:29:15 +0200 Subject: [PATCH 5/6] Default SRID. --- .../MySqlBulkInsertProvider.cs | 2 +- .../MySqlGeometryConverter.cs | 7 +- .../PostgreSqlBulkInsertProvider.cs | 4 +- .../PostgreSqlGeometryConverter.cs | 11 ++- .../SqlServerBulkInsertProvider.cs | 2 +- .../SqlServerGeometryConverter.cs | 7 +- .../SqliteBulkInsertProvider.cs | 73 +++++++++++-------- .../Abstractions/IBulkValueConverter.cs | 7 +- .../EnumerableDataReader.cs | 10 ++- .../Metadata/ColumnMetadata.cs | 9 ++- .../Options/BulkInsertOptions.cs | 5 ++ .../Tests/Geo/GeoTestsBase.cs | 25 +++++++ 12 files changed, 111 insertions(+), 51 deletions(-) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs index f9cd6d1..5d9a2d3 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs @@ -71,7 +71,7 @@ CancellationToken ctk sourceOrdinal++; } - var dataReader = new EnumerableDataReader(entities, properties, options.Converters); + var dataReader = new EnumerableDataReader(entities, properties, options); if (sync) { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs index 8ff526b..95f9f75 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs @@ -1,8 +1,9 @@ -using MySqlConnector; +using MySqlConnector; using NetTopologySuite.Geometries; using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.MySql; @@ -14,11 +15,11 @@ private MySqlGeometryConverter() { } - public bool TryConvertValue(object source, out object result) + public bool TryConvertValue(object source, BulkInsertOptions options, out object result) { if (source is Geometry geometry) { - result = MySqlGeometry.FromWkb(geometry.SRID, geometry.ToBinary()); + result = MySqlGeometry.FromWkb(options.SRID, geometry.ToBinary()); return true; } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs index 9173122..7904ef7 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs @@ -57,8 +57,6 @@ protected override async Task BulkInsert( ? connection.BeginBinaryImport(command) : await connection.BeginBinaryImportAsync(command, ctk); - var bulkValueConverters = options.Converters; - // The type mapping can be null for obvious types like string. var columnTypes = columns.Select(c => GetPostgreSqlType(c, options)).ToArray(); @@ -76,7 +74,7 @@ protected override async Task BulkInsert( for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++) { - var value = columns[columnIndex].GetValue(entity, bulkValueConverters); + var value = columns[columnIndex].GetValue(entity, options); // Get the actual type, so that the writer can do the conversation to the target type automatically. var type = columnTypes[columnIndex]; diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs index 8ff1d51..93c913c 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs @@ -1,10 +1,11 @@ -using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata; using NetTopologySuite.Geometries; using NpgsqlTypes; using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; @@ -16,10 +17,16 @@ private PostgreSqlGeometryConverter() { } - public bool TryConvertValue(object source, out object result) + public bool TryConvertValue(object source, BulkInsertOptions options, out object result) { if (source is Geometry geometry) { + if (geometry.SRID != options.SRID) + { + geometry = geometry.Copy(); + geometry.SRID = options.SRID; + } + result = geometry.ToBinary(); return true; } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs index d6cefd2..496217b 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs @@ -51,7 +51,7 @@ protected override async Task BulkInsert( bulkCopy.ColumnMappings.Add(column.PropertyName, column.ColumnName); } - var dataReader = new EnumerableDataReader(entities, columns, options.Converters); + var dataReader = new EnumerableDataReader(entities, columns, options); if (sync) { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs index da64860..2b9a866 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs @@ -1,10 +1,11 @@ -using System.Data.SqlTypes; +using System.Data.SqlTypes; using Microsoft.SqlServer.Types; using NetTopologySuite.Geometries; using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer; @@ -16,12 +17,12 @@ private SqlServerGeometryConverter() { } - public bool TryConvertValue(object source, out object result) + public bool TryConvertValue(object source, BulkInsertOptions options, out object result) { if (source is Geometry geometry) { var reversed = Reverse(geometry); - result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), geometry.SRID); + result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), options.SRID); return true; } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index 489927c..007aad4 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -144,37 +144,48 @@ CancellationToken ctk var columnList = tableInfo.GetColumns(options.CopyGeneratedColumns); var columnTypes = columnList.Select(GetSqliteType).ToArray(); - await using var insertCommand = - GetInsertCommand( - context, - tableName, - columnList, - columnTypes, - sb, - batchSize); - - foreach (var chunk in entities.Chunk(batchSize)) + DbCommand? insertCommand = null; + try { - // Full chunks - if (chunk.Length == batchSize) + foreach (var chunk in entities.Chunk(batchSize)) { - FillValues(chunk, insertCommand.Parameters, columns); - await ExecuteCommand(sync, insertCommand, ctk); + // Full chunks + if (chunk.Length == batchSize) + { + insertCommand ??= + GetInsertCommand( + context, + tableName, + columnList, + columnTypes, + sb, + batchSize); + + FillValues(chunk, insertCommand.Parameters, columns, options); + await ExecuteCommand(sync, insertCommand, ctk); + } + // Last chunk + else + { + await using var partialInsertCommand = + GetInsertCommand( + context, + tableName, + columnList, + columnTypes, + sb, + chunk.Length); + + FillValues(chunk, partialInsertCommand.Parameters, columns, options); + await ExecuteCommand(sync, partialInsertCommand, ctk); + } } - // Last chunk - else + } + finally + { + if (insertCommand != null) { - await using var partialInsertCommand = - GetInsertCommand( - context, - tableName, - columnList, - columnTypes, - sb, - chunk.Length); - - FillValues(chunk, partialInsertCommand.Parameters, columns); - await ExecuteCommand(sync, partialInsertCommand, ctk); + await insertCommand.DisposeAsync(); } } } @@ -192,7 +203,11 @@ private static async Task ExecuteCommand(bool sync, DbCommand insertCommand, Can } } - private static void FillValues(T[] chunk, DbParameterCollection parameters, IReadOnlyList columns) where T : class + private static void FillValues( + T[] chunk, + DbParameterCollection parameters, + IReadOnlyList columns, + BulkInsertOptions options) where T : class { var p = 0; @@ -203,7 +218,7 @@ private static void FillValues(T[] chunk, DbParameterCollection parameters, I for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++) { var column = columns[columnIndex]; - var value = column.GetValue(entity, null); + var value = column.GetValue(entity, options); parameters[p].Value = value; p++; } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs index 3197b64..58952cc 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs @@ -1,4 +1,6 @@ -namespace PhenX.EntityFrameworkCore.BulkInsert.Abstractions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Abstractions; /// /// Provide an interface to control how objects are written. @@ -10,6 +12,7 @@ public interface IBulkValueConverter /// /// The source object. /// The result type. + /// The options. /// Indicates if an object should be written. - bool TryConvertValue(object source, out object result); + bool TryConvertValue(object source, BulkInsertOptions options, out object result); } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs index 620c445..6650388 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs @@ -2,10 +2,14 @@ using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Metadata; +using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert; -internal sealed class EnumerableDataReader(IEnumerable rows, IReadOnlyList columns, List? converters) : IDataReader +internal sealed class EnumerableDataReader( + IEnumerable rows, + IReadOnlyList columns, + BulkInsertOptions options) : IDataReader { private readonly IEnumerator _enumerator = rows.GetEnumerator(); private readonly Dictionary _ordinalMap = @@ -24,7 +28,7 @@ public object GetValue(int i) return DBNull.Value; } - return columns[i].GetValue(current, converters)!; + return columns[i].GetValue(current, options)!; } public int GetValues(object[] values) @@ -37,7 +41,7 @@ public int GetValues(object[] values) for (var i = 0; i < columns.Count; i++) { - values[i] = columns[i].GetValue(current, converters)!; + values[i] = columns[i].GetValue(current, options)!; } return columns.Count; diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs index c5226c6..3d51957 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs @@ -3,6 +3,7 @@ using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Dialect; +using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; @@ -24,15 +25,15 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial public bool IsGenerated { get; } = property.ValueGenerated == ValueGenerated.OnAdd; - public object? GetValue(object entity, List? converters) + public object? GetValue(object entity, BulkInsertOptions options) { var result = _getter(entity); - if (converters != null && result != null) + if (options.Converters != null && result != null) { - foreach (var converter in converters) + foreach (var converter in options.Converters) { - if (converter.TryConvertValue(result, out var temp)) + if (converter.TryConvertValue(result, options, out var temp)) { result = temp; break; diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs index d58f12f..ffa775f 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs @@ -50,6 +50,11 @@ public class BulkInsertOptions /// public List? Converters { get; set; } + /// + /// Sets the ID of the Spatial Reference System used by the Geometries to be inserted. + /// + public int SRID { get; set; } = 4326; + internal int GetCopyTimeoutInSeconds() { return Math.Max(0, (int)CopyTimeout.TotalSeconds); diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs index 26b1677..38c8fb1 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs @@ -46,4 +46,29 @@ public async Task InsertEntities_WithGeo(InsertStrategy strategy) insertedEntities.Should().BeEquivalentTo(entities, o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); } + + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithGeo_And_Default_SRID(InsertStrategy strategy) + { + // Arrange + var geo1 = new Point(1, 2); + var geo2 = new Point(3, 4); + + var entities = new List + { + new TestEntityWithGeo { GeoObject = geo1 }, + new TestEntityWithGeo { GeoObject = geo2 } + }; + + // Act + var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities); + + geo1.SRID = 4326; + geo2.SRID = 4326; + + // Assert + insertedEntities.Should().BeEquivalentTo(entities, + o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); + } } From 0193298551d5fd85a1e4ad25dc828bde7e17942b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 26 May 2025 17:43:03 +0200 Subject: [PATCH 6/6] Fix geography. --- .../PostgreSqlBulkInsertProvider.cs | 1 - .../PostgreSqlGeometryConverter.cs | 20 ++------------ .../Tests/Geo/GeoTestsBase.cs | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs index 7904ef7..d7a6e35 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs @@ -35,7 +35,6 @@ private static string GetBinaryImportCommand(IReadOnlyList prope { BatchSize = 50_000, Converters = [PostgreSqlGeometryConverter.Instance], - TypeProviders = [PostgreSqlGeometryConverter.Instance], }; /// diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs index 93c913c..af78164 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs @@ -1,15 +1,11 @@ -using Microsoft.EntityFrameworkCore.Metadata; - using NetTopologySuite.Geometries; -using NpgsqlTypes; - using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; -internal sealed class PostgreSqlGeometryConverter : IBulkValueConverter, IPostgresTypeProvider +internal sealed class PostgreSqlGeometryConverter : IBulkValueConverter { public static readonly PostgreSqlGeometryConverter Instance = new(); @@ -27,23 +23,11 @@ public bool TryConvertValue(object source, BulkInsertOptions options, out object geometry.SRID = options.SRID; } - result = geometry.ToBinary(); + result = geometry; return true; } result = source; return false; } - - public bool TryGetType(IProperty property, out NpgsqlDbType result) - { - if (property.ClrType.IsAssignableTo(typeof(Geometry))) - { - result = NpgsqlDbType.Bytea; - return true; - } - - result = default; - return false; - } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs index 38c8fb1..bbc7f82 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs @@ -1,5 +1,7 @@ using FluentAssertions; +using Microsoft.EntityFrameworkCore; + using NetTopologySuite.Geometries; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; @@ -71,4 +73,29 @@ public async Task InsertEntities_WithGeo_And_Default_SRID(InsertStrategy strateg insertedEntities.Should().BeEquivalentTo(entities, o => o.RespectingRuntimeTypes().Excluding((TestEntityWithGeo e) => e.Id)); } + + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithGeo_And_Search(InsertStrategy strategy) + { + // Arrange + var runId = Guid.NewGuid(); + + var geo1 = new Point(1, 2) { SRID = 4326 }; + var geo2 = new Point(3, 4) { SRID = 4326 }; + + var entities = new List + { + new TestEntityWithGeo { TestRun = runId, GeoObject = geo1 }, + new TestEntityWithGeo { TestRun = runId, GeoObject = geo2 } + }; + + // Act + await _context.InsertWithStrategyAsync(strategy, entities); + + var found = await _context.TestEntitiesWithGeo.Where(x => x.TestRun == runId && x.GeoObject.Distance(geo1) < 1).ToListAsync(); + + // Assert + Assert.NotEmpty(found); + } }