From 9729705f16020383ffecdfaf966c4472d9960603 Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 1 Apr 2026 00:54:47 +0000 Subject: [PATCH 1/4] Add .worktrees/ to gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7019c03..3397fd9 100644 --- a/.gitignore +++ b/.gitignore @@ -372,4 +372,6 @@ ReadmeSample.db # VitePress docs docs/node_modules/ docs/.vitepress/cache/ -docs/.vitepress/dist/ \ No newline at end of file +docs/.vitepress/dist/ +# Worktrees +.worktrees/ From 3490cd04a29c163ae930116a81e39a9326d9568e Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 1 Apr 2026 01:35:40 +0000 Subject: [PATCH 2/4] Support EF Core ExecuteUpdate via IRewritableQueryable (#6) 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

(Func, P) vs SetProperty

(Func, Func). Stubs are conditional on !NET10_0_OR_GREATER since EF Core 10 replaced SetPropertyCalls with Action>. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...RewritableQueryableRelationalExtensions.cs | 56 ++++++ .../Emitter/ReflectionFieldCache.cs | 16 +- .../ExecuteUpdateIntegrationTests.cs | 172 ++++++++++++++++ .../Models/ExecuteUpdateTestDbContext.cs | 31 +++ ...ionExpression_ArrayWithSpread.verified.txt | 6 +- ...tionExpression_ListWithSpread.verified.txt | 6 +- ...ionExpression_MultipleSpreads.verified.txt | 4 +- ...llectionExpression_SpreadOnly.verified.txt | 2 +- ...bleConstructor_WithFullObject.verified.txt | 2 +- ...TypesInBodyGetsFullyQualified.verified.txt | 2 +- ...ropertyToNavigationalProperty.verified.txt | 2 +- ...ateAsync_GeneratesInterceptor.verified.txt | 37 ++++ ...ate_SetProperty_ConstantValue.verified.txt | 35 ++++ ...tProperty_WithNullConditional.verified.txt | 44 ++++ ...Property_WithSwitchExpression.verified.txt | 46 +++++ .../ExecuteUpdateTests.cs | 188 ++++++++++++++++++ ...r_GeneratesGenericInterceptor.verified.txt | 2 +- 17 files changed, 637 insertions(+), 14 deletions(-) create mode 100644 src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Extensions/RewritableQueryableRelationalExtensions.cs create mode 100644 tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/ExecuteUpdateIntegrationTests.cs create mode 100644 tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/ExecuteUpdateTestDbContext.cs create mode 100644 tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdateAsync_GeneratesInterceptor.verified.txt create mode 100644 tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_ConstantValue.verified.txt create mode 100644 tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithNullConditional.verified.txt create mode 100644 tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithSwitchExpression.verified.txt create mode 100644 tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.cs diff --git a/src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Extensions/RewritableQueryableRelationalExtensions.cs b/src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Extensions/RewritableQueryableRelationalExtensions.cs new file mode 100644 index 0000000..10e88a3 --- /dev/null +++ b/src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/Extensions/RewritableQueryableRelationalExtensions.cs @@ -0,0 +1,56 @@ +#if !NET10_0_OR_GREATER +using System.ComponentModel; +using System.Diagnostics; +using ExpressiveSharp; +using Microsoft.EntityFrameworkCore.Query; + +// ReSharper disable once CheckNamespace — intentionally in Microsoft.EntityFrameworkCore for discoverability +namespace Microsoft.EntityFrameworkCore; + +///

+/// Extension methods on for EF Core bulk update operations. +/// These stubs are intercepted by the ExpressiveSharp source generator via +/// to forward to the appropriate EF Core ExecuteUpdate method. +/// +/// +/// Only available on EF Core 8/9. In EF Core 10+, ExecuteUpdate uses Action<UpdateSettersBuilder<T>> +/// which natively supports modern C# syntax in the outer lambda. For inner SetProperty value expressions, +/// use ExpressionPolyfill.Create() to enable modern C# syntax. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class RewritableQueryableRelationalExtensions +{ + private const string InterceptedMessage = + "This method must be intercepted by the ExpressiveSharp source generator. " + + "Ensure the generator package is installed and the InterceptorsNamespaces MSBuild property is configured."; + + // ── Bulk update methods (intercepted) ──────────────────────────────── + // EF Core 8: ExecuteUpdate lives on RelationalQueryableExtensions (Relational package) + // EF Core 9: ExecuteUpdate moved to EntityFrameworkQueryableExtensions (Core package) + +#if NET9_0 + [PolyfillTarget(typeof(EntityFrameworkQueryableExtensions))] +#else + [PolyfillTarget(typeof(RelationalQueryableExtensions))] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static int ExecuteUpdate( + this IRewritableQueryable source, + Func, SetPropertyCalls> setPropertyCalls) + where TSource : class + => throw new UnreachableException(InterceptedMessage); + +#if NET9_0 + [PolyfillTarget(typeof(EntityFrameworkQueryableExtensions))] +#else + [PolyfillTarget(typeof(RelationalQueryableExtensions))] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static Task ExecuteUpdateAsync( + this IRewritableQueryable source, + Func, SetPropertyCalls> setPropertyCalls, + CancellationToken cancellationToken = default) + where TSource : class + => throw new UnreachableException(InterceptedMessage); +} +#endif diff --git a/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs b/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs index 87a16d3..b607dc1 100644 --- a/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs +++ b/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs @@ -64,7 +64,21 @@ public string EnsureMethodInfo(IMethodSymbol method) var paramCount = originalDef.Parameters.Length; var typeArgs = string.Join(", ", method.TypeArguments.Select(t => $"typeof({ResolveTypeFqn(t)})")); - return $"global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof({typeFqn}).GetMethods({flags}), m => m.Name == \"{method.Name}\" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == {genericArity} && m.GetParameters().Length == {paramCount})).MakeGenericMethod({typeArgs})"; + + // Disambiguate overloads that share name, generic arity, and parameter count + // (e.g., SetProperty

(Func, P) vs SetProperty

(Func, Func)) + // by checking whether each parameter is a generic type or a type parameter. + var paramChecks = ""; + for (int i = 0; i < originalDef.Parameters.Length; i++) + { + var paramType = originalDef.Parameters[i].Type; + if (paramType is ITypeParameterSymbol) + paramChecks += $" && !m.GetParameters()[{i}].ParameterType.IsGenericType"; + else if (paramType is INamedTypeSymbol { IsGenericType: true }) + paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericType"; + } + + return $"global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof({typeFqn}).GetMethods({flags}), m => m.Name == \"{method.Name}\" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == {genericArity} && m.GetParameters().Length == {paramCount}{paramChecks})).MakeGenericMethod({typeArgs})"; } else { diff --git a/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/ExecuteUpdateIntegrationTests.cs b/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/ExecuteUpdateIntegrationTests.cs new file mode 100644 index 0000000..a0e4aa3 --- /dev/null +++ b/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/ExecuteUpdateIntegrationTests.cs @@ -0,0 +1,172 @@ +#if !NET10_0_OR_GREATER +using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests.Models; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests; + +///

+/// End-to-end integration tests for ExecuteUpdate via IRewritableQueryable. +/// These prove that modern C# syntax (null-conditional, switch expressions) +/// inside SetProperty value expressions works with real SQL execution — functionality +/// that is impossible with normal C# expression trees. +/// +[TestClass] +public class ExecuteUpdateIntegrationTests +{ + private SqliteConnection _connection = null!; + + [TestInitialize] + public void Setup() + { + _connection = new SqliteConnection("Data Source=:memory:"); + _connection.Open(); + } + + [TestCleanup] + public void Cleanup() + { + _connection.Dispose(); + } + + private ExecuteUpdateTestDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .UseExpressives(o => o.UseRelationalExtensions()) + .Options; + var ctx = new ExecuteUpdateTestDbContext(options); + ctx.Database.EnsureCreated(); + return ctx; + } + + private static void SeedProducts(ExecuteUpdateTestDbContext ctx) + { + ctx.Products.AddRange( + new Product { Id = 1, Name = "Widget", Category = "A", Tag = "", Price = 150, Quantity = 10 }, + new Product { Id = 2, Name = "Gadget", Category = "B", Tag = "", Price = 75, Quantity = 5 }, + new Product { Id = 3, Name = "Doohickey", Category = null, Tag = "", Price = 30, Quantity = 20 }); + ctx.SaveChanges(); + } + + /// + /// Basic test: verify the generator intercepts ExecuteUpdate and forwards to EF Core. + /// + [TestMethod] + public void ExecuteUpdate_BasicConstant_Works() + { + using var ctx = CreateContext(); + SeedProducts(ctx); + + var affected = ctx.ExpressiveProducts + .ExecuteUpdate(s => s.SetProperty(p => p.Tag, "basic")); + + Assert.AreEqual(3, affected); + // ExecuteUpdate bypasses change tracker — use AsNoTracking to see actual DB state + var products = ctx.Products.AsNoTracking().OrderBy(p => p.Id).ToList(); + Assert.AreEqual("basic", products[0].Tag); + Assert.AreEqual("basic", products[1].Tag); + Assert.AreEqual("basic", products[2].Tag); + } + + /// + /// Proves new capability: switch expression inside SetProperty value lambda. + /// o.Price switch { > 100 => "premium", > 50 => "standard", _ => "budget" } + /// is impossible in a normal C# expression tree context. + /// + [TestMethod] + public void ExecuteUpdate_SwitchExpression_TranslatesToSql() + { + using var ctx = CreateContext(); + SeedProducts(ctx); + + ctx.ExpressiveProducts + .ExecuteUpdate(s => s.SetProperty( + p => p.Tag, + p => p.Price switch + { + > 100 => "premium", + > 50 => "standard", + _ => "budget" + })); + + var products = ctx.Products.AsNoTracking().OrderBy(p => p.Id).ToList(); + Assert.AreEqual("premium", products[0].Tag); // Price=150 + Assert.AreEqual("standard", products[1].Tag); // Price=75 + Assert.AreEqual("budget", products[2].Tag); // Price=30 + } + + /// + /// Proves new capability: null-coalescing operator inside SetProperty value lambda. + /// + [TestMethod] + public void ExecuteUpdate_NullCoalescing_TranslatesToSql() + { + using var ctx = CreateContext(); + SeedProducts(ctx); + + ctx.ExpressiveProducts + .ExecuteUpdate(s => s.SetProperty( + p => p.Tag, + p => p.Category ?? "UNKNOWN")); + + var products = ctx.Products.AsNoTracking().OrderBy(p => p.Id).ToList(); + Assert.AreEqual("A", products[0].Tag); // Category="A" + Assert.AreEqual("B", products[1].Tag); // Category="B" + Assert.AreEqual("UNKNOWN", products[2].Tag); // Category=null + } + + /// + /// Proves that multiple SetProperty calls with modern C# syntax work together, + /// producing multiple SET clauses in a single SQL UPDATE statement. + /// + [TestMethod] + public void ExecuteUpdate_MultipleProperties_WithModernSyntax() + { + using var ctx = CreateContext(); + SeedProducts(ctx); + + ctx.ExpressiveProducts + .ExecuteUpdate(s => s + .SetProperty(p => p.Tag, p => p.Price switch + { + > 100 => "expensive", + _ => "moderate" + }) + .SetProperty(p => p.Category, "updated")); + + var products = ctx.Products.AsNoTracking().OrderBy(p => p.Id).ToList(); + Assert.AreEqual("expensive", products[0].Tag); // Price=150 + Assert.AreEqual("updated", products[0].Category); + Assert.AreEqual("moderate", products[1].Tag); // Price=75 + Assert.AreEqual("updated", products[1].Category); + Assert.AreEqual("moderate", products[2].Tag); // Price=30 + Assert.AreEqual("updated", products[2].Category); + } + + /// + /// Proves async variant works end-to-end with modern C# syntax. + /// + [TestMethod] + public async Task ExecuteUpdateAsync_SwitchExpression_TranslatesToSql() + { + using var ctx = CreateContext(); + SeedProducts(ctx); + + await ctx.ExpressiveProducts + .ExecuteUpdateAsync(s => s.SetProperty( + p => p.Tag, + p => p.Price switch + { + > 100 => "premium", + > 50 => "standard", + _ => "budget" + })); + + var products = await ctx.Products.AsNoTracking().OrderBy(p => p.Id).ToListAsync(); + Assert.AreEqual("premium", products[0].Tag); + Assert.AreEqual("standard", products[1].Tag); + Assert.AreEqual("budget", products[2].Tag); + } +} +#endif diff --git a/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/ExecuteUpdateTestDbContext.cs b/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/ExecuteUpdateTestDbContext.cs new file mode 100644 index 0000000..f8b0a97 --- /dev/null +++ b/tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/ExecuteUpdateTestDbContext.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; + +namespace ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests.Models; + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string? Category { get; set; } + public string Tag { get; set; } = ""; + public double Price { get; set; } + public int Quantity { get; set; } +} + +public class ExecuteUpdateTestDbContext : DbContext +{ + public DbSet Products => Set(); + public ExpressiveDbSet ExpressiveProducts => this.ExpressiveSet(); + + public ExecuteUpdateTestDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + }); + } +} diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ArrayWithSpread.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ArrayWithSpread.verified.txt index 6e24d8f..188b10a 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ArrayWithSpread.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ArrayWithSpread.verified.txt @@ -17,9 +17,9 @@ namespace ExpressiveSharp.Generated var expr_3 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Items")); // Items var expr_4 = global::System.Linq.Expressions.Expression.Constant(2, typeof(int)); // 2 var expr_5 = global::System.Linq.Expressions.Expression.NewArrayInit(typeof(int), expr_4); - var expr_6 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2)).MakeGenericMethod(typeof(int)), expr_2, expr_3); - var expr_7 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2)).MakeGenericMethod(typeof(int)), expr_6, expr_5); - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(int)), expr_7); + var expr_6 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_2, expr_3); + var expr_7 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_6, expr_5); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_7); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ListWithSpread.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ListWithSpread.verified.txt index 9652f48..bfeedca 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ListWithSpread.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_ListWithSpread.verified.txt @@ -17,9 +17,9 @@ namespace ExpressiveSharp.Generated var expr_3 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Items")); // Items var expr_4 = global::System.Linq.Expressions.Expression.Constant(2, typeof(int)); // 2 var expr_5 = global::System.Linq.Expressions.Expression.NewArrayInit(typeof(int), expr_4); - var expr_6 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2)).MakeGenericMethod(typeof(int)), expr_2, expr_3); - var expr_7 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2)).MakeGenericMethod(typeof(int)), expr_6, expr_5); - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToList" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(int)), expr_7); + var expr_6 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_2, expr_3); + var expr_7 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_6, expr_5); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToList" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_7); return global::System.Linq.Expressions.Expression.Lambda>>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_MultipleSpreads.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_MultipleSpreads.verified.txt index c528328..12ccb67 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_MultipleSpreads.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_MultipleSpreads.verified.txt @@ -14,8 +14,8 @@ namespace ExpressiveSharp.Generated var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this"); var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Items")); // Items var expr_2 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Others")); // Others - var expr_3 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2)).MakeGenericMethod(typeof(int)), expr_1, expr_2); - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(int)), expr_3); + var expr_3 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Concat" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_1, expr_2); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_3); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_SpreadOnly.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_SpreadOnly.verified.txt index 3c87ebc..503f041 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_SpreadOnly.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/CollectionExpressionTests.CollectionExpression_SpreadOnly.verified.txt @@ -13,7 +13,7 @@ namespace ExpressiveSharp.Generated { var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this"); var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Items")); // Items - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(int)), expr_1); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "ToArray" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(int)), expr_1); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_WithFullObject.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_WithFullObject.verified.txt index 908f85c..e236d48 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_WithFullObject.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ConstructorTests.ProjectableConstructor_WithFullObject.verified.txt @@ -28,7 +28,7 @@ namespace ExpressiveSharp.Generated var expr_7 = global::System.Linq.Expressions.Expression.Property(p_customer, typeof(global::Foo.Customer).GetProperty("IsActive")); // customer.IsActive var expr_10 = global::System.Linq.Expressions.Expression.Property(p_customer, typeof(global::Foo.Customer).GetProperty("Orders")); // customer.Orders var expr_9 = global::System.Linq.Expressions.Expression.Convert(expr_10, typeof(global::System.Collections.Generic.IEnumerable)); - var expr_8 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(global::Foo.Order)), new global::System.Linq.Expressions.Expression[] { expr_9 }); + var expr_8 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(global::Foo.Order)), new global::System.Linq.Expressions.Expression[] { expr_9 }); var expr_11 = global::System.Linq.Expressions.Expression.Bind(typeof(global::Foo.CustomerDto).GetProperty("Id"), expr_1); var expr_12 = global::System.Linq.Expressions.Expression.Bind(typeof(global::Foo.CustomerDto).GetProperty("FullName"), expr_2); var expr_13 = global::System.Linq.Expressions.Expression.Bind(typeof(global::Foo.CustomerDto).GetProperty("IsActive"), expr_7); diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.TypesInBodyGetsFullyQualified.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.TypesInBodyGetsFullyQualified.verified.txt index 864cbce..b31092f 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.TypesInBodyGetsFullyQualified.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/MethodTests.TypesInBodyGetsFullyQualified.verified.txt @@ -15,7 +15,7 @@ namespace ExpressiveSharp.Generated var expr_3 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Dees")); // Dees var expr_2 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(global::System.Collections.IEnumerable)); var expr_1 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "OfType" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(global::Foo.D)), new global::System.Linq.Expressions.Expression[] { expr_2 }); - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(global::Foo.D)), new global::System.Linq.Expressions.Expression[] { expr_1 }); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(global::Foo.D)), new global::System.Linq.Expressions.Expression[] { expr_1 }); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/PropertyTests.ExpressivePropertyToNavigationalProperty.verified.txt b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/PropertyTests.ExpressivePropertyToNavigationalProperty.verified.txt index 3555308..8fecbbe 100644 --- a/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/PropertyTests.ExpressivePropertyToNavigationalProperty.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/PropertyTests.ExpressivePropertyToNavigationalProperty.verified.txt @@ -14,7 +14,7 @@ namespace ExpressiveSharp.Generated var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.C), "@this"); var expr_2 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.C).GetProperty("Dees")); // Dees var expr_1 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(global::System.Collections.Generic.IEnumerable)); - var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "First" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(global::Foo.D)), new global::System.Linq.Expressions.Expression[] { expr_1 }); + var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "First" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(global::Foo.D)), new global::System.Linq.Expressions.Expression[] { expr_1 }); return global::System.Linq.Expressions.Expression.Lambda>(expr_0, p__this); } } diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdateAsync_GeneratesInterceptor.verified.txt b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdateAsync_GeneratesInterceptor.verified.txt new file mode 100644 index 0000000..27037c7 --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdateAsync_GeneratesInterceptor.verified.txt @@ -0,0 +1,37 @@ +// +#nullable disable + +namespace ExpressiveSharp.Generated.Interceptors +{ + internal static class PolyfillInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(/* scrubbed */)] + internal static global::System.Threading.Tasks.Task __Polyfill_ExecuteUpdateAsync_0( + this global::ExpressiveSharp.IRewritableQueryable source, + global::System.Func, global::TestNs.SetPropertyCalls> _, + global::System.Threading.CancellationToken cancellationToken) + { + // Source: s => s.SetProperty(o => o.Tag, "updated") + var i0_p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.SetPropertyCalls), "s"); + var p_o_1 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Tag + var i0_expr_2 = global::System.Linq.Expressions.Expression.Property(p_o_1, typeof(global::TestNs.Order).GetProperty("Tag")); // o.Tag + var i0_expr_3 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_2, p_o_1); + var i0_expr_4 = global::System.Linq.Expressions.Expression.Constant("updated", typeof(string)); // "updated" + var i0_expr_0 = global::System.Linq.Expressions.Expression.Call(i0_p_s, global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::TestNs.SetPropertyCalls).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance), m => m.Name == "SetProperty" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(string)), new global::System.Linq.Expressions.Expression[] { i0_expr_3, i0_expr_4 }); + var __lambda = global::System.Linq.Expressions.Expression.Lambda, global::TestNs.SetPropertyCalls>>(i0_expr_0, i0_p_s); + return global::TestNs.MockRelationalExtensions.ExecuteUpdateAsync( + (global::System.Linq.IQueryable)source, + __lambda, + cancellationToken); + } + } +} + +namespace System.Runtime.CompilerServices +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} \ No newline at end of file diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_ConstantValue.verified.txt b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_ConstantValue.verified.txt new file mode 100644 index 0000000..8d41602 --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_ConstantValue.verified.txt @@ -0,0 +1,35 @@ +// +#nullable disable + +namespace ExpressiveSharp.Generated.Interceptors +{ + internal static class PolyfillInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(/* scrubbed */)] + internal static int __Polyfill_ExecuteUpdate_0( + this global::ExpressiveSharp.IRewritableQueryable source, + global::System.Func, global::TestNs.SetPropertyCalls> _) + { + // Source: s => s.SetProperty(o => o.Tag, "updated") + var i0_p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.SetPropertyCalls), "s"); + var p_o_1 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Tag + var i0_expr_2 = global::System.Linq.Expressions.Expression.Property(p_o_1, typeof(global::TestNs.Order).GetProperty("Tag")); // o.Tag + var i0_expr_3 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_2, p_o_1); + var i0_expr_4 = global::System.Linq.Expressions.Expression.Constant("updated", typeof(string)); // "updated" + var i0_expr_0 = global::System.Linq.Expressions.Expression.Call(i0_p_s, global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::TestNs.SetPropertyCalls).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance), m => m.Name == "SetProperty" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(string)), new global::System.Linq.Expressions.Expression[] { i0_expr_3, i0_expr_4 }); + var __lambda = global::System.Linq.Expressions.Expression.Lambda, global::TestNs.SetPropertyCalls>>(i0_expr_0, i0_p_s); + return global::TestNs.MockRelationalExtensions.ExecuteUpdate( + (global::System.Linq.IQueryable)source, + __lambda); + } + } +} + +namespace System.Runtime.CompilerServices +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} \ No newline at end of file diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithNullConditional.verified.txt b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithNullConditional.verified.txt new file mode 100644 index 0000000..e21e37a --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithNullConditional.verified.txt @@ -0,0 +1,44 @@ +// +#nullable disable + +namespace ExpressiveSharp.Generated.Interceptors +{ + internal static class PolyfillInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(/* scrubbed */)] + internal static int __Polyfill_ExecuteUpdate_0( + this global::ExpressiveSharp.IRewritableQueryable source, + global::System.Func, global::TestNs.SetPropertyCalls> _) + { + // Source: s => s.SetProperty(o => o.Tag, o => o.Customer?.Name ?? "none") + var i0_p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.SetPropertyCalls), "s"); + var p_o_1 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Tag + var i0_expr_2 = global::System.Linq.Expressions.Expression.Property(p_o_1, typeof(global::TestNs.Order).GetProperty("Tag")); // o.Tag + var i0_expr_3 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_2, p_o_1); + var p_o_4 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Customer?.Name ?? "none" + var i0_expr_6 = global::System.Linq.Expressions.Expression.Property(p_o_4, typeof(global::TestNs.Order).GetProperty("Customer")); // o.Customer + var i0_expr_7 = global::System.Linq.Expressions.Expression.Property(i0_expr_6, typeof(global::TestNs.Customer).GetProperty("Name")); // .Name + var i0_expr_9 = global::System.Linq.Expressions.Expression.Constant(null, typeof(global::TestNs.Customer)); + var i0_expr_10 = global::System.Linq.Expressions.Expression.NotEqual(i0_expr_6, i0_expr_9); + var i0_expr_11 = global::System.Linq.Expressions.Expression.Default(typeof(string)); + var i0_expr_8 = global::System.Linq.Expressions.Expression.Condition(i0_expr_10, i0_expr_7, i0_expr_11, typeof(string)); + var i0_expr_12 = global::System.Linq.Expressions.Expression.Constant("none", typeof(string)); // "none" + var i0_expr_5 = global::System.Linq.Expressions.Expression.Coalesce(i0_expr_8, i0_expr_12); + var i0_expr_13 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_5, p_o_4); + var i0_expr_0 = global::System.Linq.Expressions.Expression.Call(i0_p_s, global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::TestNs.SetPropertyCalls).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance), m => m.Name == "SetProperty" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(string)), new global::System.Linq.Expressions.Expression[] { i0_expr_3, i0_expr_13 }); + var __lambda = global::System.Linq.Expressions.Expression.Lambda, global::TestNs.SetPropertyCalls>>(i0_expr_0, i0_p_s); + return global::TestNs.MockRelationalExtensions.ExecuteUpdate( + (global::System.Linq.IQueryable)source, + __lambda); + } + } +} + +namespace System.Runtime.CompilerServices +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} \ No newline at end of file diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithSwitchExpression.verified.txt b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithSwitchExpression.verified.txt new file mode 100644 index 0000000..069e31f --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.ExecuteUpdate_SetProperty_WithSwitchExpression.verified.txt @@ -0,0 +1,46 @@ +// +#nullable disable + +namespace ExpressiveSharp.Generated.Interceptors +{ + internal static class PolyfillInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(/* scrubbed */)] + internal static int __Polyfill_ExecuteUpdate_0( + this global::ExpressiveSharp.IRewritableQueryable source, + global::System.Func, global::TestNs.SetPropertyCalls> _) + { + // Source: s => s.SetProperty(o => o.Tag, o => o.Amount switch { > 100 => "high", > 50 => "medium", _ => "low" }) + var i0_p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.SetPropertyCalls), "s"); + var p_o_1 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Tag + var i0_expr_2 = global::System.Linq.Expressions.Expression.Property(p_o_1, typeof(global::TestNs.Order).GetProperty("Tag")); // o.Tag + var i0_expr_3 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_2, p_o_1); + var p_o_4 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::TestNs.Order), "o"); // o => o.Amount switch { ... + var i0_expr_5 = global::System.Linq.Expressions.Expression.Property(p_o_4, typeof(global::TestNs.Order).GetProperty("Amount")); // o.Amount + var i0_expr_6 = global::System.Linq.Expressions.Expression.Constant("low", typeof(string)); // "low" + var i0_expr_8 = global::System.Linq.Expressions.Expression.Constant(50, typeof(int)); // 50 + var i0_expr_7 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, i0_expr_5, i0_expr_8); + var i0_expr_9 = global::System.Linq.Expressions.Expression.Constant("medium", typeof(string)); // "medium" + var i0_expr_10 = global::System.Linq.Expressions.Expression.Condition(i0_expr_7, i0_expr_9, i0_expr_6, typeof(string)); + var i0_expr_12 = global::System.Linq.Expressions.Expression.Constant(100, typeof(int)); // 100 + var i0_expr_11 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, i0_expr_5, i0_expr_12); + var i0_expr_13 = global::System.Linq.Expressions.Expression.Constant("high", typeof(string)); // "high" + var i0_expr_14 = global::System.Linq.Expressions.Expression.Condition(i0_expr_11, i0_expr_13, i0_expr_10, typeof(string)); + var i0_expr_15 = global::System.Linq.Expressions.Expression.Lambda>(i0_expr_14, p_o_4); + var i0_expr_0 = global::System.Linq.Expressions.Expression.Call(i0_p_s, global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::TestNs.SetPropertyCalls).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance), m => m.Name == "SetProperty" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[1].ParameterType.IsGenericType)).MakeGenericMethod(typeof(string)), new global::System.Linq.Expressions.Expression[] { i0_expr_3, i0_expr_15 }); + var __lambda = global::System.Linq.Expressions.Expression.Lambda, global::TestNs.SetPropertyCalls>>(i0_expr_0, i0_p_s); + return global::TestNs.MockRelationalExtensions.ExecuteUpdate( + (global::System.Linq.IQueryable)source, + __lambda); + } + } +} + +namespace System.Runtime.CompilerServices +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} \ No newline at end of file diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.cs b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.cs new file mode 100644 index 0000000..d0588b7 --- /dev/null +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/ExecuteUpdateTests.cs @@ -0,0 +1,188 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using VerifyMSTest; +using ExpressiveSharp.Generator.Tests.Infrastructure; + +namespace ExpressiveSharp.Generator.Tests.PolyfillInterceptorGenerator; + +[TestClass] +public class ExecuteUpdateTests : GeneratorTestBase +{ + /// + /// Shared mock types that simulate EF Core's SetPropertyCalls and RelationalQueryableExtensions. + /// Used by all tests since the generator tests run against a minimal Roslyn compilation. + /// + private const string MockTypes = + """ + using System; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using ExpressiveSharp; + using ExpressiveSharp.Extensions; + + namespace TestNs + { + // Mock EF Core's SetPropertyCalls + class SetPropertyCalls + { + public SetPropertyCalls SetProperty( + Func propertyExpression, + TProperty value) => this; + + public SetPropertyCalls SetProperty( + Func propertyExpression, + Func valueExpression) => this; + } + + // Mock EF Core's RelationalQueryableExtensions + static class MockRelationalExtensions + { + public static int ExecuteUpdate( + IQueryable source, + Expression, SetPropertyCalls>> setPropertyCalls) + => 0; + + public static Task ExecuteUpdateAsync( + IQueryable source, + Expression, SetPropertyCalls>> setPropertyCalls, + CancellationToken cancellationToken = default) + => Task.FromResult(0); + } + + // IRewritableQueryable stubs (matching the real pattern) + static class Stubs + { + [PolyfillTarget(typeof(MockRelationalExtensions))] + public static int ExecuteUpdate( + this IRewritableQueryable source, + Func, SetPropertyCalls> setPropertyCalls) + => throw new System.Diagnostics.UnreachableException(); + + [PolyfillTarget(typeof(MockRelationalExtensions))] + public static Task ExecuteUpdateAsync( + this IRewritableQueryable source, + Func, SetPropertyCalls> setPropertyCalls, + CancellationToken cancellationToken = default) + => throw new System.Diagnostics.UnreachableException(); + } + """; + + [TestMethod] + public Task ExecuteUpdate_SetProperty_ConstantValue() + { + var source = MockTypes + + """ + class Order { public string Tag { get; set; } } + class TestClass + { + public void Run(IQueryable query) + { + query.AsExpressive() + .ExecuteUpdate(s => s.SetProperty(o => o.Tag, "updated")); + } + } + } + """; + var result = RunPolyfillInterceptorGenerator(CreateCompilation(source)); + + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].GetText().ToString()); + } + + /// + /// Proves new capability: null-conditional operator inside SetProperty value expression. + /// This is impossible in normal C# expression trees. + /// + [TestMethod] + public Task ExecuteUpdate_SetProperty_WithNullConditional() + { + var source = MockTypes + + """ + class Customer { public string? Name { get; set; } } + class Order + { + public string Tag { get; set; } + public Customer? Customer { get; set; } + } + class TestClass + { + public void Run(IQueryable query) + { + query.AsExpressive() + .ExecuteUpdate(s => s.SetProperty( + o => o.Tag, + o => o.Customer?.Name ?? "none")); + } + } + } + """; + var result = RunPolyfillInterceptorGenerator(CreateCompilation(source)); + + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].GetText().ToString()); + } + + /// + /// Proves new capability: switch expression inside SetProperty value expression. + /// This is impossible in normal C# expression trees. + /// + [TestMethod] + public Task ExecuteUpdate_SetProperty_WithSwitchExpression() + { + var source = MockTypes + + """ + class Order + { + public string Tag { get; set; } + public int Amount { get; set; } + } + class TestClass + { + public void Run(IQueryable query) + { + query.AsExpressive() + .ExecuteUpdate(s => s.SetProperty( + o => o.Tag, + o => o.Amount switch + { + > 100 => "high", + > 50 => "medium", + _ => "low" + })); + } + } + } + """; + var result = RunPolyfillInterceptorGenerator(CreateCompilation(source)); + + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].GetText().ToString()); + } + + [TestMethod] + public Task ExecuteUpdateAsync_GeneratesInterceptor() + { + var source = MockTypes + + """ + class Order { public string Tag { get; set; } } + class TestClass + { + public async Task Run(IQueryable query) + { + await query.AsExpressive() + .ExecuteUpdateAsync(s => s.SetProperty(o => o.Tag, "updated")); + } + } + } + """; + var result = RunPolyfillInterceptorGenerator(CreateCompilation(source)); + + Assert.AreEqual(1, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees[0].GetText().ToString()); + } +} diff --git a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/JoinTests.GroupJoin_AnonymousResultSelector_GeneratesGenericInterceptor.verified.txt b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/JoinTests.GroupJoin_AnonymousResultSelector_GeneratesGenericInterceptor.verified.txt index a3215cc..33db805 100644 --- a/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/JoinTests.GroupJoin_AnonymousResultSelector_GeneratesGenericInterceptor.verified.txt +++ b/tests/ExpressiveSharp.Generator.Tests/PolyfillInterceptorGenerator/JoinTests.GroupJoin_AnonymousResultSelector_GeneratesGenericInterceptor.verified.txt @@ -25,7 +25,7 @@ namespace ExpressiveSharp.Generated.Interceptors var i0c_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "o"); var i0c_p_cs = global::System.Linq.Expressions.Expression.Parameter(typeof(global::System.Collections.Generic.IEnumerable), "cs"); var i0c_expr_1 = global::System.Linq.Expressions.Expression.Property(i0c_p_o, typeof(T0).GetProperty("CustomerId")); // o.CustomerId - var i0c_expr_2 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1)).MakeGenericMethod(typeof(T1)), new global::System.Linq.Expressions.Expression[] { i0c_p_cs }); // cs.Count() + var i0c_expr_2 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Count" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType)).MakeGenericMethod(typeof(T1)), new global::System.Linq.Expressions.Expression[] { i0c_p_cs }); // cs.Count() var i0c_expr_3 = typeof(T3).GetConstructors()[0]; var i0c_expr_0 = global::System.Linq.Expressions.Expression.New(i0c_expr_3, new global::System.Linq.Expressions.Expression[] { i0c_expr_1, i0c_expr_2 }, new global::System.Reflection.MemberInfo[] { typeof(T3).GetProperty("CustomerId"), typeof(T3).GetProperty("Count") }); var __lambda3 = global::System.Linq.Expressions.Expression.Lambda, T3>>(i0c_expr_0, i0c_p_o, i0c_p_cs); From d8a8a1ffbec2eab14fcab914362e0b02b13bf2c2 Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 1 Apr 2026 01:50:35 +0000 Subject: [PATCH 3/4] Use StringBuilder for paramChecks in ReflectionFieldCache Address CodeQL string-concatenation-in-loop suggestion. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Emitter/ReflectionFieldCache.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs b/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs index b607dc1..b321e04 100644 --- a/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs +++ b/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs @@ -68,15 +68,16 @@ public string EnsureMethodInfo(IMethodSymbol method) // Disambiguate overloads that share name, generic arity, and parameter count // (e.g., SetProperty

(Func, P) vs SetProperty

(Func, Func)) // by checking whether each parameter is a generic type or a type parameter. - var paramChecks = ""; + var paramChecksBuilder = new System.Text.StringBuilder(); for (int i = 0; i < originalDef.Parameters.Length; i++) { var paramType = originalDef.Parameters[i].Type; if (paramType is ITypeParameterSymbol) - paramChecks += $" && !m.GetParameters()[{i}].ParameterType.IsGenericType"; + paramChecksBuilder.Append($" && !m.GetParameters()[{i}].ParameterType.IsGenericType"); else if (paramType is INamedTypeSymbol { IsGenericType: true }) - paramChecks += $" && m.GetParameters()[{i}].ParameterType.IsGenericType"; + paramChecksBuilder.Append($" && m.GetParameters()[{i}].ParameterType.IsGenericType"); } + var paramChecks = paramChecksBuilder.ToString(); return $"global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof({typeFqn}).GetMethods({flags}), m => m.Name == \"{method.Name}\" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == {genericArity} && m.GetParameters().Length == {paramCount}{paramChecks})).MakeGenericMethod({typeArgs})"; } From ce170dfc9596ebd093e9ac2780507ade6f6c9c2e Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 1 Apr 2026 02:00:17 +0000 Subject: [PATCH 4/4] Document ExecuteUpdate support in EF Core guides 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) --- docs/guide/ef-core-integration.md | 34 +++++++++++++++++++++++++++++- docs/guide/rewritable-queryable.md | 30 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/guide/ef-core-integration.md b/docs/guide/ef-core-integration.md index 6369f5d..cb34429 100644 --- a/docs/guide/ef-core-integration.md +++ b/docs/guide/ef-core-integration.md @@ -152,6 +152,38 @@ Supported chain-preserving operations: - `IgnoreQueryFilters()`, `IgnoreAutoIncludes()` - `TagWith(tag)`, `TagWithCallSite()` +## Bulk Updates with ExecuteUpdate + +With the `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package, you can use modern C# syntax inside `ExecuteUpdate` / `ExecuteUpdateAsync`: + +```csharp +// Requires: .UseExpressives(o => o.UseRelationalExtensions()) +ctx.Orders + .ExecuteUpdate(s => s + .SetProperty(o => o.Tag, o => o.Price switch + { + >= 100 => "Premium", + >= 50 => "Standard", + _ => "Budget" + })); + +// Async variant +await ctx.Orders + .ExecuteUpdateAsync(s => s.SetProperty( + o => o.Tag, + o => o.Customer?.Name ?? "Unknown")); +``` + +Switch expressions and null-conditional operators inside `SetProperty` value lambdas are normally rejected by the C# compiler in expression tree contexts. The source generator converts them to `CASE WHEN` and `COALESCE` SQL expressions. + +::: info +`ExecuteDelete` works on `IRewritableQueryable` / `ExpressiveDbSet` without any additional setup — it has no lambda parameter, so no interception is needed. +::: + +::: warning +This feature is available on EF Core 8 and 9. EF Core 10 changed the `ExecuteUpdate` API to use `Action>`, which natively supports modern C# syntax in the outer lambda. For inner `SetProperty` value expressions on EF Core 10, use `ExpressionPolyfill.Create()`. +::: + ## Plugin Architecture `UseExpressives()` accepts an optional configuration callback for registering plugins: @@ -193,7 +225,7 @@ The built-in `RelationalExtensions` package (for window functions) uses this plu |---------|-------------| | [`ExpressiveSharp`](https://www.nuget.org/packages/ExpressiveSharp/) | Core runtime -- `[Expressive]` attribute, source generator, expression expansion, transformers | | [`ExpressiveSharp.EntityFrameworkCore`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore/) | EF Core integration -- `UseExpressives()`, `ExpressiveDbSet`, Include/ThenInclude, async methods, analyzers and code fixes | -| [`ExpressiveSharp.EntityFrameworkCore.RelationalExtensions`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/) | SQL window functions -- ROW_NUMBER, RANK, DENSE_RANK, NTILE (experimental) | +| [`ExpressiveSharp.EntityFrameworkCore.RelationalExtensions`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/) | Relational extensions -- `ExecuteUpdate`/`ExecuteUpdateAsync` with modern syntax, SQL window functions (ROW_NUMBER, RANK, DENSE_RANK, NTILE) | ::: info The `ExpressiveSharp.EntityFrameworkCore` package bundles Roslyn analyzers and code fixes from `ExpressiveSharp.EntityFrameworkCore.CodeFixers`. These provide compile-time diagnostics and IDE quick-fix actions for common issues like missing `[Expressive]` attributes. diff --git a/docs/guide/rewritable-queryable.md b/docs/guide/rewritable-queryable.md index 9a02967..ea8a1cc 100644 --- a/docs/guide/rewritable-queryable.md +++ b/docs/guide/rewritable-queryable.md @@ -140,6 +140,36 @@ var orders = ctx.Set() .ToList(); ``` +## EF Core: Bulk Updates with ExecuteUpdate + +::: info +Requires the `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package and `.UseExpressives(o => o.UseRelationalExtensions())` configuration. Available on EF Core 8 and 9. On EF Core 10+, `ExecuteUpdate` natively accepts delegates — use `ExpressionPolyfill.Create()` for modern syntax in individual `SetProperty` value expressions. +::: + +`ExecuteUpdate` and `ExecuteUpdateAsync` are supported on `IRewritableQueryable`, enabling modern C# syntax inside `SetProperty` value expressions — which is normally impossible in expression trees: + +```csharp +ctx.ExpressiveSet() + .ExecuteUpdate(s => s + .SetProperty(p => p.Tag, p => p.Price switch + { + > 100 => "premium", + > 50 => "standard", + _ => "budget" + }) + .SetProperty(p => p.Category, p => p.Category ?? "Uncategorized")); +``` + +This generates a single SQL `UPDATE` with `CASE WHEN` and `COALESCE` expressions — no entity loading required. + +`ExecuteDelete` works out of the box on `IRewritableQueryable` without any stubs (it has no lambda parameter): + +```csharp +ctx.ExpressiveSet() + .Where(p => p.Price switch { < 10 => true, _ => false }) + .ExecuteDelete(); +``` + ## IAsyncEnumerable Support `IRewritableQueryable` supports `AsAsyncEnumerable()` for streaming results: