Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ fabric.properties

# Nuget assets
/nupkgs

# Visual Studio Files
.vs
13 changes: 13 additions & 0 deletions PhenX.EntityFrameworkCore.BulkInsert.sln
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert", "src\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj", "{56CA0AE2-6EAB-4394-9E06-132558551251}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.PostgreSql", "src\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj", "{F37308A8-1C3C-44D2-9440-670DF76A8C31}"
Expand Down Expand Up @@ -30,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{45366E91-4
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.MySql", "src\PhenX.EntityFrameworkCore.BulkInsert.MySql\PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj", "{17649766-EA68-4333-8DA8-47B014A8B2CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -60,6 +65,13 @@ Global
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.Build.0 = Release|Any CPU
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{56CA0AE2-6EAB-4394-9E06-132558551251} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
Expand All @@ -68,5 +80,6 @@ Global
{EDCCED5F-D456-45E2-81A6-1077977F042B} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
{E4EB1C53-575C-45F8-924A-93DC42E8ACCA} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
{450E859C-411F-4D67-A0B4-4E02C3D30E14} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
{17649766-EA68-4333-8DA8-47B014A8B2CC} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;

using MySqlConnector;

using PhenX.EntityFrameworkCore.BulkInsert.Options;

namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;

internal class MySqlBulkInsertProvider : BulkInsertProviderBase<MySqlServerDialectBuilder>
{
public MySqlBulkInsertProvider(ILogger<MySqlBulkInsertProvider>? logger = null) : base(logger)
{
}

//language=sql
/// <inheritdoc />
protected override string CreateTableCopySql => "CREATE TEMPORARY TABLE {0} SELECT * FROM {1} WHERE 1 = 0;";

//language=sql
/// <inheritdoc />
protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT AUTO_INCREMENT PRIMARY KEY;";

/// <inheritdoc />
protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";

/// <inheritdoc />
protected override async Task BulkInsert<T>(
bool sync,
DbContext context,
IEnumerable<T> entities,
string tableName,
PropertyAccessor[] properties,
BulkInsertOptions options,
CancellationToken ctk
)
{
var connection = (MySqlConnection)context.Database.GetDbConnection();
var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction() as MySqlTransaction;

var bulkCopy = new MySqlBulkCopy(connection, sqlTransaction);
bulkCopy.DestinationTableName = tableName;
bulkCopy.BulkCopyTimeout = 60;
Comment thread
SebastianStehle marked this conversation as resolved.
Outdated

var sourceOrdinal = 0;
foreach (var prop in properties)
{
bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(sourceOrdinal, prop.ColumnName));
sourceOrdinal++;
}

if (sync)
{
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, properties));
}
else
{
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, properties), ctk);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;

/// <summary>
/// DbContext options extension for SQL Server.
Comment thread
SebastianStehle marked this conversation as resolved.
Outdated
/// </summary>
public static class MySqlDbContextOptionsExtensions
{
/// <summary>
/// Configures the DbContext to use the MySql bulk insert provider.
/// </summary>
public static DbContextOptionsBuilder UseBulkInsertMySql(this DbContextOptionsBuilder optionsBuilder)
{
var extension = optionsBuilder.Options.FindExtension<BulkInsertOptionsExtension<MySqlBulkInsertProvider>>() ?? new BulkInsertOptionsExtension<MySqlBulkInsertProvider>();

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

return optionsBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Text;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
using PhenX.EntityFrameworkCore.BulkInsert.Options;

namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;

internal class MySqlServerDialectBuilder : SqlDialectBuilder
{
protected override string OpenDelimiter => "`";

protected override string CloseDelimiter => "`";

protected override bool SupportsMoveRows => false;

public override bool SupportsReturning => false;

protected override void AppendConflictCondition<T>(StringBuilder sql, OnConflictOptions<T> onConflictTyped)
{
throw new NotSupportedException("Conflict conditions are not supported in MYSQL");
}

protected override void AppendOnConflictUpdate(StringBuilder sql, IEnumerable<string> updates)
{
sql.AppendLine("UPDATE");

var i = 0;
foreach (var update in updates)
{
if (i > 0)
{
sql.Append(", ");
}

sql.Append(update);
i++;
}
}

protected override void AppendOnConflictStatement(StringBuilder sql)
{
sql.Append("ON DUPLICATE KEY");
}

protected override void AppendDoNothing(StringBuilder sql, IProperty[] insertedProperties)
{
var columnName = insertedProperties[0].GetColumnName();

sql.Append($"UPDATE {Quote(columnName)} = {GetExcludedColumnName(columnName)}");
}

protected override string GetExcludedColumnName(string columnName)
{
return $"VALUES({Quote(columnName)})";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
Comment thread
SebastianStehle marked this conversation as resolved.
Outdated
<None Include="..\..\images\icon.png" Link="icon.png">
<PackagePath>\</PackagePath>
<Pack>true</Pack>
</None>
<None Include="..\..\README.md" Link="README.md">
<PackagePath>\</PackagePath>
<Pack>true</Pack>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Linq.Expressions;
using System.Linq.Expressions;
using System.Text;

using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -39,7 +39,7 @@ public override string BuildMoveDataSql<T>(DbContext context, string source,
matchColumns.Select(col => $"TARGET.{col} = SOURCE.{col}"));

var updateSet = onConflictTyped.Update != null
? string.Join(", ", GetUpdates(context, onConflictTyped.Update))
? string.Join(", ", GetUpdates(context, insertedProperties, onConflictTyped.Update))
: null;

q.AppendLine($"MERGE INTO {target} AS TARGET");
Expand Down Expand Up @@ -82,8 +82,8 @@ public override string BuildMoveDataSql<T>(DbContext context, string source,
return q.ToString();
}

protected override string GetExcludedColumnName<TEntity>(DbContext context, MemberExpression member)
protected override string GetExcludedColumnName(string columnName)
{
return $"SOURCE.{GetColumnName<TEntity>(context, member.Member.Name)}";
return $"SOURCE.{Quote(columnName)}";
}
}
23 changes: 16 additions & 7 deletions src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,26 @@ private async Task<List<TResult>> CopyFromTempTableWithoutKeysAsync<T, TResult>(
{
var (schemaName, tableName, _) = GetTableInfo(context, typeof(T));
var quotedTableName = QuoteTableName(schemaName, tableName);

var movedProperties = context.GetProperties(typeof(T), false);
var movedProperties = context.GetProperties(typeof(T), options.CopyGeneratedColumns);
var returnedProperties = returnData ? context.GetProperties(typeof(T)) : [];

if (returnData && !SqlDialect.SupportsReturning)
{
throw new NotSupportedException("Provider does not support returning entities.");
}

var query = SqlDialect.BuildMoveDataSql<T>(context, tempTableName, quotedTableName, movedProperties, returnedProperties, options, onConflict);

if (returnData)
{
return await QueryAsync(sync, context, query, cancellationToken);
}

// If not returning data, just execute the command
await ExecuteAsync(sync, context, query, cancellationToken);
return [];

static async Task<List<TResult>> QueryAsync(bool sync, DbContext context, string query, CancellationToken cancellationToken)
{
// Use EF to execute the query and return the results
IQueryable<TResult> queryable = context
Expand All @@ -128,10 +141,6 @@ private async Task<List<TResult>> CopyFromTempTableWithoutKeysAsync<T, TResult>(

return await queryable.ToListAsync(cancellationToken: cancellationToken);
}

// If not returning data, just execute the command
await ExecuteAsync(sync, context, query, cancellationToken);
return [];
}

public async Task<List<T>> BulkInsertReturnEntities<T>(
Expand Down Expand Up @@ -229,7 +238,7 @@ public async Task BulkInsert<T>(
: GetQuotedTableName(context, typeof(T));

var properties = context
.GetProperties(typeof(T), false)
.GetProperties(typeof(T), options.CopyGeneratedColumns)
.Select(p => new PropertyAccessor(p))
.ToArray();

Expand Down
Loading