Skip to content

Fix complex property update in ON CONFLICT clause#89

Merged
PhenX merged 7 commits intomainfrom
copilot/fix-bulkinset-complexproperties
Jan 18, 2026
Merged

Fix complex property update in ON CONFLICT clause#89
PhenX merged 7 commits intomainfrom
copilot/fix-bulkinset-complexproperties

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 16, 2026

  • Understand the repository structure and codebase
  • Identify the root cause of the bug
  • Fix the bug in SqlDialectBuilder.cs to handle nested MemberExpressions for complex properties
  • Fix nested MemberInitExpression for complex property assignments in Update expressions
  • Make GetUpdatesFromMemberInit truly recursive for arbitrary nesting levels
  • Add test cases for complex property updates in merge operations
  • Verify the fix with existing tests (all 100 SQLite tests pass)
  • Run code review and address feedback
  • Run CodeQL security check (no issues found)

Changes Made

1. Handle nested MemberExpression for complex property access

Added GetRootParameter() helper to traverse nested expression chains for complex property access in WHERE clauses.

2. Handle nested MemberInitExpression for complex property assignments

Added GetUpdatesFromMemberInit() helper method to properly handle Update expressions with nested object initialization for complex properties. The method is truly recursive to support arbitrary nesting levels.

Example supported expression:

Update = (inserted, excluded) => new TestEntityWithComplexType
{
    OwnedComplexType = new OwnedObject
    {
        Code = excluded.OwnedComplexType.Code,
        Name = excluded.OwnedComplexType.Name
    }
}

Tests Added

  • InsertEntities_WithComplexType_UpdateAll - Tests updating all columns including complex properties
  • InsertEntities_WithComplexType_UpdateWithWhere - Tests complex property access in WHERE clause
  • InsertEntities_WithComplexType_UpdateComplexPropertyConditionally - Tests nested MemberInitExpression for complex property assignment
Original prompt

This section details on the original issue you should resolve

<issue_title>BulkInset not update ComplexPropety properties</issue_title>
<issue_description>repo with an example https://github.com/Ashymonth/ComplexPropertyBug

When you have complex properties in your model and try to update them with the provided matching function, the complex property properties are not updated.</issue_description>

<agent_instructions>This lib supports complex properties, but it seems that when merging data with onConflictOptions, they are not updated.

You need to find the issue, and add new test cases.

The reporter linked a full repro, here is the code :

// ######### DbContext and factory :

using ComplexTypeBug.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace ComplexTypeBug;

public class ComplexTypeBugDbContext : DbContext
{
public ComplexTypeBugDbContext(DbContextOptions options) : base(options)
{
}

public DbSet<TestTable> TestTables { get; set; }
public DbSet<AnotherTestTable> AnotherTestTables { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<TestTable>().HasKey(table => new { table.Key, table.Date });
    modelBuilder.Entity<TestTable>().ComplexProperty(table => table.ComplexObject);
    modelBuilder.Entity<AnotherTestTable>().ComplexProperty(table => table.ComplexProperty);
}

}

public class ComplexTypeBugDbContextFactory : IDesignTimeDbContextFactory
{
public ComplexTypeBugDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseNpgsql("Server=localhost;Database=testdb;User Id=postgres;Password=admin;Include Error Detail = true");

    return new ComplexTypeBugDbContext(optionsBuilder.Options);
}

}

// ######### Program.cs :

using System.Globalization;
using ComplexTypeBug;
using ComplexTypeBug.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PhenX.EntityFrameworkCore.BulkInsert.Extensions;
using PhenX.EntityFrameworkCore.BulkInsert.Options;
using PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;

var services = new ServiceCollection()
.AddDbContext(builder =>
{
builder.LogTo(Console.WriteLine, LogLevel.Information);
builder.EnableSensitiveDataLogging();
builder.UseBulkInsertPostgreSql();
builder.UseNpgsql(
"Server=localhost;Database=testdb;User Id=postgres;Password=admin;Include Error Detail = true");
}).BuildServiceProvider();

var now = DateTime.UtcNow;
var tableWithComplexTypeData = new TestTable()
{
Key = "1",
Date = now,
ComplexObject = new TestComplexObject
{
Property = "test",
Property1 = 100
}
};

var dbContext = services.GetRequiredService();

// insert okey
await dbContext.ExecuteBulkInsertAsync([tableWithComplexTypeData], onConflict: new OnConflictOptions
{
Match = table => new { table.Key, table.Date }
});

tableWithComplexTypeData.ComplexObject = new TestComplexObject
{
Property = "changed",
Property1 = 200
};

//not updated
await dbContext.ExecuteBulkInsertAsync([tableWithComplexTypeData], onConflict: new OnConflictOptions
{
Match = table => new { table.Key, table.Date}
});

dbContext.ChangeTracker.Clear();

// result will have
// Property = "test",
// Property1 = 100

var result = dbContext.TestTables.ToArray();

Console.ReadLine();

// ######### Models
namespace ComplexTypeBug.Models;

public class TestTable
{
public DateTime Date { get; set; }

public string Key { get; set; } = null!;

public TestComplexObject ComplexObject { get; set; } = null!;

}

public class AnotherTestTable
{
public Guid Id { get; set; }

public TestComplexObject ComplexProperty { get; set; } = null!;

}

public class TestComplexObject
{
public string Property { get; set; } = null!;

public decimal Property1 { get; set; } 

}</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits January 16, 2026 16:35
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix BulkInset issue with ComplexProperty updates Fix complex property update in ON CONFLICT clause Jan 16, 2026
Copilot AI requested a review from PhenX January 16, 2026 16:42
@PhenX PhenX requested a review from Copilot January 16, 2026 21:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where complex property updates were not working correctly in ON CONFLICT clauses during bulk insert operations. The issue was that the expression parser only checked one level of member access, failing to properly identify the root parameter for nested complex property expressions.

Changes:

  • Added GetRootParameter() helper method to traverse nested MemberExpression chains and find the root parameter
  • Modified parameter resolution logic in ToSqlExpression() to use the new helper for proper handling of complex property access
  • Added comprehensive test coverage for complex property updates in merge operations with and without WHERE clauses

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
SqlDialectBuilder.cs Added GetRootParameter() method to traverse expression trees and fixed parameter identification for complex properties in UPDATE/WHERE expressions
MergeTestsBase.cs Added two test methods to verify complex property updates work correctly in merge operations, including scenarios with WHERE conditions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@PhenX PhenX marked this pull request as ready for review January 18, 2026 15:54
@PhenX
Copy link
Copy Markdown
Owner

PhenX commented Jan 18, 2026

@copilot it does not work (error : Property OwnedComplexType not found in entity type PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext.TestEntityWithComplexType.) with a test like this :

[SkippableTheory]
[InlineData(InsertStrategy.InsertReturn)]
[InlineData(InsertStrategy.InsertReturnAsync)]
public async Task InsertEntities_WithComplexType_UpdateComplexPropertyConditionally(InsertStrategy strategy)
{
    Skip.If(_context.IsProvider(ProviderType.MySql));
    // Oracle MERGE does not support returning entities
    Skip.If(_context.IsProvider(ProviderType.Oracle));

    // Arrange - Create entities with different Code values
    var entities = new List<TestEntityWithComplexType>
    {
        new TestEntityWithComplexType
        {
            TestRun = _run,
            OwnedComplexType = new OwnedObject { Code = 50, Name = "LowCode" }
        },
        new TestEntityWithComplexType
        {
            TestRun = _run,
            OwnedComplexType = new OwnedObject { Code = 150, Name = "HighCode" }
        }
    };

    // Act - First insert
    await _context.ExecuteBulkInsertAsync(entities);

    // Update both entities
    foreach (var entity in entities)
    {
        entity.OwnedComplexType.Code = entity.OwnedComplexType.Code + 10;
        entity.OwnedComplexType.Name = $"Modified_{entity.OwnedComplexType.Name}";
    }

    // Act - Update only where Code < 100 (only the first entity should be updated)
    var updatedEntities = await _context.InsertWithStrategyAsync(strategy, entities,
        onConflict: new OnConflictOptions<TestEntityWithComplexType>
        {
            Match = e => new { e.TestRun },
            Update = (inserted, excluded) => new TestEntityWithComplexType
            {
                OwnedComplexType = new OwnedObject
                {
                    Code = excluded.OwnedComplexType.Code,
                    Name = excluded.OwnedComplexType.Name
                }
            },
            Where = (inserted, excluded) => inserted.OwnedComplexType.Code < 100
        });

    // Assert - Only the entity with original Code < 100 should be updated
    Assert.Equal(2, updatedEntities.Count);
    Assert.Contains(updatedEntities, e => e.OwnedComplexType.Code == 60 && e.OwnedComplexType.Name == "Modified_LowCode");
    Assert.Contains(updatedEntities, e => e.OwnedComplexType.Code == 150 && e.OwnedComplexType.Name == "HighCode");
}

Copilot AI and others added 2 commits January 18, 2026 20:14
…NFLICT

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot it does not work (error : Property OwnedComplexType not found in entity type PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext.TestEntityWithComplexType.) with a test like this :

...

Fixed in 43b45ce. The issue was that when using nested MemberInitExpression for complex property assignment like OwnedComplexType = new OwnedObject { Code = ..., Name = ... }, the code was trying to find a column named "OwnedComplexType" instead of recursively extracting the individual property assignments.

Added GetUpdatesFromMemberInit() helper that recursively processes nested object initializations to extract the actual column assignments. The test you provided now passes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@PhenX PhenX merged commit dd47f26 into main Jan 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BulkInset not update ComplexPropety properties

3 participants