From 6c935188ee044a9024b0c40f59fe8a446f2c9149 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 1 Nov 2026 17:37:26 +0100 Subject: [PATCH] Expression Trees: net10.0, Func vs Expression filter, interpreter, compile benchmarks Target net10.0 and update EF Core Sqlite, xunit, Moq and test packages. Rename the multiply node in DynamicMethodGenerator to product. Add FilterWithFunc and FilterWithExpression, InterpretExpressionTree, two tests, and a Benchmarks project that measures compiling against cached and interpreted delegates. --- .../Benchmarks/Benchmarks.csproj | 18 +++++++++ .../Benchmarks/CompileBenchmarks.cs | 38 +++++++++++++++++++ .../Benchmarks/Program.cs | 3 ++ .../ExpressionTreesInCSharp.sln | 34 +++++++++++++++++ .../DynamicMethods/DynamicMethodGenerator.cs | 4 +- .../EntityFramework/EntityFrameworkSqlite.cs | 13 ++++++- .../ExpressionTrees.cs | 5 +++ .../ExpressionTreesInCSharp.csproj | 4 +- .../Tests/EntityFrameworkTests.cs | 12 ++++++ .../Tests/ExpressionTreeTests.cs | 12 +++++- .../Tests/Tests.csproj | 12 +++--- 11 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Benchmarks.csproj create mode 100644 csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/CompileBenchmarks.cs create mode 100644 csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Program.cs diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Benchmarks.csproj b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Benchmarks.csproj new file mode 100644 index 0000000000..d3c17d10a2 --- /dev/null +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Benchmarks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/CompileBenchmarks.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/CompileBenchmarks.cs new file mode 100644 index 0000000000..c7112007ff --- /dev/null +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/CompileBenchmarks.cs @@ -0,0 +1,38 @@ +using BenchmarkDotNet.Attributes; +using ExpressionTreesInCSharp; +using System.Linq.Expressions; + +namespace Benchmarks; + +[MemoryDiagnoser] +public class CompileBenchmarks +{ + private readonly Expression> _sumExpressionTree = + ExpressionTrees.CreateExpressionTreeFromLambdaExpression(); + + private readonly Func _lambda = ExpressionTrees.CreateLambdaExpression(); + private Func _compiled = default!; + private Func _interpreted = default!; + + [GlobalSetup] + public void Setup() + { + _compiled = _sumExpressionTree.Compile(); + _interpreted = _sumExpressionTree.Compile(preferInterpretation: true); + } + + [Benchmark] + public int InvokeLambda() => _lambda(1, 2); + + [Benchmark] + public int InvokeCachedCompiledDelegate() => _compiled(1, 2); + + [Benchmark] + public int InvokeCachedInterpretedDelegate() => _interpreted(1, 2); + + [Benchmark] + public int CompileAndInvoke() => _sumExpressionTree.Compile()(1, 2); + + [Benchmark] + public int InterpretAndInvoke() => _sumExpressionTree.Compile(preferInterpretation: true)(1, 2); +} diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Program.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Program.cs new file mode 100644 index 0000000000..c9a046727c --- /dev/null +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp.sln b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp.sln index e59a24efd4..e1a50157f9 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp.sln +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp.sln @@ -7,20 +7,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExpressionTreesInCSharp", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{1421730A-00C5-44F5-98F4-8E6623F7C3C5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{17A874D8-A920-4912-8E62-1B43E8176375}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|x64.Build.0 = Debug|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Debug|x86.Build.0 = Debug|Any CPU {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|Any CPU.Build.0 = Release|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|x64.ActiveCfg = Release|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|x64.Build.0 = Release|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|x86.ActiveCfg = Release|Any CPU + {DD0C1186-0518-4FA7-89A9-5101A346045C}.Release|x86.Build.0 = Release|Any CPU {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|x64.Build.0 = Debug|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Debug|x86.Build.0 = Debug|Any CPU {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|Any CPU.Build.0 = Release|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|x64.ActiveCfg = Release|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|x64.Build.0 = Release|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|x86.ActiveCfg = Release|Any CPU + {1421730A-00C5-44F5-98F4-8E6623F7C3C5}.Release|x86.Build.0 = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|x64.ActiveCfg = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|x64.Build.0 = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|x86.ActiveCfg = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Debug|x86.Build.0 = Debug|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|Any CPU.Build.0 = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|x64.ActiveCfg = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|x64.Build.0 = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|x86.ActiveCfg = Release|Any CPU + {17A874D8-A920-4912-8E62-1B43E8176375}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/DynamicMethods/DynamicMethodGenerator.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/DynamicMethods/DynamicMethodGenerator.cs index c32e1d7a27..0bb966a1e6 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/DynamicMethods/DynamicMethodGenerator.cs +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/DynamicMethods/DynamicMethodGenerator.cs @@ -13,8 +13,8 @@ public static Func GenerateExecutableFunctionFromMathStatement(st var right = Expression.Parameter(typeof(int), variables[1]); // Create the expression tree - var add = Expression.Multiply(left, right); - var lambda = Expression.Lambda(add, left, right); + var product = Expression.Multiply(left, right); + var lambda = Expression.Lambda(product, left, right); // Return the compiled method return (Func)lambda.Compile(); diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/EntityFramework/EntityFrameworkSqlite.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/EntityFramework/EntityFrameworkSqlite.cs index 29700f2ed5..62e9310e1f 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/EntityFramework/EntityFrameworkSqlite.cs +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/EntityFramework/EntityFrameworkSqlite.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; namespace ExpressionTreesInCSharp.EntityFramework; public class EntityFrameworkSqlite @@ -26,4 +27,14 @@ public string GetWhereQuery() return whereQuery; } -} \ No newline at end of file + + public IEnumerable FilterWithFunc(Func predicate) + { + return _dbContext.Users.Where(predicate); + } + + public IQueryable FilterWithExpression(Expression> predicate) + { + return _dbContext.Users.Where(predicate); + } +} diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTrees.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTrees.cs index c3c1ec5dad..4ecc9c1469 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTrees.cs +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTrees.cs @@ -33,4 +33,9 @@ public static Func CompileExpressionTreeToLambdaExpression(Expres return delegateRepresentingLambdaExpression; } + + public static Func InterpretExpressionTree(Expression> expressionTree) + { + return expressionTree.Compile(preferInterpretation: true); + } } diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTreesInCSharp.csproj b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTreesInCSharp.csproj index 554375c136..19b1557167 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTreesInCSharp.csproj +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/ExpressionTreesInCSharp/ExpressionTreesInCSharp.csproj @@ -2,13 +2,13 @@ Exe - net7.0 + net10.0 enable enable - + diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/EntityFrameworkTests.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/EntityFrameworkTests.cs index 9d62baf5de..2a45c4666c 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/EntityFrameworkTests.cs +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/EntityFrameworkTests.cs @@ -33,4 +33,16 @@ public void GivenAUserDbContext_WhenListedUsersWithFilters_ThenShouldUseCorrectQ Assert.Equal(expectedQuery, query); } + + [Fact] + public void GivenAFuncPredicate_WhenFiltering_ThenTheQueryLeavesEntityFramework() + { + var efSqlite = new EntityFrameworkSqlite(); + + var withFunc = efSqlite.FilterWithFunc(x => x.Age > 20); + var withExpression = efSqlite.FilterWithExpression(x => x.Age > 20); + + Assert.False(withFunc is IQueryable); + Assert.True(withExpression is IQueryable); + } } diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/ExpressionTreeTests.cs b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/ExpressionTreeTests.cs index fb7da83460..f666f39b31 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/ExpressionTreeTests.cs +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/ExpressionTreeTests.cs @@ -70,4 +70,14 @@ public void GivenAnExpressionTree_WhenCompiled_ThenShouldExecuteAndReturnResult( Assert.Equal(2, sum); } -} \ No newline at end of file + + [Fact] + public void GivenAnExpressionTree_WhenInterpreted_ThenShouldReturnTheSameResult() + { + var expression = ExpressionTrees.CreateExpressionTreeFromLambdaExpression(); + + var interpretedSum = ExpressionTrees.InterpretExpressionTree(expression); + + Assert.Equal(3, interpretedSum(1, 2)); + } +} diff --git a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/Tests.csproj b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/Tests.csproj index f7ba662f76..6d962eb27b 100644 --- a/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/Tests.csproj +++ b/csharp-advanced-topics/ExpressionTreesInCSharp/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -10,14 +10,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all