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)); + } }