diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index c50945b..862a6a9 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -16,17 +16,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup .NET 8.0 uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + - name: Setup .NET 9.0 uses: actions/setup-dotnet@v4 with: dotnet-version: 9.0.x + - name: Restore dependencies run: dotnet restore + - name: Build run: dotnet build --no-restore + - name: Test run: dotnet test --no-build --verbosity normal diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs index 2b2ef20..602759e 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs @@ -22,7 +22,10 @@ internal class MySqlBulkInsertProvider(ILogger logger) protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; /// - protected override MySqlBulkInsertOptions CreateDefaultOptions() => new(); + protected override MySqlBulkInsertOptions CreateDefaultOptions() => new() + { + Converters = [MySqlGeometryConverter.Instance] + }; /// protected override IAsyncEnumerable BulkInsertReturnEntities( @@ -71,11 +74,11 @@ CancellationToken ctk if (sync) { // ReSharper disable once MethodHasAsyncOverloadWithCancellation - bulkCopy.WriteToServer(new EnumerableDataReader(entities, properties)); + bulkCopy.WriteToServer(new EnumerableDataReader(entities, properties, options.Converters)); } else { - await bulkCopy.WriteToServerAsync(new EnumerableDataReader(entities, properties), ctk); + await bulkCopy.WriteToServerAsync(new EnumerableDataReader(entities, properties, options.Converters), ctk); } } } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs new file mode 100644 index 0000000..8ff526b --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlGeometryConverter.cs @@ -0,0 +1,28 @@ +using MySqlConnector; + +using NetTopologySuite.Geometries; + +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + +namespace PhenX.EntityFrameworkCore.BulkInsert.MySql; + +internal sealed class MySqlGeometryConverter : IBulkValueConverter +{ + public static readonly MySqlGeometryConverter Instance = new(); + + private MySqlGeometryConverter() + { + } + + public bool TryConvertValue(object source, out object result) + { + if (source is Geometry geometry) + { + result = MySqlGeometry.FromWkb(geometry.SRID, geometry.ToBinary()); + return true; + } + + result = source; + return false; + } +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj index baca930..2c6d8e7 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj @@ -9,6 +9,7 @@ + diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/IPostgresTypeProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/IPostgresTypeProvider.cs new file mode 100644 index 0000000..83e4fd4 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/IPostgresTypeProvider.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Metadata; + +using NpgsqlTypes; + +namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; + +/// +/// Provides the type to write. +/// +public interface IPostgresTypeProvider +{ + /// + /// Gets the type of a value before written to the output. + /// + /// The source property. + /// The result type. + /// Indicates if an object should be written. + bool TryGetType(IProperty property, out NpgsqlDbType result); +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj index 7c40821..4c56c76 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj @@ -5,6 +5,7 @@ + diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertOptions.cs new file mode 100644 index 0000000..6a21b3e --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertOptions.cs @@ -0,0 +1,14 @@ +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; + +/// +/// Options specific to SQL Server bulk insert. +/// +public class PostgreSqlBulkInsertOptions : BulkInsertOptions +{ + /// + /// A list of type providers. + /// + public List? TypeProviders { get; set; } +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs index e571558..c8d7f6d 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs @@ -11,12 +11,11 @@ using NpgsqlTypes; using PhenX.EntityFrameworkCore.BulkInsert.Metadata; -using PhenX.EntityFrameworkCore.BulkInsert.Options; namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; [UsedImplicitly] -internal class PostgreSqlBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger) +internal class PostgreSqlBulkInsertProvider(ILogger? logger) : BulkInsertProviderBase(logger) { //language=sql /// @@ -32,9 +31,11 @@ private static string GetBinaryImportCommand(IReadOnlyList prope } /// - protected override BulkInsertOptions CreateDefaultOptions() => new() + protected override PostgreSqlBulkInsertOptions CreateDefaultOptions() => new() { BatchSize = 50_000, + Converters = [PostgreSqlGeometryConverter.Instance], + TypeProviders = [PostgreSqlGeometryConverter.Instance], }; /// @@ -45,7 +46,7 @@ protected override async Task BulkInsert( IEnumerable entities, string tableName, IReadOnlyList columns, - BulkInsertOptions options, + PostgreSqlBulkInsertOptions options, CancellationToken ctk) { var connection = (NpgsqlConnection)context.Database.GetDbConnection(); @@ -57,7 +58,7 @@ protected override async Task BulkInsert( : await connection.BeginBinaryImportAsync(command, ctk); // The type mapping can be null for obvious types like string. - var columnTypes = columns.Select(GetPostgreSqlType).ToArray(); + var columnTypes = columns.Select(c => GetPostgreSqlType(c, options)).ToArray(); foreach (var entity in entities) { @@ -74,7 +75,7 @@ protected override async Task BulkInsert( var columnIndex = 0; foreach (var column in columns) { - var value = column.GetValue(entity); + var value = column.GetValue(entity, options.Converters); // Get the actual type, so that the writer can do the conversation to the target type automatically. var type = columnTypes[columnIndex]; @@ -122,8 +123,20 @@ protected override async Task BulkInsert( } } - private static NpgsqlDbType? GetPostgreSqlType(ColumnMetadata column) + private static NpgsqlDbType? GetPostgreSqlType(ColumnMetadata column, PostgreSqlBulkInsertOptions options) { + var typeProviders = options.TypeProviders; + if (typeProviders is { Count: > 0 }) + { + foreach (var typeProvider in typeProviders) + { + if (typeProvider.TryGetType(column.Property, out var type)) + { + return type; + } + } + } + var mapping = column.Property.GetRelationalTypeMapping() as NpgsqlTypeMapping; return mapping?.NpgsqlDbType; diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs new file mode 100644 index 0000000..8ff1d51 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlGeometryConverter.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Metadata; + +using NetTopologySuite.Geometries; + +using NpgsqlTypes; + +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + +namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; + +internal sealed class PostgreSqlGeometryConverter : IBulkValueConverter, IPostgresTypeProvider +{ + public static readonly PostgreSqlGeometryConverter Instance = new(); + + private PostgreSqlGeometryConverter() + { + } + + public bool TryConvertValue(object source, out object result) + { + if (source is Geometry geometry) + { + result = geometry.ToBinary(); + 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/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj index 19117ce..835f8e5 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.csproj @@ -5,6 +5,8 @@ + + diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs index 9fa08d1..dd9bd71 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs @@ -22,6 +22,7 @@ internal class SqlServerBulkInsertProvider(ILogger? protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new() { BatchSize = 50_000, + Converters = [SqlServerGeometryConverter.Instance] }; /// @@ -53,11 +54,11 @@ protected override async Task BulkInsert( if (sync) { // ReSharper disable once MethodHasAsyncOverloadWithCancellation - bulkCopy.WriteToServer(new EnumerableDataReader(entities, columns)); + bulkCopy.WriteToServer(new EnumerableDataReader(entities, columns, options.Converters)); } else { - await bulkCopy.WriteToServerAsync(new EnumerableDataReader(entities, columns), ctk); + await bulkCopy.WriteToServerAsync(new EnumerableDataReader(entities, columns, options.Converters), ctk); } } } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs new file mode 100644 index 0000000..da64860 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerGeometryConverter.cs @@ -0,0 +1,105 @@ +using System.Data.SqlTypes; + +using Microsoft.SqlServer.Types; + +using NetTopologySuite.Geometries; + +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + +namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer; + +internal sealed class SqlServerGeometryConverter : IBulkValueConverter +{ + public static readonly SqlServerGeometryConverter Instance = new(); + + private SqlServerGeometryConverter() + { + } + + public bool TryConvertValue(object source, out object result) + { + if (source is Geometry geometry) + { + var reversed = Reverse(geometry); + result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), geometry.SRID); + return true; + } + + result = source; + return false; + } + + private static Geometry Reverse(Geometry input) + { + switch (input) + { + case Point point: + return Reverse(point); + + case LineString lineString: + return Reverse(lineString); + + case Polygon polygon: + return Reverse(polygon); + + case MultiPoint multiPoint: + return Reverse(multiPoint); + + case MultiLineString multiLineString: + return Reverse(multiLineString); + + case MultiPolygon mpoly: + return Reverse(mpoly); + + case GeometryCollection gc: + return Reverse(gc); + + default: + throw new NotSupportedException($"Unsupported geometry type: {input.GeometryType}"); + } + } + + private static Point Reverse(Point input) + { + return input.Factory.CreatePoint(Swap(input.Coordinate)); + } + + private static LineString Reverse(LineString input) + { + return input.Factory.CreateLineString(Swap(input.Coordinates)); + } + + private static MultiPoint Reverse(MultiPoint input) + { + return input.Factory.CreateMultiPoint(input.Geometries.OfType().Select(Reverse).ToArray()); + } + + private static MultiLineString Reverse(MultiLineString input) + { + return input.Factory.CreateMultiLineString(input.Geometries.OfType().Select(Reverse).ToArray()); + } + + private static MultiPolygon Reverse(MultiPolygon input) + { + return input.Factory.CreateMultiPolygon(input.Geometries.OfType().Select(Reverse).ToArray()); + } + + private static GeometryCollection Reverse(GeometryCollection input) + { + return input.Factory.CreateGeometryCollection(input.Geometries.Select(Reverse).ToArray()); + } + + private static Polygon Reverse(Polygon input) + { + var factory = input.Factory; + + return input.Factory.CreatePolygon( + factory.CreateLinearRing(Swap(input.Shell.Coordinates)), + input.Holes.Select(h => factory.CreateLinearRing(Swap(h.Coordinates))).ToArray()); + } + + private static Coordinate Swap(Coordinate c) => new Coordinate(c.Y, c.X); + + private static Coordinate[] Swap(Coordinate[] coords) => coords.Select(Swap).ToArray(); + +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index 0e064f4..d8cc12e 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -190,7 +190,7 @@ private static void FillValues(T[] chunk, DbParameterCollection parameters, I { foreach (var column in columns) { - var value = column.GetValue(entity); + var value = column.GetValue(entity, null); parameters[p].Value = value; p++; } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs new file mode 100644 index 0000000..3197b64 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkValueConverter.cs @@ -0,0 +1,15 @@ +namespace PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + +/// +/// Provide an interface to control how objects are written. +/// +public interface IBulkValueConverter +{ + /// + /// Converts a value before written to the output. + /// + /// The source object. + /// The result type. + /// Indicates if an object should be written. + bool TryConvertValue(object source, out object result); +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs index d57b43c..620c445 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs @@ -1,10 +1,11 @@ using System.Data; +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Metadata; namespace PhenX.EntityFrameworkCore.BulkInsert; -internal sealed class EnumerableDataReader(IEnumerable rows, IReadOnlyList columns) : IDataReader +internal sealed class EnumerableDataReader(IEnumerable rows, IReadOnlyList columns, List? converters) : IDataReader { private readonly IEnumerator _enumerator = rows.GetEnumerator(); private readonly Dictionary _ordinalMap = @@ -23,7 +24,7 @@ public object GetValue(int i) return DBNull.Value; } - return columns[i].GetValue(current)!; + return columns[i].GetValue(current, converters)!; } public int GetValues(object[] values) @@ -36,7 +37,7 @@ public int GetValues(object[] values) for (var i = 0; i < columns.Count; i++) { - values[i] = columns[i].GetValue(current)!; + values[i] = columns[i].GetValue(current, converters)!; } return columns.Count; diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs index 6e06524..5ccd173 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Dialect; namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; @@ -25,9 +26,23 @@ internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dial public bool IsGenerated { get; } = property.ValueGenerated == ValueGenerated.OnAdd; - public object? GetValue(object entity) + public object? GetValue(object entity, List? converters) { - return _getter(entity!); + var result = _getter(entity!); + + if (converters != null && result != null) + { + foreach (var converter in converters) + { + if (converter.TryConvertValue(result, out var temp)) + { + result = temp; + break; + } + } + } + + return result; } private static PropertyAccessor.Getter BuildGetter(IProperty property) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs index cfc94be..c33ad2e 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs @@ -7,7 +7,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; internal sealed class MetadataProvider { - private Dictionary? _tables; + private Dictionary> _tablesPerContext = new(); public TableMetadata GetTableInfo(DbContext context) { @@ -23,26 +23,25 @@ public TableMetadata GetTableInfo(DbContext context) private Dictionary GetTables(DbContext context) { - if (_tables != null) + lock (_tablesPerContext) { - return _tables; - } - - lock (this) - { - if (_tables != null) + var type = context.GetType(); + if (_tablesPerContext.TryGetValue(context.GetType(), out var tables)) { - return _tables; + return tables; } var provider = context.GetService(); - _tables = + tables = context.Model.GetEntityTypes() .ToDictionary( x => x.ClrType, x => new TableMetadata(x, provider.SqlDialect)); - return _tables; + + _tablesPerContext[type] = tables; + + return tables; } } } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs index 19f4835..d58f12f 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs @@ -1,3 +1,5 @@ +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + namespace PhenX.EntityFrameworkCore.BulkInsert.Options; /// @@ -43,6 +45,11 @@ public class BulkInsertOptions /// public TimeSpan CopyTimeout { get; set; } = TimeSpan.FromMinutes(10); + /// + /// The value converters. + /// + public List? Converters { get; set; } + internal int GetCopyTimeoutInSeconds() { return Math.Max(0, (int)CopyTimeout.TotalSeconds); diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs index 3a50d54..ac63439 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainer.cs @@ -1,3 +1,5 @@ +using System.Data.Common; + using DotNet.Testcontainers.Containers; using Microsoft.EntityFrameworkCore; @@ -9,10 +11,10 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; -public abstract class TestDbContainer : IAsyncLifetime - where TDbContext : TestDbContextBase, new() +public abstract class TestDbContainer : IAsyncLifetime { - private static readonly TimeSpan WaitTime = TimeSpan.FromSeconds(30); + private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(30); + private readonly HashSet _connected = []; protected readonly IDatabaseContainer? DbContainer; protected TestDbContainer() @@ -22,12 +24,23 @@ protected TestDbContainer() protected abstract IDatabaseContainer? GetDbContainer(); - protected virtual string GetConnectionString() + protected virtual string GetConnectionString(string databaseName) { - return DbContainer?.GetConnectionString() ?? string.Empty; + if (DbContainer == null) + { + return string.Empty; + } + + var builder = new DbConnectionStringBuilder() + { + ConnectionString = DbContainer.GetConnectionString() + }; + + builder["database"] = databaseName; + return builder.ToString(); } - protected abstract void Configure(DbContextOptionsBuilder optionsBuilder); + protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName); public async Task InitializeAsync() { @@ -37,20 +50,23 @@ public async Task InitializeAsync() } } - public async Task CreateContextAsync() + public async Task CreateContextAsync(string databaseName) + where TDbContext : TestDbContextBase, new() { var dbContext = new TDbContext { ConfigureOptions = (builder) => { builder.UseLoggerFactory(NullLoggerFactory.Instance); - Configure(builder); + Configure(builder, databaseName); } }; - dbContext.Database.SetConnectionString(GetConnectionString()); + if (_connected.Add(databaseName)) + { + await EnsureConnectedAsync(dbContext, databaseName); + } - await EnsureConnectedAsync(dbContext); try { await dbContext.Database.EnsureCreatedAsync(); @@ -63,9 +79,11 @@ public async Task CreateContextAsync() return dbContext; } - protected virtual async Task EnsureConnectedAsync(TDbContext context) + protected virtual async Task EnsureConnectedAsync(TDbContext context, string databaseName) + where TDbContext : TestDbContextBase { - using var cts = new CancellationTokenSource(WaitTime); + using var cts = new CancellationTokenSource(_waitTime); + while (!await context.Database.CanConnectAsync(cts.Token)) { await Task.Delay(100, cts.Token); diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerMySql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerMySql.cs index 8111c1d..904c616 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerMySql.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerMySql.cs @@ -3,32 +3,53 @@ using Microsoft.EntityFrameworkCore; using PhenX.EntityFrameworkCore.BulkInsert.MySql; -using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; using Testcontainers.MySql; +using Xunit; + namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; -public class TestDbContainerMySql : TestDbContainer - where TDbContext : TestDbContextBase, new() +[CollectionDefinition(Name)] +public class TestDbContainerMySqlCollection : ICollectionFixture +{ + public const string Name = "MySql"; +} + +public class TestDbContainerMySql() : TestDbContainer { protected override IDatabaseContainer? GetDbContainer() { return new MySqlBuilder() .WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1", "--innodb-print-all-deadlocks=ON") .WithReuse(true) + .WithUsername("root") + .WithPassword("root") .Build(); } - protected override string GetConnectionString() + protected override string GetConnectionString(string databaseName) { - return $"{base.GetConnectionString()};AllowLoadLocalInfile=true;"; + return $"{base.GetConnectionString(databaseName)};AllowLoadLocalInfile=true;"; } - protected override void Configure(DbContextOptionsBuilder optionsBuilder) + protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) { + var connectionString = GetConnectionString(databaseName); + optionsBuilder - .UseMySql(ServerVersion.AutoDetect(GetConnectionString())) + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), o => + { + o.UseNetTopologySuite(); + }) .UseBulkInsertMySql(); } + + protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) + { + var container = (MySqlContainer)DbContainer!; + + await container.ExecScriptAsync($"CREATE DATABASE `{databaseName}`"); + await base.EnsureConnectedAsync(context, databaseName); + } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSql.cs index 22f2ab7..b03a7cc 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSql.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerPostgreSql.cs @@ -3,18 +3,25 @@ using Microsoft.EntityFrameworkCore; using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; -using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; using Testcontainers.PostgreSql; +using Xunit; + namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; -public class TestDbContainerPostgreSql : TestDbContainer - where TDbContext : TestDbContextBase, new() +[CollectionDefinition(Name)] +public class TestDbContainerPostgreSqlCollection : ICollectionFixture +{ + public const string Name = "PostgreSql"; +} + +public class TestDbContainerPostgreSql : TestDbContainer { protected override IDatabaseContainer? GetDbContainer() { return new PostgreSqlBuilder() + .WithImage("postgis/postgis") // Geo GeoSpatial support. .WithReuse(true) .WithDatabase("testdb") .WithUsername("testuser") @@ -22,10 +29,21 @@ public class TestDbContainerPostgreSql : TestDbContainer .Build(); } - protected override void Configure(DbContextOptionsBuilder optionsBuilder) + protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) { optionsBuilder - .UseNpgsql() + .UseNpgsql(GetConnectionString(databaseName), o => + { + o.UseNetTopologySuite(); + }) .UseBulkInsertPostgreSql(); } + + protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) + { + var container = (PostgreSqlContainer)DbContainer!; + + await container.ExecScriptAsync($"CREATE DATABASE \"{databaseName}\""); + await base.EnsureConnectedAsync(context, databaseName); + } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlServer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlServer.cs index cddcf06..a11f7ac 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlServer.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlServer.cs @@ -3,26 +3,44 @@ using Microsoft.EntityFrameworkCore; using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; -using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; using Testcontainers.MsSql; +using Xunit; + namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; -public class TestDbContainerSqlServer : TestDbContainer - where TDbContext : TestDbContextBase, new() +[CollectionDefinition(Name)] +public class TestDbContainerSqlServerCollection : ICollectionFixture +{ + public const string Name = "SqlServer"; +} + +public class TestDbContainerSqlServer : TestDbContainer { protected override IDatabaseContainer? GetDbContainer() { return new MsSqlBuilder() + .WithImage("vibs2006/sql_server_fts") // Geo Geospatial support .WithReuse(true) .Build(); } - protected override void Configure(DbContextOptionsBuilder optionsBuilder) + protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) { optionsBuilder - .UseSqlServer() + .UseSqlServer(GetConnectionString(databaseName), o => + { + o.UseNetTopologySuite(); + }) .UseBulkInsertSqlServer(); } + + protected override async Task EnsureConnectedAsync(TDbContext context, string databaseName) + { + var container = (MsSqlContainer)DbContainer!; + + await container.ExecScriptAsync($"CREATE DATABASE [{databaseName}]"); + await base.EnsureConnectedAsync(context, databaseName); + } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlite.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlite.cs index b6e12bf..c0245c5 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlite.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContainer/TestDbContainerSqlite.cs @@ -3,29 +3,34 @@ using Microsoft.EntityFrameworkCore; using PhenX.EntityFrameworkCore.BulkInsert.Sqlite; -using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +using Xunit; namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; -public class TestDbContainerSqlite : TestDbContainer - where TDbContext : TestDbContextBase, new() +[CollectionDefinition(Name)] +public class TestDbContainerSqliteCollection : ICollectionFixture +{ + public const string Name = "Sqlite"; +} + +public class TestDbContainerSqlite : TestDbContainer { protected override IDatabaseContainer? GetDbContainer() => null; - protected override string GetConnectionString() + protected override string GetConnectionString(string databaseName) { - // return "Data Source=:memory:;Mode=Memory;Cache=Shared"; return $"Data Source={Guid.NewGuid()}.db"; } - protected override void Configure(DbContextOptionsBuilder optionsBuilder) + protected override void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName) { optionsBuilder - .UseSqlite() + .UseSqlite(GetConnectionString(databaseName)) .UseBulkInsertSqlite(); } - protected override Task EnsureConnectedAsync(TDbContext context) + protected override Task EnsureConnectedAsync(TDbContext context, string databaseName) { return Task.CompletedTask; } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs index 9c67ab8..a15504a 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs @@ -7,8 +7,8 @@ public class TestDbContext : TestDbContextBase { public DbSet TestEntities { get; set; } = null!; public DbSet TestEntitiesWithJson { get; set; } = null!; - public DbSet TestEntitiesWithGuidIds { get; set; } = null!; - public DbSet TestEntitiesWithConverters { get; set; } = null!; + public DbSet TestEntitiesWithGuidId { get; set; } = null!; + public DbSet TestEntitiesWithConverter { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContextGeo.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContextGeo.cs new file mode 100644 index 0000000..6e2f810 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContextGeo.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +public class TestDbContextGeo : TestDbContextBase +{ + public DbSet TestEntitiesWithGeo { get; set; } = null!; +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs new file mode 100644 index 0000000..667645f --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityWithGeo.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using NetTopologySuite.Geometries; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +[Table("test_entity_geo")] +public class TestEntityWithGeo +{ + [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/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj index 009db83..9f5facd 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/PhenX.EntityFrameworkCore.BulkInsert.Tests.csproj @@ -13,7 +13,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index bb01dd1..dba9848 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -10,16 +10,15 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; -public abstract class BasicTestsBase(TestDbContainer dbContainer) : IClassFixture, IAsyncLifetime +public abstract class BasicTestsBase(TestDbContainer dbContainer) : IAsyncLifetime where TDbContext : TestDbContext, new() - where TFixture : TestDbContainer { private readonly Guid _run = Guid.NewGuid(); private TDbContext _context = null!; public async Task InitializeAsync() { - _context = await DbContainer.CreateContextAsync(); + _context = await dbContainer.CreateContextAsync("basic"); } public Task DisposeAsync() @@ -28,8 +27,6 @@ public Task DisposeAsync() return Task.CompletedTask; } - protected TestDbContainer DbContainer { get; } = dbContainer; - [Fact] public async Task InsertsEntities() { @@ -51,7 +48,7 @@ public async Task InsertsEntities() } [Fact] - public void InsertsEntities_Sync() + public void InsertEntities_Sync() { // Arrange var entities = new List @@ -71,7 +68,7 @@ public void InsertsEntities_Sync() } [SkippableFact] - public async Task InsertsEntities_AndReturn() + public async Task InsertEntities_AndReturn() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -91,8 +88,8 @@ public async Task InsertsEntities_AndReturn() Assert.Contains(insertedEntities, e => e.Name == $"{_run}_Entity2"); } - [SkippableFact] - public async Task InsertsEntities_WithJson() + [Fact] + public async Task InsertEntities_WithJson() { // Arrange var entities = new List @@ -112,7 +109,7 @@ public async Task InsertsEntities_WithJson() } [SkippableFact] - public async Task InsertsEntities_AndReturn_AsyncEnumerable() + public async Task InsertEntities_AndReturn_AsyncEnumerable() { Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); @@ -138,7 +135,7 @@ public async Task InsertsEntities_AndReturn_AsyncEnumerable() } [SkippableFact] - public void InsertsEntities_AndReturn_Sync() + public void InsertEntities_AndReturn_Sync() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -159,7 +156,7 @@ public void InsertsEntities_AndReturn_Sync() } [SkippableFact] - public async Task InsertsEntities_MultipleTimes() + public async Task InsertEntities_MultipleTimes() { Skip.If(_context.IsProvider(ProviderType.PostgreSql)); Skip.If(_context.IsProvider(ProviderType.SqlServer)); @@ -193,7 +190,7 @@ await _context.ExecuteBulkInsertAsync(entities, } [SkippableFact] - public async Task InsertsEntities_MultipleTimes_WithGuidId() + public async Task InsertEntities_MultipleTimes_WithGuidId() { // Arrange var entities = new List @@ -217,14 +214,14 @@ await _context.ExecuteBulkInsertAsync(entities, }); // Assert - var insertedEntities = _context.TestEntitiesWithGuidIds.Where(x => x.TestRun == _run).ToList(); + 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"); } [SkippableFact] - public async Task InsertsEntities_MultipleTimes_With_Conflict_On_Id() + public async Task InsertEntities_MultipleTimes_With_Conflict_On_Id() { // Arrange var entities = new List @@ -257,7 +254,7 @@ await _context.ExecuteBulkInsertAsync(insertedEntities0, } [Fact] - public async Task InsertsEntities_MoveRows() + public async Task InsertEntities_MoveRows() { // Arrange var entities = new List @@ -280,7 +277,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => } [SkippableFact] - public async Task InsertsEntities_WithConflict_SingleColumn() + public async Task InsertEntities_WithConflict_SingleColumn() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -319,7 +316,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => } [SkippableFact] - public async Task InsertsEntities_WithConflict_DoNothing() + public async Task InsertEntities_WithConflict_DoNothing() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -349,7 +346,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => } [SkippableFact] - public async Task InsertsEntities_WithConflict_Condition() + public async Task InsertEntities_WithConflict_Condition() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -380,7 +377,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => } [SkippableFact] - public async Task InsertsEntities_WithConflict_MultipleColumns() + public async Task InsertEntities_WithConflict_MultipleColumns() { Skip.If(_context.IsProvider(ProviderType.MySql)); @@ -416,7 +413,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => } [Fact] - public async Task InsertsEntities_DoesNothing_WhenEntitiesAreEmpty() + public async Task InsertEntities_DoesNothing_WhenEntitiesAreEmpty() { // Arrange var entities = new List(); @@ -430,7 +427,7 @@ public async Task InsertsEntities_DoesNothing_WhenEntitiesAreEmpty() } [Fact] - public async Task InsertsEntities_Many() + public async Task InsertEntities_Many() { // Arrange const int count = 156055; @@ -470,7 +467,7 @@ public async Task InsertEntities_AndReturn_WithEntityWithValueConverters() // Act await _context.ExecuteBulkInsertAsync(entities); - var inserted = _context.TestEntitiesWithConverters.Where(x => x.TestRun == _run).ToList(); + var inserted = _context.TestEntitiesWithConverter.Where(x => x.TestRun == _run).ToList(); // Assert Assert.Equal(2, inserted.Count); diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsMySql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsMySql.cs index 34716be..9251653 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsMySql.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsMySql.cs @@ -6,6 +6,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; [Trait("Category", "MySql")] -public class BasicTestsMySql(TestDbContainerMySql dbContainer) : BasicTestsBase, TestDbContextMySql>(dbContainer) +[Collection(TestDbContainerMySqlCollection.Name)] +public class BasicTestsMySql(TestDbContainerMySql dbContainer) : BasicTestsBase(dbContainer) { } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsPostgreSql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsPostgreSql.cs index b0ce8d3..7b369bf 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsPostgreSql.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsPostgreSql.cs @@ -6,6 +6,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; [Trait("Category", "PostgreSql")] -public class BasicTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : BasicTestsBase, TestDbContextPostgreSql>(dbContainer) +[Collection(TestDbContainerPostgreSqlCollection.Name)] +public class BasicTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : BasicTestsBase(dbContainer) { } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlServer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlServer.cs index f07ac40..c811fb5 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlServer.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlServer.cs @@ -6,6 +6,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; [Trait("Category", "SqlServer")] -public class BasicTestsSqlServer(TestDbContainerSqlServer dbContainer) : BasicTestsBase, TestDbContextSqlServer>(dbContainer) +[Collection(TestDbContainerSqlServerCollection.Name)] +public class BasicTestsSqlServer(TestDbContainerSqlServer dbContainer) : BasicTestsBase(dbContainer) { } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlite.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlite.cs index 2c728fd..eee2394 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlite.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsSqlite.cs @@ -6,7 +6,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.Tests.Basic; [Trait("Category", "Sqlite")] -public class BasicTestsSqlite(TestDbContainerSqlite dbContainer) : BasicTestsBase, TestDbContextSqlite>(dbContainer) +[Collection(TestDbContainerSqliteCollection.Name)] +public class BasicTestsSqlite(TestDbContainerSqlite dbContainer) : BasicTestsBase(dbContainer) { } - diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs new file mode 100644 index 0000000..890d0ba --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsBase.cs @@ -0,0 +1,50 @@ +using NetTopologySuite.Geometries; + +using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; +using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +using Xunit; + +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() + { + _context = await dbContainer.CreateContextAsync("geo"); + } + + public Task DisposeAsync() + { + _context.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task InsertEntities_WithGeo() + { + // Arrange + var geo1 = new Point(1, 2) { SRID = 4326 }; + var geo2 = new Point(3, 4) { SRID = 4326 }; + + var entities = new List + { + new TestEntityWithGeo { TestRun = _run, GeoObject = geo1 }, + new TestEntityWithGeo { TestRun = _run, GeoObject = geo2 } + }; + + // Act + await _context.ExecuteBulkInsertAsync(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); + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsMySql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsMySql.cs new file mode 100644 index 0000000..f1cb551 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsMySql.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.Geo; + +[Trait("Category", "MySql")] +[Collection(TestDbContainerMySqlCollection.Name)] +public class GeoTestsMySql(TestDbContainerMySql dbContainer) : GeoTestsBase(dbContainer) +{ +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsPostgreSql.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsPostgreSql.cs new file mode 100644 index 0000000..d53a2d7 --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsPostgreSql.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.Geo; + +[Trait("Category", "PostgreSql")] +[Collection(TestDbContainerPostgreSqlCollection.Name)] +public class GeoTestsPostgreSql(TestDbContainerPostgreSql dbContainer) : GeoTestsBase(dbContainer) +{ +} \ No newline at end of file diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsSqlServer.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsSqlServer.cs new file mode 100644 index 0000000..008305e --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Geo/GeoTestsSqlServer.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.Geo; + +[Trait("Category", "SqlServer")] +[Collection(TestDbContainerSqlServerCollection.Name)] +public class GeoTestsSqlServer(TestDbContainerSqlServer dbContainer) : GeoTestsBase(dbContainer) +{ +} \ No newline at end of file