Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExecuteUpdate passed a complex type instance throws when using existing table values in the update #32987

Open
ajcvickers opened this issue Feb 1, 2024 · 1 comment
Assignees
Milestone

Comments

@ajcvickers
Copy link
Contributor

This is fine:

await context.Foos.ExecuteUpdateAsync(s => s.SetProperty(e => e.Address, new Address("B", "A")));

This throws:

await context.Foos.ExecuteUpdateAsync(s => s.SetProperty(e => e.Address, b => new Address(b.Address.Line2, "B")));
 System.InvalidOperationException: The LINQ expression 'DbSet<Foo>()
    .ExecuteUpdate(s => s.SetProperty<Address>(
        propertyExpression: e => e.Address,
        valueExpression: b => new Address(
            b.Address.Line2,
            "B"
        )))' could not be translated. Additional information: The following lambda argument to 'SetProperty' does not represent a valid value: 'b => new Address(
    b.Address.Line2,
    "B"
)'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Full code:

using (var context = new SomeDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(new Foo { Address = new Address("A", "B") });
    
    await context.SaveChangesAsync();
}

using (var context = new SomeDbContext())
{
	// Works
	await context.Foos.ExecuteUpdateAsync(s => s.SetProperty(e => e.Address, new Address("B", "A")));
	
	// Throws
	await context.Foos.ExecuteUpdateAsync(s => s.SetProperty(e => e.Address, b => new Address(b.Address.Line2, "B")));
}

public class SomeDbContext : DbContext
{
	public DbSet<Foo> Foos => Set<Foo>();
	
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
		=> optionsBuilder
			.UseSqlServer(@"Data Source=localhost;Database=One;Integrated Security=True;Trust Server Certificate=True")
			.LogTo(Console.WriteLine)
			.EnableSensitiveDataLogging();
}

public class Foo
{
	public int Id { get; set; }
	public required Address Address { get; set; }
}

[ComplexType]
public record Address(string Line1, string Line2);
@roji
Copy link
Member

roji commented Feb 1, 2024

Confirmed this as a bug in our ExecuteUpdate complex property assignment feature (#32058, done for 9.0).

The general problem is that we don't translate MemberInitExpression - which makes sense as there's no SQL equivalent for it. However, we do support it in some specific scenarios, e.g. equality comparison for this does work (Where(e => e.Address == new Address { Line1 = b.Address.Line1, Line2 = "foo }) - this is because there's specific fallback logic that kicks in when translation fails, which handles MemberInitExpression. Another important case is that MemberInitExpression can be projected out via Select(), and later selected again.

The general theme here is that although MemberInitExpression isn't really translatable, it's still OK as a temporary thing during translation, where whatever is composed on top may make it valid; conceptually it's a bit similar to the situation with GroupBy() before we added support for final GroupBy - although the tree is temporarily invalid, further composition may make it valid again.

So we can introduce a new SqlExpression node, representing a temporary MemberInitExpression translation, and identify that and do the appropriate thing (but if the end tree has such a node, we throw e.g. in SQL generation). The less architectural alternative is to change things around in the ExecuteUpdate logic to handle translation failure and MemberInitExpression, similar to what we're already doing in complex type equality.

@ajcvickers ajcvickers added this to the 9.0.0 milestone Feb 8, 2024
@roji roji modified the milestones: 9.0.0, Backlog Jul 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants