Skip to content

Conversation

@SerhiiKozachenko
Copy link

@SerhiiKozachenko SerhiiKozachenko commented Dec 8, 2025

Refactor processing of method call expressions in setters.

Problem it solves:

ExecuteUpdateAsync with SetProperty chain generates reversed parameters in AOT Interceptors, causing InvalidCastException.

When running dotnet ef dbcontext optimize with --precompile-queries and --nativeaot, the generated interceptor code for an ExecuteUpdateAsync chain appears to process the SetProperty calls in reverse order (Last-In-First-Out) instead of source order.

This results in a mismatch between the parameter expected by the generated SQL and the value supplied by the interceptor.

Steps to Reproduce:

  1. Define an ExecuteUpdateAsync call with multiple SetProperty calls of different types.
var rowsAffected = await _context.Todos
    .Where(t => t.Id == todoId)
    .ExecuteUpdateAsync(s => s
        .SetProperty(t => t.IsDeleted, v => isDeleted) // Bool (First)
        .SetProperty(t => t.LastModified, v => now),   // DateTime (Second)
        ct);
  1. Run the optimization command: dotnet ef dbcontext optimize --precompile-queries --nativeaot ...
  2. Observe the generated code in EfTodoRepository.EFInterceptors.TodoDbContext.cs.

Expected Behavior: The interceptor should map Expressions[0] to the first property (IsDeleted) and Expressions[1] to the second (LastModified), matching the source order.

Actual Behavior: The generated code assumes Expressions[0] corresponds to the last property written (LastModified/now), effectively reversing the parameters.

// Generated Interceptor Code:

// 1. Accesses Expressions[0]. 
// The generator assumes this is "now" (DateTime), but due to tree traversal order, 
// this index actually holds the "isDeleted" (Bool) expression.
var new1 = (NewExpression)setters.Expressions[0];
var lambda2 = (LambdaExpression)new1.Arguments[1];
queryContext.Parameters.Add(
    "now", 
    Expression.Lambda<Func<object?>>(Expression.Convert(lambda2.Body, typeof(object)))
    .Compile(preferInterpretation: true)
    .Invoke()); 

// 2. Accesses Expressions[1].
// The generator assumes this is "isDeleted", but it holds "now".
var new4 = (NewExpression)setters.Expressions[1];
var lambda5 = (LambdaExpression)new4.Arguments[1];
queryContext.Parameters.Add(
    "isDeleted",
    Expression.Lambda<Func<object?>>(Expression.Convert(lambda5.Body, typeof(object)))
    .Compile(preferInterpretation: true)
    .Invoke());

Runtime Exception: Because Expressions[0] (Boolean) is being passed into the parameter expecting DateTime (TimestampTz), Npgsql throws a cast exception:

System.InvalidCastException: Writing values of 'System.Boolean' is not supported for parameters having NpgsqlDbType 'TimestampTz'.
   at Npgsql.Internal.AdoSerializerHelpers.<GetTypeInfoForWriting>g__ThrowWritingNotSupported...
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQueryAsync...

Proposed Fix: The issue appears to be in PrecompiledQueryCodeGenerator.ProcessExecuteUpdate. The method iterates over the MethodCallExpression tree (which is nested inside-out) but does not reverse the order before building the setters array.

Using a Stack<MethodCallExpression> to collect and then reverse the calls fixes the issue by ensuring setters are added in source code order (First -> Last).

  • I've read the guidelines for contributing and seen the walkthrough
  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team
  • The code builds and tests pass locally (also verified by our automated build checks)
  • Commit messages follow this format:
        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

Refactor processing of method call expressions in setters.
@SerhiiKozachenko SerhiiKozachenko requested a review from a team as a code owner December 8, 2025 22:49
Remove incorrect double-addition of properties during tree traversal
@SerhiiKozachenko
Copy link
Author

@dotnet-policy-service agree

@roji
Copy link
Member

roji commented Dec 9, 2025

@SerhiiKozachenko what's the problem this is solving exactly, and why is this needed? When submitting a PR, please include at least a minimal description of what it's for.

@SerhiiKozachenko
Copy link
Author

@roji I have updated description with more info, please check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants