Skip to content

Commit 71f2415

Browse files
author
fabien.menager
committed
Merge branch 'main' into dotnet10
# Conflicts: # tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj
2 parents 0225098 + 7bbf9c4 commit 71f2415

19 files changed

Lines changed: 371 additions & 173 deletions

src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ protected IEnumerable<string> GetUpdates<T>(DbContext context, TableMetadata tab
253253
}
254254
case MemberInitExpression memberInit:
255255
{
256-
foreach (var binding in memberInit.Bindings.OfType<MemberAssignment>())
256+
foreach (var updateSql in GetUpdatesFromMemberInit<T>(context, table, memberInit, lambda))
257257
{
258-
yield return $"{table.GetQuotedColumnName(binding.Member.Name)} = {ToSqlExpression<T>(context, table, binding.Expression, lambda)}";
258+
yield return updateSql;
259259
}
260260

261261
break;
@@ -297,15 +297,20 @@ private string ToSqlExpression<TEntity>(DbContext context, TableMetadata table,
297297
case MemberExpression memberExpr:
298298
var columnName = table.GetColumnName(memberExpr.Member.Name);
299299

300+
// Traverse up the expression chain to find the root parameter
301+
// This handles both simple properties (e.g., excluded.Name) and
302+
// complex properties (e.g., excluded.ComplexObject.Property)
303+
var rootParam = GetRootParameter(memberExpr);
304+
300305
// If the member expression is a property of the current lambda
301-
if (lambda is { Parameters.Count: > 1 } && memberExpr.Expression is ParameterExpression paramExpr)
306+
if (lambda is { Parameters.Count: > 1 } && rootParam != null)
302307
{
303-
if (paramExpr.Name == lambda.Parameters[0].Name)
308+
if (rootParam.Name == lambda.Parameters[0].Name)
304309
{
305310
return GetInsertedColumnName(columnName);
306311
}
307312

308-
if (paramExpr.Name == lambda.Parameters[1].Name)
313+
if (rootParam.Name == lambda.Parameters[1].Name)
309314
{
310315
return GetExcludedColumnName(columnName);
311316
}
@@ -405,4 +410,64 @@ private string ToSqlExpression<TEntity>(DbContext context, TableMetadata table,
405410
throw new NotSupportedException($"Expression not supported: {expr.NodeType}");
406411
}
407412
}
413+
414+
/// <summary>
415+
/// Extracts update SQL statements from a MemberInitExpression, handling both simple properties
416+
/// and nested complex property initializations recursively.
417+
/// </summary>
418+
/// <param name="context">DB context</param>
419+
/// <param name="table">Table metadata</param>
420+
/// <param name="memberInit">The member initialization expression</param>
421+
/// <param name="lambda">Current lambda expression</param>
422+
/// <typeparam name="T">Entity type</typeparam>
423+
/// <returns>SQL update statements for each property assignment</returns>
424+
private IEnumerable<string> GetUpdatesFromMemberInit<T>(DbContext context, TableMetadata table, MemberInitExpression memberInit, LambdaExpression lambda)
425+
{
426+
foreach (var binding in memberInit.Bindings.OfType<MemberAssignment>())
427+
{
428+
// Check if the binding expression is a nested MemberInitExpression (complex property assignment)
429+
if (binding.Expression is MemberInitExpression nestedMemberInit)
430+
{
431+
// Recursively process nested complex property assignments to handle arbitrary nesting levels
432+
foreach (var update in GetUpdatesFromMemberInit<T>(context, table, nestedMemberInit, lambda))
433+
{
434+
yield return update;
435+
}
436+
}
437+
else
438+
{
439+
// Simple property assignment - the column name is the property name
440+
yield return $"{table.GetQuotedColumnName(binding.Member.Name)} = {ToSqlExpression<T>(context, table, binding.Expression, lambda)}";
441+
}
442+
}
443+
}
444+
445+
/// <summary>
446+
/// Traverses up a member expression chain to find the root parameter expression.
447+
/// This handles both simple properties (e.g., excluded.Name) and complex properties (e.g., excluded.ComplexObject.Property).
448+
/// </summary>
449+
/// <param name="memberExpr">The member expression to traverse.</param>
450+
/// <returns>The root parameter expression if found; otherwise, null if the expression chain doesn't contain a parameter.</returns>
451+
private static ParameterExpression? GetRootParameter(MemberExpression memberExpr)
452+
{
453+
Expression? current = memberExpr.Expression;
454+
while (current != null)
455+
{
456+
if (current is ParameterExpression param)
457+
{
458+
return param;
459+
}
460+
461+
if (current is MemberExpression nested)
462+
{
463+
current = nested.Expression;
464+
}
465+
else
466+
{
467+
break;
468+
}
469+
}
470+
471+
return null;
472+
}
408473
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/PhenX.EntityFrameworkCore.BulkInsert.Benchmark.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
<ItemGroup>
1010
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
11-
<PackageReference Include="Testcontainers.PostgreSql" Version="4.7.0" />
12-
<PackageReference Include="Testcontainers.MsSql" Version="4.7.0" />
13-
<PackageReference Include="Testcontainers.MySql" Version="4.7.0" />
14-
<PackageReference Include="Testcontainers.Oracle" Version="4.7.0" />
11+
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0" />
12+
<PackageReference Include="Testcontainers.MsSql" Version="4.10.0" />
13+
<PackageReference Include="Testcontainers.MySql" Version="4.10.0" />
14+
<PackageReference Include="Testcontainers.Oracle" Version="4.10.0" />
1515
</ItemGroup>
1616

1717
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorMySql.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected override void ConfigureDbContext()
2222

2323
protected override IDatabaseContainer? GetDbContainer()
2424
{
25-
return new MySqlBuilder()
25+
return new MySqlBuilder("mysql:8.0")
2626
.WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1")
2727
.Build();
2828
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorOracle.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new OracleBuilder()
29-
.WithImage("gvenzl/oracle-free:23-slim-faststart")
28+
return new OracleBuilder("gvenzl/oracle-free:23-slim-faststart")
3029
.Build();
3130
}
3231
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorPostgreSql.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new PostgreSqlBuilder()
28+
return new PostgreSqlBuilder("postgres:15.1")
2929
.WithDatabase("testdb")
3030
.WithUsername("testuser")
3131
.WithPassword("testpassword")

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/Providers/LibComparatorSqlServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ protected override void ConfigureDbContext()
2525

2626
protected override IDatabaseContainer? GetDbContainer()
2727
{
28-
return new MsSqlBuilder().Build();
28+
return new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04").Build();
2929
}
3030
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
2+
3+
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
4+
5+
public interface IDbContextFactory
6+
{
7+
Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName)
8+
where TDbContext : TestDbContextBase, new();
9+
}
Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,51 @@
11
using System.Data.Common;
2+
using System.Reflection;
23

4+
using DotNet.Testcontainers.Builders;
5+
using DotNet.Testcontainers.Configurations;
36
using DotNet.Testcontainers.Containers;
47

58
using Microsoft.EntityFrameworkCore;
69
using Microsoft.Extensions.Logging.Abstractions;
710

811
using PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
912

10-
using Xunit;
13+
using Testcontainers.Xunit;
14+
15+
using Xunit.Abstractions;
1116

1217
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1318

14-
public abstract class TestDbContainer : IAsyncLifetime
19+
public abstract class TestDbContainer<TBuilderEntity, TContainerEntity>(IMessageSink messageSink) : DbContainerFixture<TBuilderEntity, TContainerEntity>(messageSink), IDbContextFactory
20+
where TBuilderEntity : IContainerBuilder<TBuilderEntity, TContainerEntity, IContainerConfiguration>, new()
21+
where TContainerEntity : IContainer, IDatabaseContainer
1522
{
16-
private readonly TimeSpan _waitTime = TimeSpan.FromSeconds(30);
17-
private readonly HashSet<string> _connected = [];
18-
protected readonly IDatabaseContainer? DbContainer;
23+
protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName);
24+
25+
protected abstract TBuilderEntity CreateBuilder();
26+
27+
protected virtual string DbmsName => typeof(TContainerEntity).Name.Replace("Container", "");
1928

20-
protected TestDbContainer()
29+
protected override TBuilderEntity Configure()
2130
{
22-
DbContainer = GetDbContainer();
31+
var targetFramework = GetType().Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().FirstOrDefault(e => e.Key == "TargetFramework")?.Value ?? "NA";
32+
return CreateBuilder()
33+
.WithReuse(true)
34+
.WithName($"PhenX.EntityFrameworkCore.BulkInsert.Tests.{DbmsName}-{targetFramework}")
35+
.WithWaitStrategy(Wait.ForUnixContainer().UntilDatabaseIsAvailable(DbProviderFactory));
2336
}
2437

25-
protected abstract IDatabaseContainer? GetDbContainer();
26-
2738
protected virtual string GetConnectionString(string databaseName)
2839
{
29-
if (DbContainer == null)
30-
{
31-
return string.Empty;
32-
}
33-
34-
var builder = new DbConnectionStringBuilder()
35-
{
36-
ConnectionString = DbContainer.GetConnectionString()
37-
};
38-
40+
var builder = DbProviderFactory.CreateConnectionStringBuilder() ?? new DbConnectionStringBuilder();
41+
builder.ConnectionString = ConnectionString;
3942
builder["database"] = databaseName;
4043
return builder.ToString();
4144
}
4245

43-
protected abstract void Configure(DbContextOptionsBuilder optionsBuilder, string databaseName);
44-
45-
public async Task InitializeAsync()
46+
protected virtual async Task EnsureDatabaseCreatedAsync(Microsoft.EntityFrameworkCore.DbContext dbContext)
4647
{
47-
if (DbContainer != null)
48-
{
49-
await DbContainer.StartAsync();
50-
}
48+
await dbContext.Database.EnsureCreatedAsync();
5149
}
5250

5351
public async Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName)
@@ -62,39 +60,8 @@ public async Task<TDbContext> CreateContextAsync<TDbContext>(string databaseName
6260
}
6361
};
6462

65-
if (_connected.Add(databaseName))
66-
{
67-
await EnsureConnectedAsync(dbContext, databaseName);
68-
}
69-
70-
try
71-
{
72-
await dbContext.Database.EnsureCreatedAsync();
73-
}
74-
catch
75-
{
76-
// Often fails with SQL server.
77-
}
63+
await EnsureDatabaseCreatedAsync(dbContext);
7864

7965
return dbContext;
8066
}
81-
82-
protected virtual async Task EnsureConnectedAsync<TDbContext>(TDbContext context, string databaseName)
83-
where TDbContext : TestDbContextBase
84-
{
85-
using var cts = new CancellationTokenSource(_waitTime);
86-
87-
while (!await context.Database.CanConnectAsync(cts.Token))
88-
{
89-
await Task.Delay(100, cts.Token);
90-
}
91-
}
92-
93-
public async Task DisposeAsync()
94-
{
95-
if (DbContainer != null)
96-
{
97-
await DbContainer.DisposeAsync();
98-
}
99-
}
10067
}
Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
using DotNet.Testcontainers.Containers;
1+
using System.Data.Common;
22

33
using Microsoft.EntityFrameworkCore;
44

5+
using MySqlConnector;
6+
57
using PhenX.EntityFrameworkCore.BulkInsert.MySql;
68

9+
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
10+
711
using Testcontainers.MySql;
812

913
using Xunit;
14+
using Xunit.Abstractions;
1015

1116
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContainer;
1217

@@ -16,16 +21,22 @@ public class TestDbContainerMySqlCollection : ICollectionFixture<TestDbContainer
1621
public const string Name = "MySql";
1722
}
1823

19-
public class TestDbContainerMySql() : TestDbContainer
24+
public class TestDbContainerMySql(IMessageSink messageSink) : TestDbContainer<MySqlBuilder, MySqlContainer>(messageSink)
2025
{
21-
protected override IDatabaseContainer? GetDbContainer()
26+
private static readonly ServerVersion MySqlServerVersion = ServerVersion.Create(new Version(8, 0), ServerType.MySql);
27+
28+
public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
29+
30+
protected override MySqlBuilder CreateBuilder() => new($"{MySqlServerVersion.TypeIdentifier}:{MySqlServerVersion.Version}");
31+
32+
protected override string DbmsName => MySqlServerVersion.Type.ToString();
33+
34+
protected override MySqlBuilder Configure()
2235
{
23-
return new MySqlBuilder()
36+
return base.Configure()
2437
.WithCommand("--log-bin-trust-function-creators=1", "--local-infile=1", "--innodb-print-all-deadlocks=ON")
25-
.WithReuse(true)
2638
.WithUsername("root")
27-
.WithPassword("root")
28-
.Build();
39+
.WithPassword("root");
2940
}
3041

3142
protected override string GetConnectionString(string databaseName)
@@ -38,18 +49,10 @@ protected override void Configure(DbContextOptionsBuilder optionsBuilder, string
3849
var connectionString = GetConnectionString(databaseName);
3950

4051
optionsBuilder
41-
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), o =>
52+
.UseMySql(connectionString, MySqlServerVersion, o =>
4253
{
4354
o.UseNetTopologySuite();
4455
})
4556
.UseBulkInsertMySql();
4657
}
47-
48-
protected override async Task EnsureConnectedAsync<TDbContext>(TDbContext context, string databaseName)
49-
{
50-
var container = (MySqlContainer)DbContainer!;
51-
52-
await container.ExecScriptAsync($"CREATE DATABASE `{databaseName}`");
53-
await base.EnsureConnectedAsync(context, databaseName);
54-
}
5558
}

0 commit comments

Comments
 (0)