Skip to content

Commit 91727f8

Browse files
authored
Improve section on dynamic query generation (#4270)
See dotnet/efcore#30338 (comment)
1 parent e30b2a4 commit 91727f8

File tree

2 files changed

+86
-45
lines changed

2 files changed

+86
-45
lines changed

entity-framework/core/performance/advanced-performance-topics.md

+21-10
Original file line numberDiff line numberDiff line change
@@ -153,26 +153,37 @@ Note that there is no need to parameterize each and every query: it's perfectly
153153

154154
In some situations, it is necessary to dynamically construct LINQ queries rather than specifying them outright in source code. This can happen, for example, in a website which receives arbitrary query details from a client, with open-ended query operators (sorting, filtering, paging...). In principle, if done correctly, dynamically-constructed queries can be just as efficient as regular ones (although it's not possible to use the compiled query optimization with dynamic queries). In practice, however, they are frequently the source of performance issues, since it's easy to accidentally produce expression trees with shapes that differ every time.
155155

156-
The following example uses two techniques to dynamically construct a query; we add a `Where` operator to the query only if the given parameter is not null. Note that this isn't a good use case for dynamically constructing a query - but we're using it for simplicity:
156+
The following example uses three techniques to construct a query's `Where` lambda expression:
157157

158-
### [With constant](#tab/with-constant)
158+
1. **Expression API with constant**: Dynamically build the expression with the Expression API, using a constant node. This is a frequent mistake when dynamically building expression trees, and causes EF to recompile the query each time it's invoked with a different constant value (it also usually causes plan cache pollution at the database server).
159+
2. **Expression API with parameter**: A better version, which substitutes the constant with a parameter. This ensures that the query is only compiled once regardless of the value provided, and the same (parameterized) SQL is generated.
160+
3. **Simple with parameter**: A version which doesn't use the Expression API, for comparison, which creates the same tree as the method above but is much simpler. In many cases, it's possible to dynamically build your expression tree without resorting to the Expression API, which is easy to get wrong.
159161

160-
[!code-csharp[Main](../../../samples/core/Benchmarks/DynamicallyConstructedQueries.cs?name=WithConstant&highlight=14-24)]
162+
; we add a `Where` operator to the query only if the given parameter is not null. Note that this isn't a good use case for dynamically constructing a query - but we're using it for simplicity:
161163

162-
### [With parameter](#tab/with-parameter)
164+
### [Expression API with constant](#tab/expression-api-with-constant)
163165

164-
[!code-csharp[Main](../../../samples/core/Benchmarks/DynamicallyConstructedQueries.cs?name=WithParameter&highlight=14)]
166+
[!code-csharp[Main](../../../samples/core/Benchmarks/DynamicallyConstructedQueries.cs?name=ExpressionApiWithConstant&highlight=11-18)]
167+
168+
### [Expression API with parameter](#tab/expression-api-with-parameter)
169+
170+
[!code-csharp[Main](../../../samples/core/Benchmarks/DynamicallyConstructedQueries.cs?name=ExpressionApiWithParameter&highlight=11-26)]
171+
172+
### [Simple with parameter](#tab/simple-with-parameter)
173+
174+
[!code-csharp[Main](../../../samples/core/Benchmarks/DynamicallyConstructedQueries.cs?name=SimpleWithParameter&highlight=12)]
165175

166176
***
167177

168178
Benchmarking these two techniques gives the following results:
169179

170-
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
171-
|-------------- |-----------:|---------:|----------:|--------:|-------:|------:|----------:|
172-
| WithConstant | 1,096.7 us | 12.54 us | 11.12 us | 13.6719 | 1.9531 | - | 83.91 KB |
173-
| WithParameter | 570.8 us | 42.43 us | 124.43 us | 5.8594 | - | - | 37.16 KB |
180+
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
181+
|--------------------------- |-----------:|---------:|---------:|--------:|-------:|----------:|
182+
| ExpressionApiWithConstant | 1,665.8 us | 56.99 us | 163.5 us | 15.6250 | - | 109.92 KB |
183+
| ExpressionApiWithParameter | 757.1 us | 35.14 us | 103.6 us | 12.6953 | 0.9766 | 54.95 KB |
184+
| SimpleWithParameter | 760.3 us | 37.99 us | 112.0 us | 12.6953 | - | 55.03 KB |
174185

175-
Even if the sub-millisecond difference seems small, keep in mind that the constant version continuously pollutes the cache and causes other queries to be re-compiled, slowing them down as well.
186+
Even if the sub-millisecond difference seems small, keep in mind that the constant version continuously pollutes the cache and causes other queries to be re-compiled, slowing them down as well and having a general negative impact on your overall performance. It's highly recommended to avoid constant query recompilation.
176187

177188
> [!NOTE]
178189
> Avoid constructing queries with the expression tree API unless you really need to. Aside from the API's complexity, it's very easy to inadvertently cause significant performance issues when using them.

samples/core/Benchmarks/DynamicallyConstructedQueries.cs

+65-35
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
public class DynamicallyConstructedQueries
1111
{
1212
private int _blogNumber;
13+
private bool _addWhereClause = true;
1314

1415
[GlobalSetup]
1516
public static void GlobalSetup()
@@ -19,57 +20,86 @@ public static void GlobalSetup()
1920
context.Database.EnsureCreated();
2021
}
2122

22-
#region WithConstant
23+
#region ExpressionApiWithConstant
2324
[Benchmark]
24-
public int WithConstant()
25+
public int ExpressionApiWithConstant()
2526
{
26-
return GetBlogCount("blog" + Interlocked.Increment(ref _blogNumber));
27+
var url = "blog" + Interlocked.Increment(ref _blogNumber);
28+
using var context = new BloggingContext();
29+
30+
IQueryable<Blog> query = context.Blogs;
2731

28-
static int GetBlogCount(string url)
32+
if (_addWhereClause)
2933
{
30-
using var context = new BloggingContext();
31-
32-
IQueryable<Blog> blogs = context.Blogs;
33-
34-
if (url is not null)
35-
{
36-
var blogParam = Expression.Parameter(typeof(Blog), "b");
37-
var whereLambda = Expression.Lambda<Func<Blog, bool>>(
38-
Expression.Equal(
39-
Expression.MakeMemberAccess(
40-
blogParam,
41-
typeof(Blog).GetMember(nameof(Blog.Url)).Single()
42-
),
43-
Expression.Constant(url)),
44-
blogParam);
45-
46-
blogs = blogs.Where(whereLambda);
47-
}
48-
49-
return blogs.Count();
34+
var blogParam = Expression.Parameter(typeof(Blog), "b");
35+
var whereLambda = Expression.Lambda<Func<Blog, bool>>(
36+
Expression.Equal(
37+
Expression.MakeMemberAccess(
38+
blogParam,
39+
typeof(Blog).GetMember(nameof(Blog.Url)).Single()),
40+
Expression.Constant(url)),
41+
blogParam);
42+
43+
query = query.Where(whereLambda);
5044
}
45+
46+
return query.Count();
5147
}
5248
#endregion
5349

54-
#region WithParameter
50+
#region ExpressionApiWithParameter
5551
[Benchmark]
56-
public int WithParameter()
52+
public int ExpressionApiWithParameter()
5753
{
58-
return GetBlogCount("blog" + Interlocked.Increment(ref _blogNumber));
54+
var url = "blog" + Interlocked.Increment(ref _blogNumber);
55+
using var context = new BloggingContext();
5956

60-
int GetBlogCount(string url)
57+
IQueryable<Blog> query = context.Blogs;
58+
59+
if (_addWhereClause)
6160
{
62-
using var context = new BloggingContext();
61+
var blogParam = Expression.Parameter(typeof(Blog), "b");
62+
63+
// This creates a lambda expression whose body is identical to the url captured closure variable in the non-dynamic query:
64+
// blogs.Where(b => b.Url == url)
65+
// This dynamically creates an expression node which EF can properly recognize and parameterize in the database query.
66+
// We then extract that body and use it in our dynamically-constructed query.
67+
Expression<Func<string>> urlParameterLambda = () => url;
68+
var urlParamExpression = urlParameterLambda.Body;
69+
70+
var whereLambda = Expression.Lambda<Func<Blog, bool>>(
71+
Expression.Equal(
72+
Expression.MakeMemberAccess(
73+
blogParam,
74+
typeof(Blog).GetMember(nameof(Blog.Url)).Single()),
75+
urlParamExpression),
76+
blogParam);
77+
78+
query = query.Where(whereLambda);
79+
}
80+
81+
return query.Count();
82+
}
83+
#endregion
84+
85+
#region SimpleWithParameter
86+
[Benchmark]
87+
public int SimpleWithParameter()
88+
{
89+
var url = "blog" + Interlocked.Increment(ref _blogNumber);
6390

64-
IQueryable<Blog> blogs = context.Blogs;
91+
using var context = new BloggingContext();
92+
93+
IQueryable<Blog> query = context.Blogs;
6594

66-
if (url is not null)
67-
{
68-
blogs = blogs.Where(b => b.Url == url);
69-
}
95+
if (_addWhereClause)
96+
{
97+
Expression<Func<Blog, bool>> whereLambda = b => b.Url == url;
7098

71-
return blogs.Count();
99+
query = query.Where(whereLambda);
72100
}
101+
102+
return query.Count();
73103
}
74104
#endregion
75105

0 commit comments

Comments
 (0)