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/ 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: 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..b321e04 100644 --- a/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs +++ b/src/ExpressiveSharp.Generator/Emitter/ReflectionFieldCache.cs @@ -64,7 +64,22 @@ 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 paramChecksBuilder = new System.Text.StringBuilder(); + for (int i = 0; i < originalDef.Parameters.Length; i++) + { + var paramType = originalDef.Parameters[i].Type; + if (paramType is ITypeParameterSymbol) + paramChecksBuilder.Append($" && !m.GetParameters()[{i}].ParameterType.IsGenericType"); + else if (paramType is INamedTypeSymbol { IsGenericType: true }) + 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})"; } 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);