Skip to content

Commit a33777d

Browse files
committed
Handle primitive collections as multiple parameters.
1 parent c5313b0 commit a33777d

File tree

78 files changed

+2626
-2693
lines changed

Some content is hidden

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

78 files changed

+2626
-2693
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore;
5+
6+
/// <summary>
7+
/// Methods that are useful in application code. For example, referencing a shadow state property in a LINQ query.
8+
/// </summary>
9+
/// <remarks>
10+
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> and
11+
/// <see href="https://aka.ms/efcore-docs-efproperty">Using EF.Property in EF Core queries</see> for more information and examples.
12+
/// </remarks>
13+
public static class EFExtensions
14+
{
15+
/// <summary>
16+
/// Methods that are useful in application code. For example, referencing a shadow state property in a LINQ query.
17+
/// </summary>
18+
/// <remarks>
19+
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> and
20+
/// <see href="https://aka.ms/efcore-docs-efproperty">Using EF.Property in EF Core queries</see> for more information and examples.
21+
/// </remarks>
22+
extension(EF)
23+
{
24+
/// <summary>
25+
/// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a multiple parameter expressions.
26+
/// </summary>
27+
/// <remarks>Note that this is a static method accessed through the top-level <see cref="EF" /> static type.</remarks>
28+
/// <typeparam name="T">The type of collection element.</typeparam>
29+
/// <param name="argument">The collection to be integrated as parameters into the query.</param>
30+
/// <returns>The same value for further use in the query.</returns>
31+
public static IEnumerable<T> MultipleParameters<T>(IEnumerable<T> argument)
32+
=> throw new InvalidOperationException(RelationalStrings.EFMultipleParametersInvoked);
33+
}
34+
}

src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.ComponentModel;
5-
using Microsoft.EntityFrameworkCore.Internal;
65

76
namespace Microsoft.EntityFrameworkCore.Infrastructure;
87

@@ -163,10 +162,9 @@ public virtual TBuilder ExecutionStrategy(
163162
/// </summary>
164163
/// <remarks>
165164
/// <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.
165+
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
166+
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
167+
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
170168
/// </para>
171169
/// <para>
172170
/// <see cref="TranslateParameterizedCollectionsToConstants" /> instructs EF to translate the collection to a set of constants:
@@ -176,36 +174,42 @@ public virtual TBuilder ExecutionStrategy(
176174
/// <para>
177175
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
178176
/// 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.
177+
/// the default.
182178
/// </para>
183179
/// </remarks>
180+
[Obsolete("Use UseParameterizedCollectionMode instead.")]
184181
public virtual TBuilder TranslateParameterizedCollectionsToConstants()
185-
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Constantize));
182+
=> UseParameterizedCollectionMode(ParameterizedCollectionMode.Constants);
186183

187184
/// <summary>
188-
/// Configures the context to translate parameterized collections to parameters.
185+
/// Configures the context to translate parameterized collections to a single array-like 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 single array-like parameter:
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>
203+
[Obsolete("Use UseParameterizedCollectionMode instead.")]
207204
public virtual TBuilder TranslateParameterizedCollectionsToParameters()
208-
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Parameterize));
205+
=> UseParameterizedCollectionMode(ParameterizedCollectionMode.Parameter);
206+
207+
/// <summary>
208+
/// Configures the <see cref="ParameterizedCollectionMode" /> to use when translating parameterized collections.
209+
/// </summary>
210+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
211+
public virtual TBuilder UseParameterizedCollectionMode(ParameterizedCollectionMode parameterizedCollectionMode)
212+
=> WithOption(e => (TExtension)e.WithUseParameterizedCollectionMode(parameterizedCollectionMode));
209213

210214
/// <summary>
211215
/// Sets an option by cloning the extension used to store the settings. This ensures the builder

src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Text;
5-
using Microsoft.EntityFrameworkCore.Internal;
65

76
namespace Microsoft.EntityFrameworkCore.Infrastructure;
87

@@ -37,7 +36,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension
3736
private string? _migrationsHistoryTableName;
3837
private string? _migrationsHistoryTableSchema;
3938
private Func<ExecutionStrategyDependencies, IExecutionStrategy>? _executionStrategyFactory;
40-
private ParameterizedCollectionTranslationMode? _parameterizedCollectionTranslationMode;
39+
private ParameterizedCollectionMode? _parameterizedCollectionMode;
4140

4241
/// <summary>
4342
/// Creates a new set of options with everything set to default values.
@@ -65,7 +64,7 @@ protected RelationalOptionsExtension(RelationalOptionsExtension copyFrom)
6564
_migrationsHistoryTableName = copyFrom._migrationsHistoryTableName;
6665
_migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema;
6766
_executionStrategyFactory = copyFrom._executionStrategyFactory;
68-
_parameterizedCollectionTranslationMode = copyFrom._parameterizedCollectionTranslationMode;
67+
_parameterizedCollectionMode = copyFrom._parameterizedCollectionMode;
6968
}
7069

7170
/// <summary>
@@ -387,20 +386,20 @@ public virtual RelationalOptionsExtension WithExecutionStrategyFactory(
387386
/// <summary>
388387
/// Configured translation mode for parameterized collections.
389388
/// </summary>
390-
public virtual ParameterizedCollectionTranslationMode? ParameterizedCollectionTranslationMode
391-
=> _parameterizedCollectionTranslationMode;
389+
public virtual ParameterizedCollectionMode ParameterizedCollectionMode
390+
=> _parameterizedCollectionMode ?? ParameterizedCollectionMode.MultipleParameters;
392391

393392
/// <summary>
394393
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
395394
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
396395
/// </summary>
397-
/// <param name="parameterizedCollectionTranslationMode">The option to change.</param>
398-
public virtual RelationalOptionsExtension WithParameterizedCollectionTranslationMode(
399-
ParameterizedCollectionTranslationMode parameterizedCollectionTranslationMode)
396+
/// <param name="parameterizedCollectionMode">The option to change.</param>
397+
public virtual RelationalOptionsExtension WithUseParameterizedCollectionMode(
398+
ParameterizedCollectionMode parameterizedCollectionMode)
400399
{
401400
var clone = Clone();
402401

403-
clone._parameterizedCollectionTranslationMode = parameterizedCollectionTranslationMode;
402+
clone._parameterizedCollectionMode = parameterizedCollectionMode;
404403

405404
return clone;
406405
}
@@ -563,9 +562,9 @@ public override string LogFragment
563562
builder.Append(Extension._migrationsHistoryTableName ?? HistoryRepository.DefaultTableName).Append(' ');
564563
}
565564

566-
if (Extension._parameterizedCollectionTranslationMode != null)
565+
if (Extension._parameterizedCollectionMode != null)
567566
{
568-
builder.Append("ParameterizedCollectionTranslationMode=").Append(Extension._parameterizedCollectionTranslationMode)
567+
builder.Append("ParameterizedCollectionTranslationMode=").Append(Extension._parameterizedCollectionMode)
569568
.Append(' ');
570569
}
571570

src/EFCore.Relational/Internal/ParameterizedCollectionTranslationMode.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore;
5+
6+
/// <summary>
7+
/// Indicates how parameterized collections are translated into SQL.
8+
/// </summary>
9+
public enum ParameterizedCollectionMode
10+
{
11+
/// <summary>
12+
/// Instructs EF to translate the collection to a set of constants:
13+
/// <c>WHERE [x].[Id] IN (1, 2, 3)</c>.
14+
/// </summary>
15+
/// <remarks>
16+
/// <para>
17+
/// This can produce better query plans for certain query types, but can also lead to query
18+
/// plan bloat.
19+
/// </para>
20+
/// <para>
21+
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
22+
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(x => EF.Constant(ids).Contains(x.Id)</c>. This overrides
23+
/// the default.
24+
/// </para>
25+
/// </remarks>
26+
Constants,
27+
28+
/// <summary>
29+
/// Instructs EF to translate the collection to a single array-like parameter:
30+
/// <c>WHERE [x].[Id] IN (SELECT [i].[value] FROM OPENJSON(@ids) ...)</c>.
31+
/// </summary>
32+
/// <remarks>
33+
/// <para>
34+
/// This can produce suboptimal query plans for certain query types.
35+
/// </para>
36+
/// <para>
37+
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
38+
/// parameterized collection in <see cref="EF.Parameter{T}" />: <c>Where(x => EF.Parameter(ids).Contains(x.Id)</c>. This overrides
39+
/// the default.
40+
/// </para>
41+
/// </remarks>
42+
Parameter,
43+
44+
/// <summary>
45+
/// Instructs EF to translate the collection to a set of parameters:
46+
/// <c>WHERE [x].[Id] IN (@ids1, @ids2, @ids3)</c>.
47+
/// </summary>
48+
/// <remarks>
49+
/// <para>
50+
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
51+
/// parameterized collection in <see cref="EFExtensions.MultipleParameters{T}" />: <c>Where(x => EF.MultipleParameters(ids).Contains(x.Id)</c>. This overrides
52+
/// the default.
53+
/// </para>
54+
/// </remarks>
55+
MultipleParameters,
56+
}

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@
355355
<data name="DuplicateSeedDataSensitive" xml:space="preserve">
356356
<value>A seed entity for entity type '{entityType}' has the same key value {keyValue} as another seed entity mapped to the same table '{table}'. Key values should be unique across seed entities.</value>
357357
</data>
358+
<data name="EFMultipleParametersInvoked" xml:space="preserve">
359+
<value>The EF.MultipleParameters&lt;T&gt; method may only be used within Entity Framework LINQ queries.</value>
360+
</data>
358361
<data name="EmptyCollectionNotSupportedAsInlineQueryRoot" xml:space="preserve">
359362
<value>Empty collections are not supported as inline query roots.</value>
360363
</data>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ public RelationalCommandCache(
3434
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
3535
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
3636
Expression queryExpression,
37-
bool useRelationalNulls)
37+
bool useRelationalNulls,
38+
ParameterizedCollectionMode parameterizedCollectionMode)
3839
{
3940
_memoryCache = memoryCache;
4041
_querySqlGeneratorFactory = querySqlGeneratorFactory;
4142
_queryExpression = queryExpression;
4243
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(
43-
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls));
44+
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parameterizedCollectionMode));
4445
}
4546

4647
/// <summary>
@@ -49,7 +50,7 @@ public RelationalCommandCache(
4950
/// any release. You should only use it directly in your code with extreme caution and knowing that
5051
/// doing so can result in application failures when updating to a new Entity Framework Core release.
5152
/// </summary>
52-
public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(IReadOnlyDictionary<string, object?> parameters)
53+
public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(Dictionary<string, object?> parameters)
5354
{
5455
var cacheKey = new CommandCacheKey(_queryExpression, parameters);
5556

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

0 commit comments

Comments
 (0)