diff --git a/README.md b/README.md index 227a927..62902e2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,28 @@ # PhenX.EntityFrameworkCore.BulkInsert -A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite. +A high-performance, provider-agnostic bulk insert extension for Entity Framework Core 8+. Supports SQL Server, PostgreSQL, SQLite and MySQL. Its main purpose is to provide a fast way to perform simple bulk inserts in Entity Framework Core applications. ## Why this library? - **Performance**: It is designed to be fast and memory efficient, making it suitable for high-performance applications. -- **Provider-agnostic**: It works with multiple database providers (SQL Server, PostgreSQL, and SQLite), allowing you to use it in different environments without changing your code. +- **Provider-agnostic**: It works with multiple database providers (SQL Server, PostgreSQL, SQLite and MySQL), allowing you to use it in different environments without changing your code. - **Simplicity**: The API is simple and easy to use, making it accessible for developers of all skill levels. For now, it does not support navigation properties, complex types, owned types, shadow properties, or inheritance, but they are in [the roadmap](#roadmap). +## Packages + +| Package Name | Description | NuGet Link | +|---------------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `PhenX.EntityFrameworkCore.BulkInsert.SqlServer` | For SQL Server | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.SqlServer.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.SqlServer) | +| `PhenX.EntityFrameworkCore.BulkInsert.PostgreSql` | For PostgreSQL | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql) | +| `PhenX.EntityFrameworkCore.BulkInsert.Sqlite` | For SQLite | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.Sqlite) | +| `PhenX.EntityFrameworkCore.BulkInsert.MySql` | For MySql | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.Sqlite.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert.MySql) | +| `PhenX.EntityFrameworkCore.BulkInsert` | Common library | [![NuGet](https://img.shields.io/nuget/v/PhenX.EntityFrameworkCore.BulkInsert.svg)](https://www.nuget.org/packages/PhenX.EntityFrameworkCore.BulkInsert) | + ## Installation Install the NuGet package for your database provider: @@ -26,6 +36,9 @@ Install-Package PhenX.EntityFrameworkCore.BulkInsert.PostgreSql # For SQLite Install-Package PhenX.EntityFrameworkCore.BulkInsert.Sqlite + +# For MySql +Install-Package PhenX.EntityFrameworkCore.BulkInsert.MySql ``` ## Usage @@ -43,6 +56,8 @@ services.AddDbContext(options => .UseBulkInsertSqlServer() // OR .UseBulkInsertSqlite() + // OR + .UseBulkInsertMySql() ; }); ``` @@ -57,13 +72,35 @@ await dbContext.ExecuteBulkInsertAsync(entities); dbContext.ExecuteBulkInsert(entities); ``` -3. Optionally, you can configure the bulk insert options: +3. You can also configure the bulk insert options: ```csharp +// Common options await dbContext.ExecuteBulkInsertAsync(entities, options => { options.BatchSize = 1000; // Set the batch size for the insert operation, the default value is different for each provider }); + +// Provider specific options, when available, example for SQL Server +await dbContext.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) => // <<< here specify the SQL Server options class +{ + options.EnableStreaming = true; // Enable streaming for SQL Server +}); + +// Provider specific options, supporting multiple providers +await dbContext.ExecuteBulkInsertAsync(entities, o => +{ + o.MoveRows = true; + + if (o is SqlServerBulkInsertOptions sqlServerOptions) + { + sqlServerOptions.EnableStreaming = true; + } + else if (o is MySqlBulkInsertOptions mysqlOptions) + { + mysqlOptions.BatchSize = 1000; + } +}); ``` 4. You can also return the inserted entities (slower): diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertOptions.cs new file mode 100644 index 0000000..d1e8d60 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertOptions.cs @@ -0,0 +1,11 @@ +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.MySql; + +/// +/// Options specific to MySQL bulk insert. +/// +public class MySqlBulkInsertOptions : BulkInsertOptions +{ + +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs index ab08f92..ff32163 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs @@ -9,7 +9,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.MySql; -internal class MySqlBulkInsertProvider : BulkInsertProviderBase +internal class MySqlBulkInsertProvider : BulkInsertProviderBase { public MySqlBulkInsertProvider(ILogger? logger = null) : base(logger) { @@ -22,6 +22,9 @@ public MySqlBulkInsertProvider(ILogger? logger = null) /// protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + /// + protected override MySqlBulkInsertOptions CreateDefaultOptions() => new(); + /// public override IAsyncEnumerable BulkInsertReturnEntities( bool sync, @@ -43,21 +46,25 @@ protected override async Task BulkInsert( IEnumerable entities, string tableName, IReadOnlyList properties, - BulkInsertOptions options, + MySqlBulkInsertOptions options, CancellationToken ctk ) { var connection = (MySqlConnection)context.Database.GetDbConnection(); - var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction() + + var sqlTransaction = context.Database.CurrentTransaction?.GetDbTransaction() ?? throw new InvalidOperationException("No open transaction found."); + if (sqlTransaction is not MySqlTransaction mySqlTransaction) { throw new InvalidOperationException($"Invalid transaction foud, got {sqlTransaction.GetType()}."); } - var bulkCopy = new MySqlBulkCopy(connection, mySqlTransaction); - bulkCopy.DestinationTableName = tableName; - bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds(); + var bulkCopy = new MySqlBulkCopy(connection, mySqlTransaction) + { + DestinationTableName = tableName, + BulkCopyTimeout = options.GetCopyTimeoutInSeconds(), + }; var sourceOrdinal = 0; foreach (var prop in properties) diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs index 1258845..4dd86a2 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs @@ -13,7 +13,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql; [UsedImplicitly] -internal class PostgreSqlBulkInsertProvider : BulkInsertProviderBase +internal class PostgreSqlBulkInsertProvider : BulkInsertProviderBase { public PostgreSqlBulkInsertProvider(ILogger? logger = null) : base(logger) { @@ -32,6 +32,12 @@ private static string GetBinaryImportCommand(IReadOnlyList prope return sql.ToString(); } + /// + protected override BulkInsertOptions CreateDefaultOptions() => new() + { + BatchSize = 50_000, + }; + /// protected override async Task BulkInsert( bool sync, diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertOptions.cs new file mode 100644 index 0000000..b31b6cc --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertOptions.cs @@ -0,0 +1,18 @@ +using Microsoft.Data.SqlClient; + +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer; + +/// +/// Options specific to SQL Server bulk insert. +/// +public class SqlServerBulkInsertOptions : BulkInsertOptions +{ + /// + public SqlBulkCopyOptions CopyOptions { get; set; } = SqlBulkCopyOptions.Default; + + /// + public bool EnableStreaming { get; set; } = false; + +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs index 79fc58f..1efbbb4 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs @@ -11,7 +11,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer; [UsedImplicitly] -internal class SqlServerBulkInsertProvider : BulkInsertProviderBase +internal class SqlServerBulkInsertProvider : BulkInsertProviderBase { public SqlServerBulkInsertProvider(ILogger? logger = null) : base(logger) { @@ -24,6 +24,11 @@ public SqlServerBulkInsertProvider(ILogger? logger /// protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new() + { + BatchSize = 50_000, + }; + /// protected override async Task BulkInsert( bool sync, @@ -32,16 +37,18 @@ protected override async Task BulkInsert( IEnumerable entities, string tableName, IReadOnlyList columns, - BulkInsertOptions options, + SqlServerBulkInsertOptions options, CancellationToken ctk) { var connection = (SqlConnection) context.Database.GetDbConnection(); var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction() as SqlTransaction; - using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, sqlTransaction); + using var bulkCopy = new SqlBulkCopy(connection, options.CopyOptions, sqlTransaction); + bulkCopy.DestinationTableName = tableName; - bulkCopy.BatchSize = options.BatchSize ?? 50_000; + bulkCopy.BatchSize = options.BatchSize; bulkCopy.BulkCopyTimeout = options.GetCopyTimeoutInSeconds(); + bulkCopy.EnableStreaming = options.EnableStreaming; foreach (var column in columns) { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index bd82de2..439d0f2 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -13,7 +13,7 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Sqlite; [UsedImplicitly] -internal class SqliteBulkInsertProvider : BulkInsertProviderBase +internal class SqliteBulkInsertProvider : BulkInsertProviderBase { public SqliteBulkInsertProvider(ILogger? logger = null) : base(logger) { @@ -26,6 +26,12 @@ public SqliteBulkInsertProvider(ILogger? logger = null /// protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite + /// + protected override BulkInsertOptions CreateDefaultOptions() => new() + { + BatchSize = 5, + }; + /// protected override Task AddBulkInsertIdColumn( bool sync, @@ -142,10 +148,10 @@ CancellationToken ctk ) where T : class { const int maxParams = 1000; - var batchSize = options.BatchSize ?? 5; + var batchSize = options.BatchSize; batchSize = Math.Min(batchSize, maxParams / columns.Count); - // The StringBuilder can be resuse between the batches. + // The StringBuilder can be resuse between the batches. var sb = new StringBuilder(); var columnList = tableInfo.GetColumns(options.CopyGeneratedColumns); diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkInsertProvider.cs index 226b52a..a8fa8e6 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Abstractions/IBulkInsertProvider.cs @@ -38,4 +38,9 @@ internal Task BulkInsert( ) where T : class; SqlDialectBuilder SqlDialect { get; } + + /// + /// Make the default options for the provider, can be a subclass of . + /// + internal BulkInsertOptions InternalCreateDefaultOptions(); } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs index ace778a..48fa384 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs @@ -12,7 +12,9 @@ namespace PhenX.EntityFrameworkCore.BulkInsert; -internal abstract class BulkInsertProviderBase(ILogger>? logger = null) : IBulkInsertProvider where TDialect : SqlDialectBuilder, new() +internal abstract class BulkInsertProviderBase(ILogger>? logger = null) : IBulkInsertProvider + where TDialect : SqlDialectBuilder, new() + where TOptions : BulkInsertOptions, new() { protected readonly TDialect SqlDialect = new(); @@ -24,6 +26,13 @@ namespace PhenX.EntityFrameworkCore.BulkInsert; SqlDialectBuilder IBulkInsertProvider.SqlDialect => SqlDialect; + public BulkInsertOptions InternalCreateDefaultOptions() => CreateDefaultOptions(); + + /// + /// Create the default options for the provider, can be a subclass of . + /// + protected abstract TOptions CreateDefaultOptions(); + public virtual async IAsyncEnumerable BulkInsertReturnEntities( bool sync, DbContext context, @@ -33,6 +42,11 @@ public virtual async IAsyncEnumerable BulkInsertReturnEntities( OnConflictOptions? onConflict, [EnumeratorCancellation] CancellationToken ctk) where T : class { + if (options is not TOptions providerOptions) + { + throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}"); + } + using var activity = Telemetry.ActivitySource.StartActivity("BulkInsertReturnEntities"); activity?.AddTag("tableName", tableInfo.TableName); activity?.AddTag("synchronous", sync); @@ -45,10 +59,10 @@ public virtual async IAsyncEnumerable BulkInsertReturnEntities( Log.UsingTempTablToReturnData(logger); } - var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, options, tempTableRequired: true, ctk: ctk); + var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, providerOptions, tempTableRequired: true, ctk: ctk); var result = - await CopyFromTempTableAsync(sync, context, tableInfo, tableName, true, options, onConflict, ctk: ctk) + await CopyFromTempTableAsync(sync, context, tableInfo, tableName, true, providerOptions, onConflict, ctk: ctk) ?? throw new InvalidOperationException("Copy returns null enumerable."); await foreach (var item in result.WithCancellation(ctk)) @@ -74,6 +88,11 @@ public virtual async Task BulkInsert( OnConflictOptions? onConflict, CancellationToken ctk) where T : class { + if (options is not TOptions providerOptions) + { + throw new InvalidOperationException($"Invalid options type: {options.GetType().Name}. Expected: {typeof(TOptions).Name}"); + } + using var activity = Telemetry.ActivitySource.StartActivity("BulkInsert"); activity?.AddTag("tableName", tableInfo.TableName); activity?.AddTag("synchronous", sync); @@ -88,9 +107,9 @@ public virtual async Task BulkInsert( Log.UsingTempTableToResolveConflicts(logger); } - var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, options, tempTableRequired: true, ctk: ctk); + var tableName = await PerformBulkInsertAsync(sync, context, tableInfo, entities, providerOptions, tempTableRequired: true, ctk: ctk); - await CopyFromTempTableAsync(sync, context, tableInfo, tableName, false, options, onConflict, ctk); + await CopyFromTempTableAsync(sync, context, tableInfo, tableName, false, providerOptions, onConflict, ctk); } else { @@ -99,7 +118,7 @@ public virtual async Task BulkInsert( Log.UsingDirectInsert(logger); } - await PerformBulkInsertAsync(sync, context, tableInfo, entities, options, tempTableRequired: false, ctk: ctk); + await PerformBulkInsertAsync(sync, context, tableInfo, entities, providerOptions, tempTableRequired: false, ctk: ctk); } // Commit the transaction if we own them. @@ -116,7 +135,7 @@ private async Task PerformBulkInsertAsync( DbContext context, TableMetadata tableInfo, IEnumerable entities, - BulkInsertOptions options, + TOptions options, bool tempTableRequired, CancellationToken ctk) where T : class { @@ -149,7 +168,7 @@ protected abstract Task BulkInsert( IEnumerable entities, string tableName, IReadOnlyList columns, - BulkInsertOptions options, + TOptions options, CancellationToken ctk) where T : class; protected async Task CreateTableCopyAsync( @@ -187,7 +206,7 @@ protected virtual async Task AddBulkInsertIdColumn( TableMetadata tableInfo, string tempTableName, bool returnData, - BulkInsertOptions options, + TOptions options, OnConflictOptions? onConflict, CancellationToken ctk) where T : class where TResult : class { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Enums/ProviderType.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Enums/ProviderType.cs new file mode 100644 index 0000000..1010c28 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Enums/ProviderType.cs @@ -0,0 +1,27 @@ +namespace PhenX.EntityFrameworkCore.BulkInsert.Enums; + +/// +/// Enumeration of supported database providers. +/// +public enum ProviderType +{ + /// + /// SQL Server provider. + /// + SqlServer, + + /// + /// PostgreSQL provider. + /// + PostgreSql, + + /// + /// SQLite provider. + /// + Sqlite, + + /// + /// MySQL provider. + /// + MySql, +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbSetExtensions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbSetExtensions.cs deleted file mode 100644 index 7e773a9..0000000 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbSetExtensions.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; -using PhenX.EntityFrameworkCore.BulkInsert.Options; - -namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; - -/// -/// DbSet extensions for bulk insert operations. -/// -public static class DbSetExtensions -{ - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant). - /// - public static List ExecuteBulkInsertReturnEntities( - this DbSet dbSet, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null - ) where T : class - { - return dbSet.ExecuteBulkInsertReturnEntitiesCoreAsync(true, entities, configure, onConflict, default).GetAwaiter().GetResult(); - } - - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext (synchronous variant). - /// - public static List ExecuteBulkInsertReturnEntities( - this DbContext dbContext, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null - ) where T : class - { - var dbSet = dbContext.Set() ?? throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); - - return dbSet.ExecuteBulkInsertReturnEntitiesCoreAsync(true, entities, configure, onConflict, default).GetAwaiter().GetResult(); - } - - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext. - /// - public static Task> ExecuteBulkInsertReturnEntitiesAsync( - this DbContext dbContext, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - var dbSet = dbContext.Set() ?? throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); - - return dbSet.ExecuteBulkInsertReturnEntitiesCoreAsync(false, entities, configure, onConflict, ctk); - } - - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet. - /// - public static Task> ExecuteBulkInsertReturnEntitiesAsync( - this DbSet dbSet, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - return dbSet.ExecuteBulkInsertReturnEntitiesCoreAsync(false, entities, configure, onConflict, ctk); - } - - private static async Task> ExecuteBulkInsertReturnEntitiesCoreAsync( - this DbSet dbSet, - bool sync, - IEnumerable entities, - Action? configure, - OnConflictOptions? onConflict, - CancellationToken ctk - ) where T : class - { - var provider = InitProvider(dbSet, configure, out var context, out var options); - - var enumerable = provider.BulkInsertReturnEntities(sync, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, ctk); - - var result = new List(); - await foreach (var item in enumerable.WithCancellation(ctk)) - { - result.Add(item); - } - - return result; - } - - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext. - /// - public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( - this DbContext dbContext, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - var dbSet = dbContext.Set() ?? throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); - - return dbSet.ExecuteBulkInsertReturnEnumerableAsync(entities, configure, onConflict, ctk); - } - - /// - /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet. - /// - public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( - this DbSet dbSet, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - var provider = InitProvider(dbSet, configure, out var context, out var options); - - return provider.BulkInsertReturnEntities(false, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, ctk); - } - - /// - /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext. - /// - public static async Task ExecuteBulkInsertAsync( - this DbContext dbContext, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - var dbSet = dbContext.Set() ?? throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); - - await dbSet.ExecuteBulkInsertAsync(entities, configure, onConflict, ctk); - } - - /// - /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet. - /// - public static async Task ExecuteBulkInsertAsync( - this DbSet dbSet, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null, - CancellationToken ctk = default - ) where T : class - { - var provider = InitProvider(dbSet, configure, out var context, out var options); - - await provider.BulkInsert(false, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, ctk); - } - - /// - /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext (synchronous variant). - /// - public static void ExecuteBulkInsert( - this DbContext dbContext, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null - ) where T : class - { - var dbSet = dbContext.Set() ?? throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); - - dbSet.ExecuteBulkInsert(entities, configure, onConflict); - } - - /// - /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant). - /// - public static void ExecuteBulkInsert( - this DbSet dbSet, - IEnumerable entities, - Action? configure = null, - OnConflictOptions? onConflict = null - ) where T : class - { - var provider = InitProvider(dbSet, configure, out var context, out var options); - - provider.BulkInsert(true, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict).GetAwaiter().GetResult(); - } - - private static DbContext GetDbContext(this DbSet dbSet) where T : class - { - IInfrastructure infrastructure = dbSet; - return (infrastructure.Instance.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext)!.Context; - } - - private static IBulkInsertProvider InitProvider(DbSet dbSet, Action? configure, out DbContext context, - out BulkInsertOptions options) where T : class - { - context = dbSet.GetDbContext(); - var provider = context.GetService(); - - options = new BulkInsertOptions(); - configure?.Invoke(options); - return provider; - } -} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbContextExtensions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/InternalExtensions.cs similarity index 73% rename from src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbContextExtensions.cs rename to src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/InternalExtensions.cs index 2b4bd16..16443b7 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/DbContextExtensions.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/InternalExtensions.cs @@ -6,18 +6,20 @@ using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; using PhenX.EntityFrameworkCore.BulkInsert.Metadata; +using PhenX.EntityFrameworkCore.BulkInsert.Enums; + namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; -internal static class DbContextExtensions +internal static class InternalExtensions { - public static TableMetadata GetTableInfo(this DbContext context) + internal static TableMetadata GetTableInfo(this DbContext context) { var provider = context.GetService(); return provider.GetTableInfo(context); } - public static DbContextOptionsBuilder UseProvider(this DbContextOptionsBuilder optionsBuilder) + internal static DbContextOptionsBuilder UseProvider(this DbContextOptionsBuilder optionsBuilder) where TProvider : class, IBulkInsertProvider { var extension = optionsBuilder.Options.FindExtension>() ?? new BulkInsertOptionsExtension(); @@ -69,4 +71,17 @@ internal static async Task GetConnection( return new ConnectionInfo(connection, wasClosed, transaction, wasBegan); } + + /// + /// Tells if the current provider is the specified provider type. + /// + internal static bool IsProvider(this DbContext context, ProviderType providerType) + { + if (context.Database.ProviderName == null) + { + throw new InvalidOperationException("Database provider name is null."); + } + + return context.Database.ProviderName.Contains(providerType.ToString(), StringComparison.OrdinalIgnoreCase); + } } diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbContext.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbContext.cs new file mode 100644 index 0000000..9bc410b --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbContext.cs @@ -0,0 +1,239 @@ +using Microsoft.EntityFrameworkCore; + +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; + +public static partial class PublicExtensions +{ + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext (synchronous variant), with provider specific options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + where TConfig : BulkInsertOptions + { + var dbSet = dbContext.Set() ?? + throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); + + return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet, true, entities, configure, onConflict, CancellationToken.None) + .GetAwaiter().GetResult(); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext (synchronous variant), with common options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + { + return ExecuteBulkInsertReturnEntities(dbContext, entities, configure, onConflict); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext (synchronous variant), without options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbContext dbContext, + IEnumerable entities, + OnConflictOptions? onConflict = null + ) + where T : class + { + return ExecuteBulkInsertReturnEntities(dbContext, entities, _ => { }, onConflict); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, with provider specific options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TConfig : BulkInsertOptions + { + var dbSet = dbContext.Set() ?? + throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); + + return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet,false, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, with common options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEntitiesAsync(dbContext, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, without options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbContext dbContext, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEntitiesAsync(dbContext, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, with provider specific options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TConfig : BulkInsertOptions + { + var dbSet = dbContext.Set() ?? + throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); + + return dbSet.ExecuteBulkInsertReturnEnumerableAsync(entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, with common options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) where T : class + { + return ExecuteBulkInsertReturnEnumerableAsync(dbContext, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbContext, without options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbContext dbContext, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) where T : class + { + return ExecuteBulkInsertReturnEnumerableAsync(dbContext, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext, with provider specific options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TConfig : BulkInsertOptions + { + var dbSet = dbContext.Set() ?? + throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); + + await dbSet.ExecuteBulkInsertAsync(entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext, with common options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + await ExecuteBulkInsertAsync(dbContext, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext, without options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbContext dbContext, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + await ExecuteBulkInsertAsync(dbContext, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext (synchronous variant), with provider specific options. + /// + public static void ExecuteBulkInsert( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + where TConfig : BulkInsertOptions + { + var dbSet = dbContext.Set() ?? + throw new InvalidOperationException($"DbSet of type {typeof(T).Name} not found in DbContext."); + + dbSet.ExecuteBulkInsert(entities, configure, onConflict); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext (synchronous variant), with common options. + /// + public static void ExecuteBulkInsert( + this DbContext dbContext, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) where T : class + { + ExecuteBulkInsert(dbContext, entities, configure, onConflict); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbContext (synchronous variant), without options. + /// + public static void ExecuteBulkInsert( + this DbContext dbContext, + IEnumerable entities, + OnConflictOptions? onConflict = null + ) where T : class + { + ExecuteBulkInsert(dbContext, entities, _ => { }, onConflict); + } +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbSet.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbSet.cs new file mode 100644 index 0000000..c7cd183 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.DbSet.cs @@ -0,0 +1,237 @@ +using Microsoft.EntityFrameworkCore; + +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; + +public static partial class PublicExtensions +{ + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), with provider specific options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + where TOptions : BulkInsertOptions + { + return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet, true, entities, configure, onConflict, CancellationToken.None) + .GetAwaiter().GetResult(); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), with common options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + { + return ExecuteBulkInsertReturnEntities(dbSet, entities, configure, onConflict); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet (synchronous variant), without options. + /// + public static List ExecuteBulkInsertReturnEntities( + this DbSet dbSet, + IEnumerable entities, + OnConflictOptions? onConflict = null + ) + where T : class + { + return ExecuteBulkInsertReturnEntities(dbSet, entities, _ => { }, onConflict); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with provider specific options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TOptions : BulkInsertOptions + { + return ExecuteBulkInsertReturnEntitiesCoreAsync(dbSet, false, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with common options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEntitiesAsync(dbSet, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, without options. + /// + public static Task> ExecuteBulkInsertReturnEntitiesAsync( + this DbSet dbSet, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEntitiesAsync(dbSet, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with provider specific options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TOptions : BulkInsertOptions + { + var provider = InitProvider(dbSet, configure, out var context, out var options); + + return provider.BulkInsertReturnEntities(false, context, dbSet.GetDbContext().GetTableInfo(), entities, + options, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, with common options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEnumerableAsync(dbSet, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation returning the inserted/updated entities, from the DbSet, without options. + /// + public static IAsyncEnumerable ExecuteBulkInsertReturnEnumerableAsync( + this DbSet dbSet, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + return ExecuteBulkInsertReturnEnumerableAsync(dbSet, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, with provider specific options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + where TOptions : BulkInsertOptions + { + var provider = InitProvider(dbSet, configure, out var context, out var options); + + await provider.BulkInsert(false, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, + ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, with common options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + await ExecuteBulkInsertAsync(dbSet, entities, configure, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet, without options. + /// + public static async Task ExecuteBulkInsertAsync( + this DbSet dbSet, + IEnumerable entities, + OnConflictOptions? onConflict = null, + CancellationToken ctk = default + ) + where T : class + { + await ExecuteBulkInsertAsync(dbSet, entities, _ => { }, onConflict, ctk); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), with provider specific options. + /// + public static void ExecuteBulkInsert( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + where TOptions : BulkInsertOptions + { + var provider = InitProvider(dbSet, configure, out var context, out var options); + + provider.BulkInsert(true, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict) + .GetAwaiter().GetResult(); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), with common options. + /// + public static void ExecuteBulkInsert( + this DbSet dbSet, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict = null + ) + where T : class + { + ExecuteBulkInsert(dbSet, entities, configure, onConflict); + } + + /// + /// Executes a bulk insert operation without returning the inserted/updated entities, from the DbSet (synchronous variant), without options. + /// + public static void ExecuteBulkInsert( + this DbSet dbSet, + IEnumerable entities, + OnConflictOptions? onConflict = null + ) + where T : class + { + ExecuteBulkInsert(dbSet, entities, _ => { }, onConflict); + } +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.cs new file mode 100644 index 0000000..4dcc850 --- /dev/null +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/PublicExtensions.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; +using PhenX.EntityFrameworkCore.BulkInsert.Options; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; + +/// +/// DbSet extensions for bulk insert operations. +/// +public static partial class PublicExtensions +{ + private static async Task> ExecuteBulkInsertReturnEntitiesCoreAsync( + this DbSet dbSet, + bool sync, + IEnumerable entities, + Action configure, + OnConflictOptions? onConflict, + CancellationToken ctk + ) + where T : class + where TOptions : BulkInsertOptions + { + var provider = InitProvider(dbSet, configure, out var context, out var options); + + var enumerable = provider.BulkInsertReturnEntities(sync, context, dbSet.GetDbContext().GetTableInfo(), entities, options, onConflict, ctk); + + var result = new List(); + await foreach (var item in enumerable.WithCancellation(ctk)) + { + result.Add(item); + } + + return result; + } + + private static DbContext GetDbContext(this DbSet dbSet) where T : class + { + IInfrastructure infrastructure = dbSet; + return (infrastructure.Instance.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext)!.Context; + } + + private static IBulkInsertProvider InitProvider( + DbSet dbSet, + Action? configure, + out DbContext context, + out TOptions options + ) + where T : class where TOptions : BulkInsertOptions + { + context = dbSet.GetDbContext(); + var provider = context.GetService(); + + var defaultOptions = provider.InternalCreateDefaultOptions(); + + if (defaultOptions is not TOptions castedOptions) + { + throw new InvalidOperationException($"Options type mismatch. Expected {defaultOptions.GetType().Name}, but got {typeof(TOptions).Name}."); + } + + options = castedOptions; + configure?.Invoke(options); + + return provider; + } +} diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/ConnectionInfo.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ConnectionInfo.cs similarity index 95% rename from src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/ConnectionInfo.cs rename to src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ConnectionInfo.cs index 33ecc9b..cbb52e1 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Extensions/ConnectionInfo.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ConnectionInfo.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore.Storage; -namespace PhenX.EntityFrameworkCore.BulkInsert.Extensions; +namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; internal readonly record struct ConnectionInfo(DbConnection Connection, bool WasClosed, IDbContextTransaction Transaction, bool WasBegan) { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs index c72f919..19f4835 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Options/BulkInsertOptions.cs @@ -31,17 +31,17 @@ public class BulkInsertOptions /// /// /// - public int? BatchSize { get; set; } + public int BatchSize { get; set; } /// /// Indicates if also generated columns should be copied. This is useful for upsert operations. /// public bool CopyGeneratedColumns { get; set; } - + /// /// The timeout to copy records. /// - public TimeSpan CopyTimeout = TimeSpan.FromMinutes(10); + public TimeSpan CopyTimeout { get; set; } = TimeSpan.FromMinutes(10); internal int GetCopyTimeoutInSeconds() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj b/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj index 17da02a..1f957fb 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index eaa1976..d344a1c 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -1,5 +1,8 @@ +using PhenX.EntityFrameworkCore.BulkInsert.Enums; using PhenX.EntityFrameworkCore.BulkInsert.Extensions; +using PhenX.EntityFrameworkCore.BulkInsert.MySql; using PhenX.EntityFrameworkCore.BulkInsert.Options; +using PhenX.EntityFrameworkCore.BulkInsert.SqlServer; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer; using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; @@ -74,7 +77,7 @@ public void InsertsEntitiesSuccessfully_Sync() [SkippableFact] public async Task InsertsEntitiesAndReturn() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); // Arrange var entities = new List @@ -121,7 +124,7 @@ public async Task InsertsEntitiesAndReturnAsyncEnumerable() [SkippableFact] public void InsertsEntitiesAndReturn_Sync() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); // Arrange var entities = new List @@ -142,8 +145,8 @@ public void InsertsEntitiesAndReturn_Sync() [SkippableFact] public async Task InsertsEntities_MultipleTimes() { - Skip.If(_context.Database.ProviderName!.Contains("Postgres", StringComparison.InvariantCultureIgnoreCase)); - Skip.If(_context.Database.ProviderName!.Contains("SqlServer", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.PostgreSql)); + Skip.If(_context.IsProvider(ProviderType.SqlServer)); // Arrange var entities = new List @@ -263,7 +266,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => [SkippableFact] public async Task InsertsEntitiesWithConflict_SingleColumn() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }); await _context.SaveChangesAsync(); @@ -302,7 +305,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => [SkippableFact] public async Task InsertsEntitiesWithConflict_DoNothing() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }); await _context.SaveChangesAsync(); @@ -332,7 +335,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => [SkippableFact] public async Task InsertsEntitiesWithConflict_Condition() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 }); await _context.SaveChangesAsync(); @@ -363,7 +366,7 @@ await _context.ExecuteBulkInsertAsync(entities, o => [SkippableFact] public async Task InsertsEntitiesWithConflict_MultipleColumns() { - Skip.If(_context.Database.ProviderName!.Contains("Mysql", StringComparison.InvariantCultureIgnoreCase)); + Skip.If(_context.IsProvider(ProviderType.MySql)); _context.TestEntities.Add(new TestEntity { TestRun = _run, Name = $"{_run}_Entity1", Price = 10 }); await _context.SaveChangesAsync(); @@ -548,4 +551,36 @@ public void BulkInsert_WithOpenTransaction_RollsBackOnFailure_Sync() Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail1"); Assert.DoesNotContain(insertedEntities, e => e.Name == $"{_run}_EntityWithTxFail2"); } + + [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 + var entities = new List + { + new TestEntity { TestRun = _run, Name = $"{_run}_Entity1" }, + new TestEntity { TestRun = _run, Name = $"{_run}_Entity2" } + }; + + // Act & Assert + if (_context.IsProvider(ProviderType.SqlServer)) + { + await Assert.ThrowsAsync(async () => + await _context.ExecuteBulkInsertAsync(entities, (MySqlBulkInsertOptions o) => + { + })); + } + + if (_context.IsProvider(ProviderType.MySql)) + { + await Assert.ThrowsAsync(async () => + await _context.ExecuteBulkInsertAsync(entities, (SqlServerBulkInsertOptions o) => + { + })); + } + } }