Skip to content

Commit b878f5d

Browse files
committed
Handle primitive collections as multiple parameters.
1 parent 7caf11e commit b878f5d

File tree

65 files changed

+17417
-2406
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+17417
-2406
lines changed

src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,9 @@ public virtual TBuilder ExecutionStrategy(
163163
/// </summary>
164164
/// <remarks>
165165
/// <para>
166-
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
167-
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
168-
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
169-
/// produce worse query plans for certain query types.
166+
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
167+
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
168+
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
170169
/// </para>
171170
/// <para>
172171
/// <see cref="TranslateParameterizedCollectionsToConstants" /> instructs EF to translate the collection to a set of constants:
@@ -176,37 +175,57 @@ public virtual TBuilder ExecutionStrategy(
176175
/// <para>
177176
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
178177
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
179-
/// the default. Likewise, you can translate a specific collection in a specific query to a single parameter by wrapping the
180-
/// parameterized collection in <see cref="EF.Parameter{T}(T)" />: <c>Where(b => EF.Parameter(ids).Contains(b.Id)</c>. This
181-
/// overrides the <see cref="TranslateParameterizedCollectionsToConstants" /> setting.
178+
/// the default.
182179
/// </para>
183180
/// </remarks>
184181
public virtual TBuilder TranslateParameterizedCollectionsToConstants()
185182
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Constantize));
186183

187184
/// <summary>
188-
/// Configures the context to translate parameterized collections to parameters.
185+
/// Configures the context to translate parameterized collections to parameter.
189186
/// </summary>
190187
/// <remarks>
191188
/// <para>
192-
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
193-
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
194-
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
195-
/// produce worse query plans for certain query types.
189+
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
190+
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
191+
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
196192
/// </para>
197193
/// <para>
198-
/// <see cref="TranslateParameterizedCollectionsToParameters" /> explicitly instructs EF to perform the default translation
199-
/// of parameterized collections, which is translating them to parameters.
194+
/// <see cref="TranslateParameterizedCollectionsToParameters" /> instructs EF to translate the collection to a set of constants:
195+
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@ids) ...)</c>.
200196
/// </para>
201197
/// <para>
202-
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
203-
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
198+
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
199+
/// parameterized collection in <see cref="EF.Parameter{T}" />: <c>Where(b => EF.Parameter(ids).Contains(b.Id)</c>. This overrides
204200
/// the default.
205201
/// </para>
206202
/// </remarks>
207203
public virtual TBuilder TranslateParameterizedCollectionsToParameters()
208204
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Parameterize));
209205

206+
/// <summary>
207+
/// Configures the context to translate parameterized collections to expanded parameters.
208+
/// </summary>
209+
/// <remarks>
210+
/// <para>
211+
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
212+
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
213+
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
214+
/// </para>
215+
/// <para>
216+
/// <see cref="TranslateParameterizedCollectionsToExpandedParameters" /> instructs EF to translate the collection to a set of parameters:
217+
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
218+
/// </para>
219+
/// </remarks>
220+
//TODO: When appropriate EF method is implemented, mention it here.
221+
// <para>
222+
// Note that it's possible to cause EF to translate a specific collection in a specific query to expanded parameters by wrapping the
223+
// parameterized collection in <see cref="EF.???{T}" />: <c>Where(b => EF.Parameter(ids).???(b.Id)</c>. This overrides
224+
// the default.
225+
// </para>
226+
public virtual TBuilder TranslateParameterizedCollectionsToExpandedParameters()
227+
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.ParameterizeExpanded));
228+
210229
/// <summary>
211230
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
212231
/// does not modify options that are already in use elsewhere.

src/EFCore.Relational/Internal/ParameterizedCollectionTranslationMode.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,12 @@ public enum ParameterizedCollectionTranslationMode
2626
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2727
/// </summary>
2828
Parameterize,
29+
30+
/// <summary>
31+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
32+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
33+
/// any release. You should only use it directly in your code with extreme caution and knowing that
34+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
35+
/// </summary>
36+
ParameterizeExpanded,
2937
}

src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Concurrent;
55
using System.Runtime.CompilerServices;
6+
using Microsoft.EntityFrameworkCore.Internal;
67
using Microsoft.Extensions.Caching.Memory;
78

89
namespace Microsoft.EntityFrameworkCore.Query.Internal;
@@ -34,13 +35,14 @@ public RelationalCommandCache(
3435
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
3536
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
3637
Expression queryExpression,
37-
bool useRelationalNulls)
38+
bool useRelationalNulls,
39+
ParameterizedCollectionTranslationMode? parameterizedCollectionTranslationMode)
3840
{
3941
_memoryCache = memoryCache;
4042
_querySqlGeneratorFactory = querySqlGeneratorFactory;
4143
_queryExpression = queryExpression;
4244
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(
43-
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls));
45+
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parameterizedCollectionTranslationMode));
4446
}
4547

4648
/// <summary>
@@ -49,7 +51,7 @@ public RelationalCommandCache(
4951
/// any release. You should only use it directly in your code with extreme caution and knowing that
5052
/// doing so can result in application failures when updating to a new Entity Framework Core release.
5153
/// </summary>
52-
public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(IReadOnlyDictionary<string, object?> parameters)
54+
public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(Dictionary<string, object?> parameters)
5355
{
5456
var cacheKey = new CommandCacheKey(_queryExpression, parameters);
5557

src/EFCore.Relational/Query/Internal/RelationalCommandResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal;
99
/// any release. You should only use it directly in your code with extreme caution and knowing that
1010
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1111
/// </summary>
12-
public delegate IRelationalCommandTemplate RelationalCommandResolver(IReadOnlyDictionary<string, object?> parameters);
12+
public delegate IRelationalCommandTemplate RelationalCommandResolver(Dictionary<string, object?> parameters);

src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public RelationalParameterBasedSqlProcessor(
4949
/// <returns>An optimized query expression.</returns>
5050
public virtual Expression Optimize(
5151
Expression queryExpression,
52-
IReadOnlyDictionary<string, object?> parametersValues,
52+
Dictionary<string, object?> parametersValues,
5353
out bool canCache)
5454
{
5555
canCache = true;
@@ -72,7 +72,7 @@ public virtual Expression Optimize(
7272
/// <returns>A processed query expression.</returns>
7373
protected virtual Expression ProcessSqlNullability(
7474
Expression queryExpression,
75-
IReadOnlyDictionary<string, object?> parametersValues,
75+
Dictionary<string, object?> parametersValues,
7676
out bool canCache)
7777
=> new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache);
7878

src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.EntityFrameworkCore.Internal;
5+
46
namespace Microsoft.EntityFrameworkCore.Query;
57

68
/// <summary>
@@ -13,11 +15,20 @@ public sealed record RelationalParameterBasedSqlProcessorParameters
1315
/// </summary>
1416
public bool UseRelationalNulls { get; init; }
1517

18+
/// <summary>
19+
/// A value indicating what translation mode should be used.
20+
/// </summary>
21+
public ParameterizedCollectionTranslationMode? ParameterizedCollectionTranslationMode { get; init; }
22+
1623
/// <summary>
1724
/// Creates a new instance of <see cref="RelationalParameterBasedSqlProcessorParameters" />.
1825
/// </summary>
1926
/// <param name="useRelationalNulls">A value indicating if relational nulls should be used.</param>
27+
/// <param name="parameterizedCollectionTranslationMode">A value indicating what translation mode should be used.</param>
2028
[EntityFrameworkInternal]
21-
public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls)
22-
=> UseRelationalNulls = useRelationalNulls;
29+
public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, ParameterizedCollectionTranslationMode? parameterizedCollectionTranslationMode)
30+
{
31+
UseRelationalNulls = useRelationalNulls;
32+
ParameterizedCollectionTranslationMode = parameterizedCollectionTranslationMode;
33+
}
2334
}

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,10 @@ JsonScalarExpression jsonScalar
295295

296296
var primitiveCollectionsBehavior = RelationalOptionsExtension.Extract(QueryCompilationContext.ContextOptions)
297297
.ParameterizedCollectionTranslationMode;
298-
299298
var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_'));
299+
300300
if (queryParameter.ShouldBeConstantized
301-
|| (primitiveCollectionsBehavior == ParameterizedCollectionTranslationMode.Constantize
301+
|| (primitiveCollectionsBehavior is ParameterizedCollectionTranslationMode.Constantize
302302
&& !queryParameter.ShouldNotBeConstantized))
303303
{
304304
var valuesExpression = new ValuesExpression(
@@ -313,6 +313,21 @@ JsonScalarExpression jsonScalar
313313
sqlParameterExpression.IsNullable);
314314
}
315315

316+
if ((primitiveCollectionsBehavior is null or ParameterizedCollectionTranslationMode.ParameterizeExpanded)
317+
&& !queryParameter.ShouldNotBeConstantized)
318+
{
319+
var valuesExpression = new ValuesExpression(
320+
tableAlias,
321+
sqlParameterExpression,
322+
[ValuesOrderingColumnName, ValuesValueColumnName]);
323+
return CreateShapedQueryExpressionForValuesExpression(
324+
valuesExpression,
325+
tableAlias,
326+
parameterQueryRootExpression.ElementType,
327+
sqlParameterExpression.TypeMapping,
328+
sqlParameterExpression.IsNullable);
329+
}
330+
316331
return TranslatePrimitiveCollection(sqlParameterExpression, property: null, tableAlias);
317332
}
318333

@@ -569,16 +584,23 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s
569584
return TranslateAny(source, anyLambda);
570585
}
571586

572-
// Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements
587+
var primitiveCollectionsBehavior = RelationalOptionsExtension.Extract(QueryCompilationContext.ContextOptions)
588+
.ParameterizedCollectionTranslationMode;
589+
// Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements.
573590
if (TryExtractBareInlineCollectionValues(source, out var values, out var valuesParameter))
574591
{
575-
var inExpression = (values, valuesParameter) switch
592+
if (values is not null)
576593
{
577-
(not null, null) => _sqlExpressionFactory.In(translatedItem, values),
578-
(null, not null) => _sqlExpressionFactory.In(translatedItem, valuesParameter),
579-
_ => throw new UnreachableException(),
580-
};
581-
return source.Update(new SelectExpression(inExpression, _sqlAliasManager), source.ShaperExpression);
594+
var inExpression = _sqlExpressionFactory.In(translatedItem, values);
595+
return source.Update(new SelectExpression(inExpression, _sqlAliasManager), source.ShaperExpression);
596+
}
597+
if (valuesParameter is not null
598+
// Expanding parameters will happen in 2nd stage of query pipeline.
599+
&& primitiveCollectionsBehavior is not (null or ParameterizedCollectionTranslationMode.ParameterizeExpanded))
600+
{
601+
var inExpression = _sqlExpressionFactory.In(translatedItem, valuesParameter);
602+
return source.Update(new SelectExpression(inExpression, _sqlAliasManager), source.ShaperExpression);
603+
}
582604
}
583605

584606
// Translate to IN with a subquery.

src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue
1919
private readonly bool _threadSafetyChecksEnabled;
2020
private readonly bool _detailedErrorsEnabled;
2121
private readonly bool _useRelationalNulls;
22+
private readonly ParameterizedCollectionTranslationMode? _parameterizedCollectionTranslationMode;
2223
private readonly bool _isPrecompiling;
2324

2425
private readonly RelationalParameterBasedSqlProcessor _relationalParameterBasedSqlProcessor;
@@ -54,14 +55,15 @@ public RelationalShapedQueryCompilingExpressionVisitor(
5455

5556
_relationalParameterBasedSqlProcessor =
5657
relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create(
57-
new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls));
58+
new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls, _parameterizedCollectionTranslationMode));
5859
_querySqlGeneratorFactory = relationalDependencies.QuerySqlGeneratorFactory;
5960

6061
_contextType = queryCompilationContext.ContextType;
6162
_tags = queryCompilationContext.Tags;
6263
_threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled;
6364
_detailedErrorsEnabled = dependencies.CoreSingletonOptions.AreDetailedErrorsEnabled;
6465
_useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls;
66+
_parameterizedCollectionTranslationMode = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).ParameterizedCollectionTranslationMode;
6567
_isPrecompiling = queryCompilationContext.IsPrecompiling;
6668
}
6769

@@ -497,15 +499,16 @@ private Expression CreateRelationalCommandResolverExpression(Expression queryExp
497499
RelationalDependencies.QuerySqlGeneratorFactory,
498500
RelationalDependencies.RelationalParameterBasedSqlProcessorFactory,
499501
queryExpression,
500-
_useRelationalNulls);
502+
_useRelationalNulls,
503+
_parameterizedCollectionTranslationMode);
501504

502505
var commandLiftableConstant = RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant(
503506
relationalCommandCache,
504507
GenerateRelationalCommandCacheExpression(),
505508
"relationalCommandCache",
506509
typeof(RelationalCommandCache));
507510

508-
var parametersParameter = Parameter(typeof(IReadOnlyDictionary<string, object?>), "parameters");
511+
var parametersParameter = Parameter(typeof(Dictionary<string, object?>), "parameters");
509512

510513
return Lambda<RelationalCommandResolver>(
511514
Call(
@@ -542,7 +545,7 @@ bool TryGeneratePregeneratedCommandResolver(
542545
return false;
543546
}
544547

545-
var parameterDictionaryParameter = Parameter(typeof(IReadOnlyDictionary<string, object?>), "parameters");
548+
var parameterDictionaryParameter = Parameter(typeof(Dictionary<string, object?>), "parameters");
546549
var resultParameter = Parameter(typeof(IRelationalCommandTemplate), "result");
547550
Expression resolverBody;
548551
bool canCache;
@@ -657,7 +660,7 @@ static object GenerateNonNullParameterValue(Type type)
657660
}
658661
}
659662

660-
Expression GenerateRelationalCommandExpression(IReadOnlyDictionary<string, object?> parameters, out bool canCache)
663+
Expression GenerateRelationalCommandExpression(Dictionary<string, object?> parameters, out bool canCache)
661664
{
662665
var queryExpression = _relationalParameterBasedSqlProcessor.Optimize(select, parameters, out canCache);
663666
if (!canCache)
@@ -747,7 +750,8 @@ Expression<Func<RelationalMaterializerLiftableConstantContext, object>> Generate
747750
MakeMemberAccess(contextParameter, _relationalDependenciesProperty),
748751
_relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty),
749752
Constant(queryExpression),
750-
Constant(_useRelationalNulls)),
753+
Constant(_useRelationalNulls),
754+
Constant(_parameterizedCollectionTranslationMode, typeof(ParameterizedCollectionTranslationMode?))),
751755
contextParameter);
752756
}
753757
}

src/EFCore.Relational/Query/SqlExpressions/ValuesExpression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public override Expression Quote()
167167
Constant(Alias, typeof(string)),
168168
RowValues is not null
169169
? NewArrayInit(typeof(RowValueExpression), RowValues.Select(rv => rv.Quote()))
170-
: Constant(null, typeof(RowValueExpression)),
170+
: Constant(null, typeof(IReadOnlyList<RowValueExpression>)),
171171
RelationalExpressionQuotingUtilities.QuoteOrNull(ValuesParameter),
172172
NewArrayInit(typeof(string), ColumnNames.Select(Constant)),
173173
RelationalExpressionQuotingUtilities.QuoteAnnotations(Annotations));

0 commit comments

Comments
 (0)