Skip to content

Support EF Core ExecuteUpdate via IRewritableQueryable#17

Open
koenbeuk wants to merge 4 commits intomainfrom
feature/execute-update
Open

Support EF Core ExecuteUpdate via IRewritableQueryable#17
koenbeuk wants to merge 4 commits intomainfrom
feature/execute-update

Conversation

@koenbeuk
Copy link
Copy Markdown
Collaborator

@koenbeuk koenbeuk commented Apr 1, 2026

Summary

Closes #6

  • Add ExecuteUpdate/ExecuteUpdateAsync stubs on IRewritableQueryable<T> in the RelationalExtensions project with [PolyfillTarget] routing (EF Core 8 → RelationalQueryableExtensions, EF Core 9 → EntityFrameworkQueryableExtensions)
  • Fix generic method overload disambiguation in ReflectionFieldCache to correctly resolve SetProperty<P>(Func<T,P>, P) vs SetProperty<P>(Func<T,P>, Func<T,P>) by checking parameter type patterns
  • Stubs excluded on net10.0 where EF Core replaced SetPropertyCalls<T> with Action<UpdateSettersBuilder<T>>

What this enables

Modern C# syntax inside SetProperty value expressions that the C# compiler normally rejects in expression trees:

ctx.ExpressiveProducts.ExecuteUpdate(s => s
    .SetProperty(p => p.Tag, p => p.Price switch
    {
        > 100 => "premium",
        > 50 => "standard",
        _ => "budget"
    })
    .SetProperty(p => p.Category, p => p.Category ?? "UNKNOWN"));

Test plan

  • Generator snapshot tests: constant value, null-conditional, switch expression, async variant (4 tests)
  • EF Core integration tests against SQLite: basic constant, switch expression, null-coalescing, multiple properties, async (5 tests)
  • All 1977 existing tests pass across net8.0/net9.0/net10.0

🤖 Generated with Claude Code

koenbeuk and others added 2 commits April 1, 2026 00:54
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ExecuteUpdate/ExecuteUpdateAsync stubs to RelationalExtensions with
[PolyfillTarget] routing (RelationalQueryableExtensions on EF Core 8,
EntityFrameworkQueryableExtensions on EF Core 9). Enables modern C# syntax
(switch expressions, null-conditional) inside SetProperty value lambdas.

Fix generic method overload disambiguation in ReflectionFieldCache by
checking parameter type patterns (IsGenericType vs IsGenericParameter) to
correctly resolve SetProperty<P>(Func<T,P>, P) vs SetProperty<P>(Func<T,P>, Func<T,P>).

Stubs are conditional on !NET10_0_OR_GREATER since EF Core 10 replaced
SetPropertyCalls<T> with Action<UpdateSettersBuilder<T>>.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 01:44
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

Adds ExecuteUpdate/ExecuteUpdateAsync support to ExpressiveSharp’s EF Core relational extensions by introducing intercepted stubs on IRewritableQueryable<T> (EF Core 8/9 only), plus a generator fix to correctly resolve ambiguous generic overloads when emitting reflection-based MethodInfo lookups.

Changes:

  • Add EF Core 8/9 ExecuteUpdate/ExecuteUpdateAsync polyfill stubs for IRewritableQueryable<T> in the RelationalExtensions package (excluded on net10.0+).
  • Update generator method resolution logic (ReflectionFieldCache) to better disambiguate generic overloads based on parameter-type patterns.
  • Add generator snapshot tests and EF Core SQLite integration tests covering ExecuteUpdate with constant, null-coalescing, switch expression, multiple setters, and async variants.

Reviewed changes

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

Show a summary per file
File Description
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/JoinTests.GroupJoin_AnonymousResultSelector_GeneratesGenericInterceptor.verified.txt Updates snapshot due to new overload-disambiguation logic in generated reflection lookups.
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdateAsync_GeneratesInterceptor.verified.txt New snapshot validating generated interceptor for ExecuteUpdateAsync.
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithSwitchExpression.verified.txt New snapshot validating switch-expression rewriting inside SetProperty value lambda.
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithNullConditional.verified.txt New snapshot validating null-conditional/coalesce rewriting inside SetProperty value lambda.
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_ConstantValue.verified.txt New snapshot validating constant-value setter overload selection and emission.
tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.cs New generator tests that compile mock EF Core APIs and verify produced interceptors/snapshots.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/PropertyTests.ExpressivePropertyToNavigationalProperty.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.TypesInBodyGetsFullyQualified.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_WithFullObject.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_SpreadOnly.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_MultipleSpreads.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ListWithSpread.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ArrayWithSpread.verified.txt Snapshot updates from the refined reflection method selection predicate.
tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/ExecuteUpdateTestDbContext.cs Adds EF Core test model + DbContext used for ExecuteUpdate integration tests.
tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/ExecuteUpdateIntegrationTests.cs Adds end-to-end SQLite tests for ExecuteUpdate/ExecuteUpdateAsync with modern C# syntax in setter lambdas.
src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs Adds parameter-type-pattern checks to disambiguate generic method overloads during reflection lookup emission.
src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Extensions/RewritableQueryableRelationalExtensions.cs Adds intercepted EF Core 8/9 ExecuteUpdate stubs on IRewritableQueryable<T> with version-dependent PolyfillTarget routing.
.gitignore Ignores .worktrees/ and normalizes docs dist ignore entry formatting.

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

Comment on lines +76 to +78
paramChecks += $" && !m.GetParameters()[{i}].ParameterType.IsGenericType";
else if (paramType is INamedTypeSymbol { IsGenericType: true })
paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericType";
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The overload-disambiguation predicate treats type-parameter parameters as !ParameterType.IsGenericType. That also matches any non-generic concrete type (e.g., int, string), so it can still select the wrong overload when a competing overload has a non-generic parameter at the same position. For ITypeParameterSymbol parameters, the reflection check should be ParameterType.IsGenericParameter (and potentially also exclude IsGenericType) to precisely match a generic method's type parameter.

Suggested change
paramChecks += $" && !m.GetParameters()[{i}].ParameterType.IsGenericType";
else if (paramType is INamedTypeSymbol { IsGenericType: true })
paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericType";
paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericParameter && !m.GetParameters()[{i}].ParameterType.IsGenericType";
else if (paramType is INamedTypeSymbol { IsGenericType: true })
paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericType && !m.GetParameters()[{i}].ParameterType.IsGenericParameter";

Copilot uses AI. Check for mistakes.
koenbeuk and others added 2 commits April 1, 2026 01:50
Address CodeQL string-concatenation-in-loop suggestion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ExecuteUpdate/ExecuteUpdateAsync section to rewritable-queryable.md
and ef-core-integration.md with usage examples, version compatibility
notes, and updated RelationalExtensions package description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Support for EFCore ExecuteUpdate

2 participants