From be064031d18483d3d3645c9d163c1c29e7a832ed Mon Sep 17 00:00:00 2001 From: NorthernLight1 <49600465+NorthernLight1@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:10:12 -0400 Subject: [PATCH] Updated README.md --- .github/workflows/release.yml | 2 + .../Data/BulkOperation.cs | 2 + .../Data/BulkOperationAsync.cs | 2 + .../Data/DatabaseFacadeExtensions.cs | 3 +- .../Data/DatabaseFacadeExtensionsAsync.cs | 3 +- .../Data/DatabaseFacadeExtensions.cs | 9 ++- .../Data/DatabaseFacadeExtensionsAsync.cs | 9 ++- .../Data/Address.cs | 6 +- .../Data/Address.cs | 6 +- .../N.EntityFrameworkCore.Extensions.csproj | 2 +- README.md | 59 +++++++++++++++---- 11 files changed, 73 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5009e2b..7132a35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,8 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true with: tag_name: v${{ inputs.version }} name: Release v${{ inputs.version }} diff --git a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperation.cs b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperation.cs index ab2f9fb..4c7c849 100644 --- a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperation.cs +++ b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperation.cs @@ -99,8 +99,10 @@ internal void PreallocateIdentityValues(IEnumerable entities) throw new InvalidDataException("Failed to allocate PostgreSql identity values."); object sequenceValue = Convert.ChangeType(reader.GetValue(0), identityProperty.ClrType); +#pragma warning disable EF1001 if (entity is InternalEntityEntry internalEntry) internalEntry.SetStoreGeneratedValue(identityProperty, sequenceValue); +#pragma warning restore EF1001 else identityProperty.PropertyInfo.SetValue(entity, sequenceValue); } diff --git a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperationAsync.cs b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperationAsync.cs index c01a24b..e9a90e0 100644 --- a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperationAsync.cs +++ b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/BulkOperationAsync.cs @@ -255,8 +255,10 @@ internal async Task PreallocateIdentityValuesAsync(IEnumerable entities, Canc throw new InvalidDataException("Failed to allocate PostgreSql identity values."); object sequenceValue = Convert.ChangeType(reader.GetValue(0), identityProperty.ClrType); +#pragma warning disable EF1001 if (entity is InternalEntityEntry internalEntry) internalEntry.SetStoreGeneratedValue(identityProperty, sequenceValue); +#pragma warning restore EF1001 else identityProperty.PropertyInfo.SetValue(entity, sequenceValue); } diff --git a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensions.cs b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensions.cs index 0011e0d..b34d200 100644 --- a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensions.cs +++ b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensions.cs @@ -19,7 +19,8 @@ public static SqlQuery FromSqlQuery(this DatabaseFacade database, string sqlText } public static int ClearTable(this DatabaseFacade database, string tableName) { - return database.ExecuteSqlRaw($"DELETE FROM {database.DelimitTableName(tableName)}"); + string sql = $"DELETE FROM {database.DelimitTableName(tableName)}"; + return database.ExecuteSqlRaw(sql); } public static int DropTable(this DatabaseFacade database, string tableName, bool ifExists = false) { diff --git a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensionsAsync.cs b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensionsAsync.cs index 36c9918..dcbbc17 100644 --- a/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensionsAsync.cs +++ b/N.EntityFrameworkCore.Extensions.PostgreSql/Data/DatabaseFacadeExtensionsAsync.cs @@ -12,7 +12,8 @@ public static class DatabaseFacadeExtensionsAsync { public static async Task ClearTableAsync(this DatabaseFacade database, string tableName, CancellationToken cancellationToken = default) { - return await database.ExecuteSqlRawAsync($"DELETE FROM {database.DelimitTableName(tableName)}", cancellationToken); + string sql = $"DELETE FROM {database.DelimitTableName(tableName)}"; + return await database.ExecuteSqlRawAsync(sql, cancellationToken); } public static async Task TruncateTableAsync(this DatabaseFacade database, string tableName, bool ifExists = false, CancellationToken cancellationToken = default) { diff --git a/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensions.cs b/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensions.cs index d80b258..90b0616 100644 --- a/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensions.cs +++ b/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensions.cs @@ -19,7 +19,8 @@ public static SqlQuery FromSqlQuery(this DatabaseFacade database, string sqlText } public static int ClearTable(this DatabaseFacade database, string tableName) { - return database.ExecuteSqlRaw($"DELETE FROM {database.DelimitTableName(tableName)}"); + string sql = $"DELETE FROM {database.DelimitTableName(tableName)}"; + return database.ExecuteSqlRaw(sql); } public static int DropTable(this DatabaseFacade database, string tableName, bool ifExists = false) { @@ -34,7 +35,8 @@ public static void TruncateTable(this DatabaseFacade database, string tableName, return; string formattedTableName = database.DelimitTableName(tableName); - database.ExecuteSqlRaw($"TRUNCATE TABLE {formattedTableName}"); + string sql = $"TRUNCATE TABLE {formattedTableName}"; + database.ExecuteSqlRaw(sql); } public static bool TableExists(this DatabaseFacade database, string tableName) { @@ -59,7 +61,8 @@ internal static int CloneTable(this DatabaseFacade database, IEnumerable if (!string.IsNullOrEmpty(internalIdColumnName)) columns = $"{columns},CAST(NULL AS INT) AS {database.DelimitIdentifier(internalIdColumnName)}"; - return database.ExecuteSqlRaw($"SELECT TOP 0 {columns} INTO {destinationTable} FROM {string.Join(",", sourceTables)}"); + string sql = $"SELECT TOP 0 {columns} INTO {destinationTable} FROM {string.Join(",", sourceTables)}"; + return database.ExecuteSqlRaw(sql); } internal static DbCommand CreateCommand(this DatabaseFacade database, ConnectionBehavior connectionBehavior = ConnectionBehavior.Default) { diff --git a/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensionsAsync.cs b/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensionsAsync.cs index 3406351..eb900ec 100644 --- a/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensionsAsync.cs +++ b/N.EntityFrameworkCore.Extensions.SqlServer/Data/DatabaseFacadeExtensionsAsync.cs @@ -12,7 +12,8 @@ public static class DatabaseFacadeExtensionsAsync { public static async Task ClearTableAsync(this DatabaseFacade database, string tableName, CancellationToken cancellationToken = default) { - return await database.ExecuteSqlRawAsync($"DELETE FROM {database.DelimitTableName(tableName)}", cancellationToken); + string sql = $"DELETE FROM {database.DelimitTableName(tableName)}"; + return await database.ExecuteSqlRawAsync(sql, cancellationToken); } public static async Task TruncateTableAsync(this DatabaseFacade database, string tableName, bool ifExists = false, CancellationToken cancellationToken = default) { @@ -21,7 +22,8 @@ public static async Task TruncateTableAsync(this DatabaseFacade database, string return; string formattedTableName = database.DelimitTableName(tableName); - await database.ExecuteSqlRawAsync($"TRUNCATE TABLE {formattedTableName}", cancellationToken); + string sql = $"TRUNCATE TABLE {formattedTableName}"; + await database.ExecuteSqlRawAsync(sql, cancellationToken); } internal static async Task CloneTableAsync(this DatabaseFacade database, string sourceTable, string destinationTable, IEnumerable columnNames, string internalIdColumnName = null, CancellationToken cancellationToken = default) { @@ -33,7 +35,8 @@ internal static async Task CloneTableAsync(this DatabaseFacade database, IE if (!string.IsNullOrEmpty(internalIdColumnName)) columns = $"{columns},CAST(NULL AS INT) AS {database.DelimitIdentifier(internalIdColumnName)}"; - return await database.ExecuteSqlRawAsync($"SELECT TOP 0 {columns} INTO {destinationTable} FROM {string.Join(",", sourceTables)}", cancellationToken); + string sql = $"SELECT TOP 0 {columns} INTO {destinationTable} FROM {string.Join(",", sourceTables)}"; + return await database.ExecuteSqlRawAsync(sql, cancellationToken); } internal static async Task ExecuteSqlAsync(this DatabaseFacade database, string sql, int? commandTimeout = null, CancellationToken cancellationToken = default) { diff --git a/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.PostgreSql.Test/Data/Address.cs b/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.PostgreSql.Test/Data/Address.cs index 6396e16..aede9ed 100644 --- a/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.PostgreSql.Test/Data/Address.cs +++ b/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.PostgreSql.Test/Data/Address.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; +#nullable enable using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace N.EntityFrameworkCore.Extensions.Test.Data; diff --git a/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.SqlServer.Test/Data/Address.cs b/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.SqlServer.Test/Data/Address.cs index 6396e16..aede9ed 100644 --- a/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.SqlServer.Test/Data/Address.cs +++ b/N.EntityFrameworkCore.Extensions.Test/N.EntityFrameworkCore.Extensions.SqlServer.Test/Data/Address.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; +#nullable enable using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace N.EntityFrameworkCore.Extensions.Test.Data; diff --git a/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj b/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj index 05a5cc9..1f4e0f0 100644 --- a/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj +++ b/N.EntityFrameworkCore.Extensions/N.EntityFrameworkCore.Extensions.csproj @@ -12,7 +12,7 @@ Meta-package that includes both N.EntityFrameworkCore.Extensions (SQL Server) and N.EntityFrameworkCore.Extensions.PostgreSql (PostgreSql). Extends your DbContext in EF Core with high-performance bulk operations: BulkDelete, BulkInsert, BulkMerge, BulkSync, BulkUpdate, Fetch, DeleteFromQuery, InsertFromQuery, UpdateFromQuery. MIT README.md - + NU5128 false contentFiles diff --git a/README.md b/README.md index c0d2039..4186fdd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,13 @@ High-performance bulk data extensions for Entity Framework Core. Extends your `D **Inheritance Models:** Table-Per-Concrete · Table-Per-Hierarchy · Table-Per-Type -**Database:** SQL Server · PostgreSql +**Database:** SQL Server · PostgreSql · MySQL + +--- + +> 💬 **Feedback & Feature Requests** +> Found a bug? Have an idea for a new feature or improvement? We'd love to hear from you! +> Please [open an issue](https://github.com/NorthernLight1/N.EntityFrameworkCore.Extensions/issues) on GitHub — whether it's a bug report, a feature request, a question, or general feedback, all contributions are welcome. --- @@ -51,6 +57,7 @@ High-performance bulk data extensions for Entity Framework Core. Extends your `D - [QueryToFileResult](#querytofileresult) - [SqlQuery](#sqlquery) - [Transactions](#transactions) +- [MySQL Limitations](#mysql-limitations) - [API Reference](#api-reference) - [Donations](#donations) @@ -58,21 +65,19 @@ High-performance bulk data extensions for Entity Framework Core. Extends your `D ## Installation -### SQL Server - -The latest stable version is available on [NuGet](https://www.nuget.org/packages/N.EntityFrameworkCore.Extensions). +Install the **all-in-one meta-package** (includes SQL Server and PostgreSql): ```sh dotnet add package N.EntityFrameworkCore.Extensions ``` -### PostgreSql - -A separate package is available for PostgreSql on [NuGet](https://www.nuget.org/packages/N.EntityFrameworkCore.Extensions.PostgreSql). +Or install **only the provider you need**: -```sh -dotnet add package N.EntityFrameworkCore.Extensions.PostgreSql -``` +| Provider | Package | +| --- | --- | +| SQL Server | [![](https://img.shields.io/nuget/v/N.EntityFrameworkCore.Extensions.SqlServer?label=NuGet)](https://www.nuget.org/packages/N.EntityFrameworkCore.Extensions.SqlServer) `dotnet add package N.EntityFrameworkCore.Extensions.SqlServer` | +| PostgreSql | [![](https://img.shields.io/nuget/v/N.EntityFrameworkCore.Extensions.PostgreSql?label=NuGet)](https://www.nuget.org/packages/N.EntityFrameworkCore.Extensions.PostgreSql) `dotnet add package N.EntityFrameworkCore.Extensions.PostgreSql` | +| MySQL | [![](https://img.shields.io/nuget/v/N.EntityFrameworkCore.Extensions.MySql?label=NuGet)](https://www.nuget.org/packages/N.EntityFrameworkCore.Extensions.MySql) `dotnet add package N.EntityFrameworkCore.Extensions.MySql` | --- @@ -102,11 +107,22 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } ``` +### MySQL + +```csharp +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +{ + optionsBuilder + .UseMySql("your-connection-string", ServerVersion.AutoDetect("your-connection-string")) + .SetupEfCoreExtensions(); +} +``` + This registers an EF Core `DbCommandInterceptor` used internally by bulk operations. It is required for operations that rewrite table names at execution time (e.g. `InsertFromQuery` targeting a new table); all other operations work without it. ### Test configuration -The test project uses SQL Server through `N.EntityFrameworkCore.Extensions.Test\appsettings.json` (or `ConnectionStrings__SqlServerTestDatabase` in the environment). The PostgreSql test project uses `N.EntityFrameworkCore.Extensions.PostgreSql.Test\appsettings.json` (or `ConnectionStrings__PostgreSqlTestDatabase` in the environment). +The test project uses SQL Server through `N.EntityFrameworkCore.Extensions.Test\appsettings.json` (or `ConnectionStrings__SqlServerTestDatabase` in the environment). The PostgreSql test project uses `N.EntityFrameworkCore.Extensions.PostgreSql.Test\appsettings.json` (or `ConnectionStrings__PostgreSqlTestDatabase` in the environment). The MySQL test project uses `N.EntityFrameworkCore.Extensions.MySql.Test\appsettings.json` (or `ConnectionStrings__MySqlTestDatabase` in the environment). --- @@ -636,6 +652,27 @@ catch --- +## MySQL Limitations + +MySQL has specific constraints that affect certain operations due to how it handles DDL statements and transactions. + +### InsertFromQuery and Transactions + +`InsertFromQuery` / `InsertFromQueryAsync` are **not supported inside user-managed transactions** on MySQL. Internally these operations execute `CREATE TABLE ... SELECT`, which is a DDL statement. MySQL automatically issues an implicit commit before and after any DDL statement, which would silently commit your active transaction. + +```csharp +// ⚠️ Do NOT use InsertFromQuery inside a transaction on MySQL +using var transaction = dbContext.Database.BeginTransaction(); +dbContext.Products + .Where(x => x.Price < 10M) + .InsertFromQuery("ProductsUnderTen", o => new { o.Id, o.Price }); // implicit commit! +transaction.Rollback(); // has no effect — already committed +``` + +Use `InsertFromQuery` outside of a transaction on MySQL, or use `BulkInsert` as an alternative when transactional safety is required. + +--- + ## API Reference ### DbContext Extensions