-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
PrecompiledQueryCodeGenerator.cs
1135 lines (995 loc) · 63.4 KB
/
PrecompiledQueryCodeGenerator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Runtime.ExceptionServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.EntityFrameworkCore.Query.Internal;
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class PrecompiledQueryCodeGenerator
{
private readonly QueryLocator _queryLocator;
private readonly CSharpToLinqTranslator _csharpToLinqTranslator;
private SyntaxGenerator _g = null!;
private IQueryCompiler _queryCompiler = null!;
private ExpressionTreeFuncletizer _funcletizer = null!;
private LinqToCSharpSyntaxTranslator _linqToCSharpTranslator = null!;
private LiftableConstantProcessor _liftableConstantProcessor = null!;
private Symbols _symbols;
private readonly HashSet<string> _namespaces = new();
private readonly HashSet<MethodDeclarationSyntax> _unsafeAccessors = new();
private readonly IndentedStringBuilder _code = new();
private const string InterceptorsNamespace = "Microsoft.EntityFrameworkCore.GeneratedInterceptors";
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public PrecompiledQueryCodeGenerator()
{
_queryLocator = new QueryLocator();
_csharpToLinqTranslator = new CSharpToLinqTranslator();
}
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IReadOnlyList<GeneratedInterceptorFile> GeneratePrecompiledQueries(
Compilation compilation,
SyntaxGenerator syntaxGenerator,
DbContext dbContext,
List<QueryPrecompilationError> precompilationErrors,
Assembly? additionalAssembly = null,
CancellationToken cancellationToken = default)
{
_queryLocator.Initialize(compilation);
_symbols = Symbols.Load(compilation);
_g = syntaxGenerator;
_linqToCSharpTranslator = new LinqToCSharpSyntaxTranslator(_g);
_liftableConstantProcessor = new LiftableConstantProcessor(null!);
_queryCompiler = dbContext.GetService<IQueryCompiler>();
_unsafeAccessors.Clear();
_funcletizer = new ExpressionTreeFuncletizer(
dbContext.Model,
dbContext.GetService<IEvaluatableExpressionFilter>(),
dbContext.GetType(),
generateContextAccessors: false,
dbContext.GetService<IDiagnosticsLogger<DbLoggerCategory.Query>>());
// This must be done after we complete generating the final compilation above
_csharpToLinqTranslator.Load(compilation, dbContext, additionalAssembly);
// TODO: Ignore our auto-generated code! Also compiled model, generated code (comment, filename...?).
var generatedSyntaxTrees = new List<GeneratedInterceptorFile>();
foreach (var syntaxTree in compilation.SyntaxTrees)
{
if (_queryLocator.LocateQueries(syntaxTree, precompilationErrors, cancellationToken) is not { Count: > 0 } locatedQueries)
{
continue;
}
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var generatedSyntaxTree = ProcessSyntaxTreeAsync(
syntaxTree, semanticModel, locatedQueries, precompilationErrors, cancellationToken);
if (generatedSyntaxTree is not null)
{
generatedSyntaxTrees.Add(generatedSyntaxTree);
}
}
return generatedSyntaxTrees;
}
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual GeneratedInterceptorFile? ProcessSyntaxTreeAsync(
SyntaxTree syntaxTree,
SemanticModel semanticModel,
IReadOnlyList<InvocationExpressionSyntax> locatedQueries,
List<QueryPrecompilationError> precompilationErrors,
CancellationToken cancellationToken)
{
var queriesPrecompiledInFile = 0;
_namespaces.Clear();
_code.Clear();
_code
.AppendLine()
.AppendLine("#pragma warning disable EF9100 // Precompiled query is experimental")
.AppendLine()
.Append("namespace ").AppendLine(InterceptorsNamespace)
.AppendLine("{")
.IncrementIndent()
.AppendLine("file static class EntityFrameworkCoreInterceptors")
.AppendLine("{")
.IncrementIndent();
for (var queryNum = 0; queryNum < locatedQueries.Count; queryNum++)
{
var querySyntax = locatedQueries[queryNum];
try
{
// We have a query lambda, as a Roslyn syntax tree. Translate to LINQ expression tree.
// TODO: Add verification that this is an EF query over our user's context. If translation returns null the moment
// there's another query root (another context or another LINQ provider), that's fine.
if (_csharpToLinqTranslator.Translate(querySyntax, semanticModel) is not MethodCallExpression terminatingOperator)
{
throw new UnreachableException("Non-method call encountered as the root of a LINQ query");
}
// We have a LINQ representation of the query tree as it appears in the user's source code, but this isn't the same as the
// LINQ tree the EF query pipeline needs to get; the latter is the result of evaluating the queryable operators in the user's
// source code. For example, in the user's code the root is a DbSet as the root, but the expression tree we require needs to
// contain an EntityQueryRootExpression. To get the LINQ tree for EF, we need to evaluate the operator chain, building an
// expression tree as usual.
// However, we cannot evaluate the last operator, since that would execute the query instead of returning an expression tree.
// So we need to chop off the last operator before evaluation, and then (optionally) recompose it back afterwards.
// For ToList(), we don't actually recompose it (since ToList() isn't a node in the expression tree), and for async operators,
// we need to rewrite them to their sync counterparts (since that's what gets injected into the query tree).
var penultimateOperator = terminatingOperator switch
{
// This is needed e.g. for GetEnumerator(), DbSet.AsAsyncEnumerable (non-static terminating operators)
{ Object: Expression @object } => @object,
{ Arguments: [var sourceArgument, ..] } => sourceArgument,
_ => throw new UnreachableException()
};
penultimateOperator = Expression.Lambda<Func<IQueryable>>(penultimateOperator)
.Compile(preferInterpretation: true)().Expression;
// Pass the query through EF's query pipeline; this returns the query's executor function, which can produce an enumerable
// that invokes the query.
// Note that we cannot recompose the terminating operator on top of the evaluated penultimate, since method signatures
// may not allow that (e.g. DbSet.AsAsyncEnumerable() requires a DbSet, but the evaluated value for a DbSet is
// EntityQueryRootExpression. So we handle the penultimate and the terminating separately.
var queryExecutor = CompileQuery(penultimateOperator, terminatingOperator);
// The query has been compiled successfully by the EF query pipeline.
// Now go over each LINQ operator, generating an interceptor for it.
_code.AppendLine($"#region Query{queryNum + 1}").AppendLine();
try
{
_funcletizer.ResetPathCalculation();
if (querySyntax is not { Expression: MemberAccessExpressionSyntax { Expression: var penultimateOperatorSyntax } })
{
throw new UnreachableException();
}
// Generate interceptors for all LINQ operators in the query, starting from the root up until the penultimate.
// Then generate the interceptor for the terminating operator, and finally the query's executor.
GenerateOperatorInterceptorsRecursively(
_code, penultimateOperator, penultimateOperatorSyntax, semanticModel, queryNum + 1, out var operatorNum,
cancellationToken: cancellationToken);
GenerateOperatorInterceptor(
_code, terminatingOperator, querySyntax, semanticModel, queryNum + 1, operatorNum + 1, isTerminatingOperator: true,
cancellationToken);
GenerateQueryExecutor(_code, queryNum + 1, queryExecutor, _namespaces, _unsafeAccessors);
}
finally
{
_code
.AppendLine()
.AppendLine($"#endregion Query{queryNum + 1}");
}
}
catch (Exception e)
{
precompilationErrors.Add(new(querySyntax, e));
continue;
}
// We're done generating the interceptors for the query's LINQ operators.
queriesPrecompiledInFile++;
}
if (queriesPrecompiledInFile == 0)
{
return null;
}
// Output all the unsafe accessors that were generated for all intercepted shapers, e.g.:
// [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<Name>k__BackingField")]
// static extern ref int GetSet_Foo_Name(Foo f);
if (_unsafeAccessors.Count > 0)
{
_code.AppendLine("#region Unsafe accessors");
foreach (var unsafeAccessor in _unsafeAccessors)
{
_code.AppendLine(unsafeAccessor.NormalizeWhitespace().ToFullString());
}
_code.AppendLine("#endregion Unsafe accessors");
}
_code
.DecrementIndent().AppendLine("}")
.DecrementIndent().AppendLine("}");
var mainCode = _code.ToString();
_code.Clear();
_code.AppendLine("// <auto-generated />").AppendLine();
// In addition to the namespaces auto-detected by LinqToCSharpTranslator, we manually add these namespaces which are required
// by manually generated code above.
_namespaces.UnionWith(
[
"System",
"System.Collections.Concurrent",
"System.Collections.Generic",
"System.Linq",
"System.Linq.Expressions",
"System.Runtime.CompilerServices",
"System.Reflection",
"System.Threading.Tasks",
"Microsoft.EntityFrameworkCore",
"Microsoft.EntityFrameworkCore.ChangeTracking.Internal",
"Microsoft.EntityFrameworkCore.Diagnostics",
"Microsoft.EntityFrameworkCore.Infrastructure",
"Microsoft.EntityFrameworkCore.Infrastructure.Internal",
"Microsoft.EntityFrameworkCore.Internal",
"Microsoft.EntityFrameworkCore.Metadata",
"Microsoft.EntityFrameworkCore.Query",
"Microsoft.EntityFrameworkCore.Query.Internal",
"Microsoft.EntityFrameworkCore.Storage"
]);
foreach (var ns in _namespaces
.OrderBy(
ns => ns switch
{
_ when ns.StartsWith("System.", StringComparison.Ordinal) => 10,
_ when ns.StartsWith("Microsoft.", StringComparison.Ordinal) => 9,
_ => 0
})
.ThenBy(ns => ns))
{
_code.Append("using ").Append(ns).AppendLine(";");
}
_code.AppendLine(mainCode);
_code.AppendLine(
"""
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(string filePath, int line, int column) { }
}
}
""");
return new(
$"{Path.GetFileNameWithoutExtension(syntaxTree.FilePath)}.EFInterceptors.g{Path.GetExtension(syntaxTree.FilePath)}",
_code.ToString());
}
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual Expression CompileQuery(Expression penultimateOperator, MethodCallExpression terminatingOperator)
{
// First, check whether this is an async query.
var async = terminatingOperator.Type.IsGenericType
&& terminatingOperator.Type.GetGenericTypeDefinition() is var genericDefinition
&& (genericDefinition == typeof(Task<>) || genericDefinition == typeof(ValueTask<>));
var preparedQuery = PrepareQueryForCompilation(penultimateOperator, terminatingOperator);
// We now need to figure out the return type of the query's executor.
// Non-scalar query expressions (e.g. ToList()) return an IQueryable; the query executor will return an enumerable (sync or async).
// Scalar query expressions just return the scalar type.
var returnType = preparedQuery.Type.IsGenericType
&& preparedQuery.Type.GetGenericTypeDefinition().IsAssignableTo(typeof(IQueryable))
? (async
? typeof(IAsyncEnumerable<>)
: typeof(IEnumerable<>)).MakeGenericType(preparedQuery.Type.GetGenericArguments()[0])
: terminatingOperator.Type;
// We now have the query as a finalized LINQ expression tree, ready for compilation.
// Compile the query, invoking CompileQueryToExpression on the IQueryCompiler from the user's context instance.
try
{
return (Expression)_queryCompiler.GetType()
.GetMethod(nameof(IQueryCompiler.PrecompileQuery))!
.MakeGenericMethod(returnType)
.Invoke(_queryCompiler, [preparedQuery, async])!;
}
catch (TargetInvocationException e) when (e.InnerException is not null)
{
// Unwrap the TargetInvocationException wrapper we get from Invoke()
ExceptionDispatchInfo.Capture(e.InnerException).Throw();
throw;
}
}
private void GenerateOperatorInterceptorsRecursively(
IndentedStringBuilder code,
Expression operatorExpression,
ExpressionSyntax operatorSyntax,
SemanticModel semanticModel,
int queryNum,
out int operatorNum,
CancellationToken cancellationToken)
{
// For non-root operators, we get here with an InvocationExpressionSyntax and its corresponding LINQ MethodCallExpression.
// For the query root, we usually don't get called here: a regular EntityQueryRootExpression corresponds to a DbSet (either
// property access on DbContext or a Set<>() method invocation). We can't intercept property accesses, and in any case there's
// nothing to intercept there.
// However, for FromSql specifically, we get here with an InvocationExpressionSyntax (representing the FromSql() invocation), but
// with a corresponding FromSqlQueryRootExpression - not a MethodCallExpression. We must pass this query root through the
// funcletizer as usual to mimic the normal flow.
switch (operatorExpression)
{
// Regular, non-root LINQ operator; the LINQ method call must correspond to a Roslyn syntax invocation.
// We first recurse to handle the nested operator (i.e. generate the interceptor from the root outer).
case MethodCallExpression operatorMethodCall:
if (operatorSyntax is not InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax { Expression: var nestedOperatorSyntax }
})
{
throw new UnreachableException();
}
// We're an operator (not the query root).
// Continue recursing down - we want to handle from the root up.
var nestedOperatorExpression = operatorMethodCall switch
{
// This is needed e.g. for GetEnumerator(), DbSet.AsAsyncEnumerable (non-static terminating operators)
{ Object: Expression @object } => @object,
{ Arguments: [var sourceArgument, ..] } => sourceArgument,
_ => throw new UnreachableException()
};
GenerateOperatorInterceptorsRecursively(
code, nestedOperatorExpression, nestedOperatorSyntax, semanticModel, queryNum, out operatorNum,
cancellationToken: cancellationToken);
operatorNum++;
GenerateOperatorInterceptor(
code, operatorExpression, operatorSyntax, semanticModel, queryNum, operatorNum, isTerminatingOperator: false,
cancellationToken);
return;
// For FromSql() queries, an InvocationExpressionSyntax (representing the FromSql() invocation), but with a corresponding
// FromSqlQueryRootExpression - not a MethodCallExpression.
// We must generate an interceptor for FromSql() and pass the arguments array through the funcletizer as usual.
case FromSqlQueryRootExpression:
operatorNum = 1;
GenerateOperatorInterceptor(
code, operatorExpression, operatorSyntax, semanticModel, queryNum, operatorNum, isTerminatingOperator: false,
cancellationToken);
return;
// For other query roots, we don't generate interceptors - there are no possible captured variables that need to be
// pass through funcletization (as with FromSqlQueryRootExpression). Simply return to process the first non-root operator.
case QueryRootExpression:
operatorNum = 0;
return;
default:
throw new UnreachableException();
}
}
private void GenerateOperatorInterceptor(
IndentedStringBuilder code,
Expression operatorExpression,
ExpressionSyntax operatorSyntax,
SemanticModel semanticModel,
int queryNum,
int operatorNum,
bool isTerminatingOperator,
CancellationToken cancellationToken)
{
// At this point we know we're intercepting a method call invocation.
// Extract the MemberAccessExpressionSyntax for the invocation, representing the method being called.
var memberAccessSyntax = (operatorSyntax as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax
?? throw new UnreachableException();
// Create the parameter list for our interceptor method from the LINQ operator method's parameter list
if (semanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol is not IMethodSymbol operatorSymbol)
{
throw new InvalidOperationException("Couldn't find method symbol for: " + memberAccessSyntax);
}
// Throughout the code generation below, we will only be dealing with the original generic definition of the operator (and
// generating a generic interceptor); we'll never be dealing with the concrete types for this invocation, since these may
// be unspeakable anonymous types which we can't embed in generated code.
operatorSymbol = operatorSymbol.OriginalDefinition;
// For extension methods, this provides the form which has the "this" as its first parameter.
// TODO: Validate the below, throw informative (e.g. top-level TVF fails here because non-generic)
var reducedOperatorSymbol = operatorSymbol.GetConstructedReducedFrom() ?? operatorSymbol;
var (sourceVariableName, sourceTypeSymbol) = reducedOperatorSymbol.IsStatic
? (reducedOperatorSymbol.Parameters[0].Name, reducedOperatorSymbol.Parameters[0].Type)
: ("source", reducedOperatorSymbol.ReceiverType!);
if (sourceTypeSymbol is not INamedTypeSymbol { TypeArguments: [var sourceElementTypeSymbol]})
{
throw new UnreachableException($"Non-IQueryable first parameter in LINQ operator '{operatorSymbol.Name}'");
}
var sourceElementTypeName = sourceElementTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var returnTypeSymbol = reducedOperatorSymbol.ReturnType;
// Unwrap Task<T> to get the element type (e.g. Task<List<int>>)
var returnTypeWithoutTask = returnTypeSymbol is INamedTypeSymbol namedReturnType
&& returnTypeSymbol.OriginalDefinition.Equals(_symbols.GenericTask, SymbolEqualityComparer.Default)
? namedReturnType.TypeArguments[0]
: returnTypeSymbol;
var returnElementTypeSymbol = returnTypeWithoutTask switch
{
IArrayTypeSymbol arrayTypeSymbol => arrayTypeSymbol.ElementType,
INamedTypeSymbol namedReturnType2
when namedReturnType2.AllInterfaces.Prepend(namedReturnType2)
.Any(
i => i.OriginalDefinition.Equals(_symbols.GenericEnumerable, SymbolEqualityComparer.Default)
|| i.OriginalDefinition.Equals(_symbols.GenericAsyncEnumerable, SymbolEqualityComparer.Default)
|| i.OriginalDefinition.Equals(_symbols.GenericEnumerator, SymbolEqualityComparer.Default))
=> namedReturnType2.TypeArguments[0],
_ => null
};
// Output the interceptor method signature preceded by the [InterceptsLocation] attribute.
var startPosition = operatorSyntax.SyntaxTree.GetLineSpan(memberAccessSyntax.Name.Span, cancellationToken).StartLinePosition;
var interceptorName = $"Query{queryNum}_{memberAccessSyntax.Name}{operatorNum}";
code.AppendLine($"""[InterceptsLocation("{operatorSyntax.SyntaxTree.FilePath}", {startPosition.Line + 1}, {startPosition.Character + 1})]""");
GenerateInterceptorMethodSignature();
code.AppendLine("{").IncrementIndent();
// If this is the first query operator (no nested operator), cast the input source to IInfrastructure<DbContext> and extract the
// DbContext, create a new QueryContext, and wrap it all in a PrecompiledQueryContext that will flow through to the
// terminating operator, where the query will actually get executed.
// Otherwise, if this is a non-first operator, receive the PrecompiledQueryContext from the nested operator and flow it forward.
code.AppendLine(
"var precompiledQueryContext = "
+ (operatorNum == 1
? $"new PrecompiledQueryContext<{sourceElementTypeName}>(((IInfrastructure<DbContext>){sourceVariableName}).Instance);"
: $"(PrecompiledQueryContext<{sourceElementTypeName}>){sourceVariableName};"));
var declaredQueryContextVariable = false;
ProcessCapturedVariables();
if (isTerminatingOperator)
{
// We're intercepting the query's terminating operator - this is where the query actually gets executed.
if (!declaredQueryContextVariable)
{
code.AppendLine("var queryContext = precompiledQueryContext.QueryContext;");
}
var executorFieldIdentifier = $"Query{queryNum}_Executor";
code.AppendLine(
$"{executorFieldIdentifier} ??= Query{queryNum}_GenerateExecutor(precompiledQueryContext.DbContext, precompiledQueryContext.QueryContext);");
if (returnElementTypeSymbol is null)
{
// The query returns a scalar, not an enumerable (e.g. the terminating operator is Max()).
// The executor directly returns the needed result (e.g. int), so just return that.
var returnType = returnTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
code.AppendLine($"return ((Func<QueryContext, {returnType}>)({executorFieldIdentifier}))(queryContext);");
}
else
{
// The query returns an IEnumerable/IAsyncEnumerable/IQueryable, which is a bit trickier: the executor doesn't return a
// simple value as in the scalar case, but rather e.g. SingleQueryingEnumerable; we need to compose the terminating
// operator (e.g. ToList()) on top of that. Cast the executor delegate to Func<QueryContext, IEnumerable<T>>
// (contravariance).
var isAsync =
operatorExpression.Type.IsGenericType
&& operatorExpression.Type.GetGenericTypeDefinition() is var genericDefinition
&& (
genericDefinition == typeof(Task<>)
|| genericDefinition == typeof(ValueTask<>)
|| genericDefinition == typeof(IAsyncEnumerable<>));
var isQueryable = !isAsync
&& operatorExpression.Type.IsGenericType
&& operatorExpression.Type.GetGenericTypeDefinition() == typeof(IQueryable<>);
var returnValue = isAsync
? $"IAsyncEnumerable<{sourceElementTypeName}>"
: $"IEnumerable<{sourceElementTypeName}>";
code.AppendLine(
$"var queryingEnumerable = ((Func<QueryContext, {returnValue}>)({executorFieldIdentifier}))(queryContext);");
if (isQueryable)
{
// If the terminating operator returns IQueryable<T>, that means the query is actually evaluated via foreach
// (i.e. there's no method such as AsEnumerable/ToList which evaluates). Note that this is necessarily sync only -
// IQueryable can't be directly inside await foreach (AsAsyncEnumerable() is required).
// For this case, we need to compose AsQueryable() on top, to make the querying enumerable compatible with the
// operator signature.
code.AppendLine("return queryingEnumerable.AsQueryable();");
}
else
{
if (isAsync)
{
// For sync queries, we get an IEnumerable<TSource> above, and can just compose the original terminating operator
// directly on top of that (ToList(), ToDictionary()...).
// But for async queries, we get an IAsyncEnumerable<TSource> above, but cannot directly compose the original
// terminating operator (ToListAsync(), ToDictionaryAsync()...), since those require an IQueryable<T> in their
// signature (which they internally case to IAsyncEnumerable<T>).
// So we introduce an adapter in the middle, which implements both IQueryable<T> (to be able to compose
// ToListAsync() on top), and IAsyncEnumerable<T> (so that the actual implementation of ToListAsync() works).
// TODO: This is an additional runtime allocation; if we had System.Linq.Async we wouldn't need this. We could
// have additional versions of all async terminating operators over IAsyncEnumerable<T> (effectively duplicating
// System.Linq.Async) as an alternative.
code.AppendLine($"var asyncQueryingEnumerable = new PrecompiledQueryableAsyncEnumerableAdapter<{sourceElementTypeName}>(queryingEnumerable);");
code.Append("return asyncQueryingEnumerable");
}
else
{
code.Append("return queryingEnumerable");
}
// Invoke the original terminating operator (e.g. ToList(), ToDictionary()...) on the querying enumerable, passing
// through the interceptor's arguments.
code.AppendLine(
$".{memberAccessSyntax.Name}({string.Join(", ", operatorSymbol.Parameters.Select(p => p.Name))});");
}
}
}
else
{
// Non-terminating operator - we need to flow precompiledQueryContext forward.
// The operator returns a different IQueryable type as its source (e.g. Select), convert the precompiledQueryContext
// before returning it.
Check.DebugAssert(returnElementTypeSymbol is not null, "Non-terminating operator must return IEnumerable<T>");
code.AppendLine(
returnTypeSymbol switch
{
// The operator return IQueryable<T> or IOrderedQueryable<T>.
// If T is the same as the source, simply return our context as is (note that PrecompiledQueryContext implements
// IOrderedQueryable). Otherwise, e.g. Select() is being applied - change the context's type.
_ when returnTypeSymbol.OriginalDefinition.Equals(_symbols.IQueryable, SymbolEqualityComparer.Default)
|| returnTypeSymbol.OriginalDefinition.Equals(_symbols.IOrderedQueryable, SymbolEqualityComparer.Default)
=> SymbolEqualityComparer.Default.Equals(sourceElementTypeSymbol, returnElementTypeSymbol)
? "return precompiledQueryContext;"
: $"return precompiledQueryContext.ToType<{returnElementTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();",
_ when returnTypeSymbol.OriginalDefinition.Equals(_symbols.IIncludableQueryable, SymbolEqualityComparer.Default)
&& returnTypeSymbol is INamedTypeSymbol { OriginalDefinition.TypeArguments: [_, var includedPropertySymbol] }
=> $"return precompiledQueryContext.ToIncludable<{includedPropertySymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();",
_ => throw new UnreachableException()
});
}
code.DecrementIndent().AppendLine("}").AppendLine();
void GenerateInterceptorMethodSignature()
{
code
.Append("public static ")
.Append(_g.TypeExpression(reducedOperatorSymbol.ReturnType).ToFullString())
.Append(' ')
.Append(interceptorName);
var (typeParameters, constraints) = (reducedOperatorSymbol.IsGenericMethod, reducedOperatorSymbol.ContainingType.IsGenericType) switch
{
(true, false) => (reducedOperatorSymbol.TypeParameters, ((MethodDeclarationSyntax)_g.MethodDeclaration(reducedOperatorSymbol)).ConstraintClauses),
(false, true) => (reducedOperatorSymbol.ContainingType.TypeParameters, ((TypeDeclarationSyntax)_g.Declaration(reducedOperatorSymbol.ContainingType)).ConstraintClauses),
(false, false) => ([], []),
(true, true) => throw new NotImplementedException("Generic method on generic type not supported")
};
if (typeParameters.Length > 0)
{
code.Append('<');
for (var i = 0; i < typeParameters.Length; i++)
{
if (i > 0)
{
code.Append(", ");
}
code.Append(_g.TypeExpression(typeParameters[i]).ToFullString());
}
code.Append('>');
}
code.Append('(');
// For instance methods (IEnumerable<T>.GetEnumerator(), DbSet.GetAsyncEnumerable()...), we generate an extension method
// (with this) for the interceptor.
if (reducedOperatorSymbol is { IsStatic: false, ReceiverType: not null })
{
code
.Append("this ")
.Append(_g.TypeExpression(reducedOperatorSymbol.ReceiverType).ToFullString())
.Append(' ')
.Append(sourceVariableName);
}
for (var i = 0; i < reducedOperatorSymbol.Parameters.Length; i++)
{
var parameter = reducedOperatorSymbol.Parameters[i];
if (i == 0)
{
switch (reducedOperatorSymbol)
{
case { IsExtensionMethod: true }:
code.Append("this ");
break;
// For instance methods we already added a this parameter above
case { IsStatic: false, ReceiverType: not null }:
code.Append(", ");
break;
default:
throw new NotImplementedException("Non-extension static method not supported");
}
}
else
{
code.Append(", ");
}
code
.Append(_g.TypeExpression(parameter.Type).ToFullString())
.Append(' ')
.Append(parameter.Name);
}
code.AppendLine(")");
foreach (var f in constraints)
{
code.AppendLine(f.NormalizeWhitespace().ToFullString());
}
}
void ProcessCapturedVariables()
{
// Go over the operator's arguments (skipping the first, which is the source).
// For those which have captured variables, run them through our funcletizer, which will return code for extracting any captured
// variables from them.
switch (operatorExpression)
{
// Regular case: this is an operator method
case MethodCallExpression operatorMethodCall:
{
var parameters = operatorMethodCall.Method.GetParameters();
for (var i = 1; i < parameters.Length; i++)
{
var parameter = parameters[i];
if (parameter.ParameterType == typeof(CancellationToken))
{
continue;
}
if (_funcletizer.CalculatePathsToEvaluatableRoots(operatorMethodCall, i) is not ExpressionTreeFuncletizer.PathNode
evaluatableRootPaths)
{
// There are no captured variables in this lambda argument - skip the argument
continue;
}
// We have a lambda argument with captured variables. Use the information returned by the funcletizer to generate code
// which extracts them and sets them on our query context.
if (!declaredQueryContextVariable)
{
code.AppendLine("var queryContext = precompiledQueryContext.QueryContext;");
declaredQueryContextVariable = true;
}
if (!parameter.ParameterType.IsSubclassOf(typeof(Expression)))
{
// Special case: this is a non-lambda argument (Skip/Take/FromSql).
// Simply add the argument directly as a parameter
code.AppendLine($"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {parameter.Name});""");
continue;
}
var variableCounter = 0;
// Lambda argument. Recurse through evaluatable path trees.
foreach (var child in evaluatableRootPaths.Children!)
{
GenerateCapturedVariableExtractors(parameter.Name!, parameter.ParameterType, child);
void GenerateCapturedVariableExtractors(
string currentIdentifier,
Type currentType,
ExpressionTreeFuncletizer.PathNode capturedVariablesPathTree)
{
var linqPathSegment =
capturedVariablesPathTree.PathFromParent!(Expression.Parameter(currentType, currentIdentifier));
var collectedNamespaces = new HashSet<string>();
var unsafeAccessors = new HashSet<MethodDeclarationSyntax>();
var roslynPathSegment = _linqToCSharpTranslator.TranslateExpression(
linqPathSegment, constantReplacements: null, collectedNamespaces, unsafeAccessors);
var variableName = capturedVariablesPathTree.ExpressionType.Name;
variableName = char.ToLower(variableName[0]) + variableName[1..^"Expression".Length] + ++variableCounter;
code.AppendLine(
$"var {variableName} = ({capturedVariablesPathTree.ExpressionType.Name}){roslynPathSegment};");
if (capturedVariablesPathTree.Children?.Count > 0)
{
// This is an intermediate node which has captured variables in the children. Continue recursing down.
foreach (var child in capturedVariablesPathTree.Children)
{
GenerateCapturedVariableExtractors(variableName, capturedVariablesPathTree.ExpressionType, child);
}
return;
}
// We've reached a leaf, meaning that it's an evaluatable node that contains captured variables.
// Generate code to evaluate this node and assign the result to the parameters dictionary:
// TODO: For the common case of a simple parameter (member access over closure type), generate reflection code directly
// TODO: instead of going through the interpreter, as we do in the funcletizer itself (for perf)
// TODO: Remove the convert to object. We can flow out the actual type of the evaluatable root, and just stick it
// in Func<> instead of object.
// TODO: For specific cases, don't go through the interpreter, but just integrate code that extracts the value directly.
// (see ExpressionTreeFuncletizer.Evaluate()).
// TODO: Basically this means that the evaluator should come from ExpressionTreeFuncletizer itself, as part of its outputs
// TODO: Integrate try/catch around the evaluation?
code.AppendLine("queryContext.AddParameter(");
using (code.Indent())
{
code
.Append('"').Append(capturedVariablesPathTree.ParameterName!).AppendLine("\",")
.AppendLine($"Expression.Lambda<Func<object?>>(Expression.Convert({variableName}, typeof(object)))")
.AppendLine(".Compile(preferInterpretation: true)")
.AppendLine(".Invoke());");
}
}
}
}
break;
}
// Special case: this is a FromSql query root; we're intercepting the invocation syntax for the FromSql() call, but on the LINQ
// side we have a query root (i.e. not the MethodCallExpression for the FromSql(), but rather its evaluated result)
case FromSqlQueryRootExpression fromSqlQueryRoot:
{
if (_funcletizer.CalculatePathsToEvaluatableRoots(fromSqlQueryRoot.Argument) is not ExpressionTreeFuncletizer.PathNode
evaluatableRootPaths)
{
// There are no captured variables in this FromSqlQueryRootExpression, skip it.
break;
}
// We have a lambda argument with captured variables. Use the information returned by the funcletizer to generate code
// which extracts them and sets them on our query context.
if (!declaredQueryContextVariable)
{
code.AppendLine("var queryContext = precompiledQueryContext.QueryContext;");
declaredQueryContextVariable = true;
}
var argumentsParameter = reducedOperatorSymbol switch
{
{ Name: "FromSqlRaw", Parameters: [_, _, { Name: "parameters" }] } => "parameters",
{ Name: "FromSql", Parameters: [_, { Name: "sql" }] } => "sql.GetArguments()",
{ Name: "FromSqlInterpolated", Parameters: [_, { Name: "sql" }] } => "sql.GetArguments()",
_ => throw new UnreachableException()
};
code.AppendLine(
$"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {argumentsParameter});""");
break;
}
default:
throw new UnreachableException();
}
}
}
private void GenerateQueryExecutor(
IndentedStringBuilder code,
int queryNum,
Expression queryExecutor,
HashSet<string> namespaces,
HashSet<MethodDeclarationSyntax> unsafeAccessors)
{
// We're going to generate the method which will create the query executor (Func<QueryContext, TResult>).
// Note that the we store the executor itself (and return it) as object, not as a typed Func<QueryContext, TResult>.
// We can't strong-type it since it may return an anonymous type, which is unspeakable; so instead we cast down from object to
// the real strongly-typed signature inside the interceptor, where the return value is represented as a generic type parameter
// (which can be an anonymous type).
code
.AppendLine($"private static object Query{queryNum}_GenerateExecutor(DbContext dbContext, QueryContext queryContext)")
.AppendLine("{")
.IncrementIndent()
.AppendLine("var relationalModel = dbContext.Model.GetRelationalModel();")
.AppendLine("var relationalTypeMappingSource = dbContext.GetService<IRelationalTypeMappingSource>();")
.AppendLine("var materializerLiftableConstantContext = new RelationalMaterializerLiftableConstantContext(")
.AppendLine(" dbContext.GetService<ShapedQueryCompilingExpressionVisitorDependencies>(),")
.AppendLine(" dbContext.GetService<RelationalShapedQueryCompilingExpressionVisitorDependencies>(),")
.AppendLine(" dbContext.GetService<RelationalCommandBuilderDependencies>());");
HashSet<string> variableNames = ["relationalModel", "relationalTypeMappingSource", "materializerLiftableConstantContext"];
var materializerLiftableConstantContext =
Expression.Parameter(typeof(RelationalMaterializerLiftableConstantContext), "materializerLiftableConstantContext");
// The materializer expression tree contains LiftedConstantExpression nodes, which contain instructions on how to resolve
// constant values which need to be lifted.
var queryExecutorAfterLiftingExpression =
_liftableConstantProcessor.LiftConstants(queryExecutor, materializerLiftableConstantContext, variableNames);
foreach (var liftedConstant in _liftableConstantProcessor.LiftedConstants)
{
var variableValueSyntax = _linqToCSharpTranslator.TranslateExpression(
liftedConstant.Expression, constantReplacements: null, namespaces, unsafeAccessors);
// code.AppendLine($"{liftedConstant.Parameter.Type.Name} {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};");
code.AppendLine($"var {liftedConstant.Parameter.Name} = {variableValueSyntax.NormalizeWhitespace().ToFullString()};");
}
var queryExecutorSyntaxTree =
(AnonymousFunctionExpressionSyntax)_linqToCSharpTranslator.TranslateExpression(
queryExecutorAfterLiftingExpression,
constantReplacements: null,
namespaces,
unsafeAccessors);
code
.AppendLine($"return {queryExecutorSyntaxTree.NormalizeWhitespace().ToFullString()};")
.DecrementIndent()
.AppendLine("}")
.AppendLine()
.AppendLine($"private static object Query{queryNum}_Executor;");
}
/// <summary>
/// Performs processing of a query's terminating operator before handing the query off for EF compilation.
/// This involves removing the operator when it shouldn't be in the tree (e.g. ToList()), and rewriting async terminating operators
/// to their sync counterparts (e.g. MaxAsync() -> Max()). This only needs to be modified/overridden if a new terminating operator
/// is introduced which needs to be rewritten.
/// </summary>
/// <remarks>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
protected virtual Expression PrepareQueryForCompilation(Expression penultimateOperator, MethodCallExpression terminatingOperator)
{
var method = terminatingOperator.Method;
return method.Name switch
{
// These sync terminating operators are defined over IEnumerable, and don't inject a node into the query tree. Simply remove them.
nameof(Enumerable.AsEnumerable)
or nameof(Enumerable.ToArray)
or nameof(Enumerable.ToDictionary)
or nameof(Enumerable.ToHashSet)
or nameof(Enumerable.ToLookup)
or nameof(Enumerable.ToList)
when method.DeclaringType == typeof(Enumerable)
=> penultimateOperator,
nameof(IEnumerable.GetEnumerator)
when method.DeclaringType is { IsConstructedGenericType: true } declaringType
&& declaringType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
=> penultimateOperator,
// Async ToListAsync, ToArrayAsync and AsAsyncEnumerable don't inject a node into the query tree - remove these as well.
nameof(EntityFrameworkQueryableExtensions.AsAsyncEnumerable)
or nameof(EntityFrameworkQueryableExtensions.ToArrayAsync)
or nameof(EntityFrameworkQueryableExtensions.ToDictionaryAsync)
or nameof(EntityFrameworkQueryableExtensions.ToHashSetAsync)
// or nameof(EntityFrameworkQueryableExtensions.ToLookupAsync)
or nameof(EntityFrameworkQueryableExtensions.ToListAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> penultimateOperator,
// There's also an instance method version of AsAsyncEnumerable on DbSet, remove that as well.
nameof(EntityFrameworkQueryableExtensions.AsAsyncEnumerable)
when method.DeclaringType?.IsConstructedGenericType == true
&& method.DeclaringType.GetGenericTypeDefinition() == typeof(DbSet<>)
=> penultimateOperator,
// The EF async counterparts to all the standard scalar-returning terminating operators. These need to be rewritten, as they
// inject the sync versions into the query tree.
nameof(EntityFrameworkQueryableExtensions.AllAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(QueryableMethods.All),
nameof(EntityFrameworkQueryableExtensions.AnyAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.AnyWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.AnyAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.AnyWithPredicate),
nameof(EntityFrameworkQueryableExtensions.AverageAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(
QueryableMethods.GetAverageWithoutSelector(method.GetParameters()[0].ParameterType.GenericTypeArguments[0])),
nameof(EntityFrameworkQueryableExtensions.AverageAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(
QueryableMethods.GetAverageWithSelector(
method.GetParameters()[1].ParameterType.GenericTypeArguments[0].GenericTypeArguments[1])),
nameof(EntityFrameworkQueryableExtensions.ContainsAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(QueryableMethods.Contains),
nameof(EntityFrameworkQueryableExtensions.CountAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.CountWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.CountAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.CountWithPredicate),
// nameof(EntityFrameworkQueryableExtensions.DefaultIfEmptyAsync)
nameof(EntityFrameworkQueryableExtensions.ElementAtAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(QueryableMethods.ElementAt),
nameof(EntityFrameworkQueryableExtensions.ElementAtOrDefaultAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions)
=> RewriteToSync(QueryableMethods.ElementAtOrDefault),
nameof(EntityFrameworkQueryableExtensions.FirstAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.FirstWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.FirstAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.FirstWithPredicate),
nameof(EntityFrameworkQueryableExtensions.FirstOrDefaultAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.FirstOrDefaultWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.FirstOrDefaultAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.FirstOrDefaultWithPredicate),
nameof(EntityFrameworkQueryableExtensions.LastAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.LastWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.LastAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.LastWithPredicate),
nameof(EntityFrameworkQueryableExtensions.LastOrDefaultAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 2
=> RewriteToSync(QueryableMethods.LastOrDefaultWithoutPredicate),
nameof(EntityFrameworkQueryableExtensions.LastOrDefaultAsync)
when method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && method.GetParameters().Length == 3
=> RewriteToSync(QueryableMethods.LastOrDefaultWithPredicate),