Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ internal void PreallocateIdentityValues(IEnumerable<T> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,10 @@ internal async Task PreallocateIdentityValuesAsync(IEnumerable<T> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public static class DatabaseFacadeExtensionsAsync
{
public static async Task<int> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -59,7 +61,8 @@ internal static int CloneTable(this DatabaseFacade database, IEnumerable<string>
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public static class DatabaseFacadeExtensionsAsync
{
public static async Task<int> 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)
{
Expand All @@ -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<int> CloneTableAsync(this DatabaseFacade database, string sourceTable, string destinationTable, IEnumerable<string> columnNames, string internalIdColumnName = null, CancellationToken cancellationToken = default)
{
Expand All @@ -33,7 +35,8 @@ internal static async Task<int> 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<int> ExecuteSqlAsync(this DatabaseFacade database, string sql, int? commandTimeout = null, CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<Description>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.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- No source files — this is a dependency aggregator only -->
<NoWarn>NU5128</NoWarn>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>contentFiles</ContentTargetFolders>
</PropertyGroup>
Expand Down
59 changes: 48 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -51,28 +57,27 @@ 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)

---

## 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` |

---

Expand Down Expand Up @@ -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).

---

Expand Down Expand Up @@ -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
Expand Down
Loading