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

3 participants