Skip to content

Commit 4dbb7b9

Browse files
CopilotPhenX
andcommitted
Fix nested MemberInitExpression for complex property updates in ON CONFLICT
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
1 parent 7cfed68 commit 4dbb7b9

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

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

Lines changed: 34 additions & 2 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;
@@ -411,6 +411,38 @@ private string ToSqlExpression<TEntity>(DbContext context, TableMetadata table,
411411
}
412412
}
413413

414+
/// <summary>
415+
/// Extracts update SQL statements from a MemberInitExpression, handling both simple properties
416+
/// and nested complex property initializations.
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
432+
foreach (var nestedBinding in nestedMemberInit.Bindings.OfType<MemberAssignment>())
433+
{
434+
// For complex properties, the column name is the nested property name (e.g., "Code", "Name")
435+
yield return $"{table.GetQuotedColumnName(nestedBinding.Member.Name)} = {ToSqlExpression<T>(context, table, nestedBinding.Expression, lambda)}";
436+
}
437+
}
438+
else
439+
{
440+
// Simple property assignment
441+
yield return $"{table.GetQuotedColumnName(binding.Member.Name)} = {ToSqlExpression<T>(context, table, binding.Expression, lambda)}";
442+
}
443+
}
444+
}
445+
414446
/// <summary>
415447
/// Traverses up a member expression chain to find the root parameter expression.
416448
/// This handles both simple properties (e.g., excluded.Name) and complex properties (e.g., excluded.ComplexObject.Property).

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Merge/MergeTestsBase.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,4 +455,63 @@ public async Task InsertEntities_WithComplexType_UpdateWithWhere(InsertStrategy
455455
Assert.True(e.OwnedComplexType.Code > 100);
456456
});
457457
}
458+
459+
[SkippableTheory]
460+
[InlineData(InsertStrategy.InsertReturn)]
461+
[InlineData(InsertStrategy.InsertReturnAsync)]
462+
public async Task InsertEntities_WithComplexType_UpdateComplexPropertyConditionally(InsertStrategy strategy)
463+
{
464+
Skip.If(_context.IsProvider(ProviderType.MySql));
465+
// Oracle MERGE does not support returning entities
466+
Skip.If(_context.IsProvider(ProviderType.Oracle));
467+
468+
// Arrange - Create entities with different Code values
469+
var entities = new List<TestEntityWithComplexType>
470+
{
471+
new TestEntityWithComplexType
472+
{
473+
TestRun = _run,
474+
OwnedComplexType = new OwnedObject { Code = 50, Name = "LowCode" }
475+
},
476+
new TestEntityWithComplexType
477+
{
478+
TestRun = _run,
479+
OwnedComplexType = new OwnedObject { Code = 150, Name = "HighCode" }
480+
}
481+
};
482+
483+
// Act - First insert (returns entities with generated IDs)
484+
var insertedEntities = await _context.InsertWithStrategyAsync(strategy, entities);
485+
486+
// Update both entities with new values
487+
foreach (var entity in insertedEntities)
488+
{
489+
entity.OwnedComplexType.Code = entity.OwnedComplexType.Code + 10;
490+
entity.OwnedComplexType.Name = $"Modified_{entity.OwnedComplexType.Name}";
491+
}
492+
493+
// Act - Update using nested MemberInitExpression for complex property assignment
494+
// Note: entities with Code >= 100 (original value) will not be updated due to WHERE clause
495+
var updatedEntities = await _context.InsertWithStrategyAsync(strategy, insertedEntities,
496+
o => o.CopyGeneratedColumns = true,
497+
onConflict: new OnConflictOptions<TestEntityWithComplexType>
498+
{
499+
Update = (inserted, excluded) => new TestEntityWithComplexType
500+
{
501+
OwnedComplexType = new OwnedObject
502+
{
503+
Code = excluded.OwnedComplexType.Code,
504+
Name = excluded.OwnedComplexType.Name
505+
}
506+
},
507+
Where = (inserted, excluded) => inserted.OwnedComplexType.Code < 100
508+
});
509+
510+
// Assert - Only the entity with original Code < 100 should be updated (Code was 50, now 60)
511+
// The one with original Code >= 100 is not updated but is also not returned by RETURNING clause
512+
Assert.Single(updatedEntities);
513+
var updatedEntity = updatedEntities.Single();
514+
Assert.Equal(60, updatedEntity.OwnedComplexType.Code);
515+
Assert.Equal("Modified_LowCode", updatedEntity.OwnedComplexType.Name);
516+
}
458517
}

0 commit comments

Comments
 (0)