Skip to content

Handle primitive collections as multiple parameters. #36157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 26, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/EFCore.Relational/EFExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Methods that are useful in application code. For example, referencing a shadow state property in a LINQ query.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> and
/// <see href="https://aka.ms/efcore-docs-efproperty">Using EF.Property in EF Core queries</see> for more information and examples.
/// </remarks>
public static class EFExtensions
{
/// <summary>
/// Methods that are useful in application code. For example, referencing a shadow state property in a LINQ query.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-database-functions">Database functions</see> and
/// <see href="https://aka.ms/efcore-docs-efproperty">Using EF.Property in EF Core queries</see> for more information and examples.
/// </remarks>
extension(EF)
{
/// <summary>
/// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a multiple parameter expressions.
/// </summary>
/// <remarks>Note that this is a static method accessed through the top-level <see cref="EF" /> static type.</remarks>
/// <typeparam name="T">The type of collection element.</typeparam>
/// <param name="argument">The collection to be integrated as parameters into the query.</param>
/// <returns>The same value for further use in the query.</returns>
public static IEnumerable<T> MultipleParameters<T>(IEnumerable<T> argument)
=> throw new InvalidOperationException(RelationalStrings.EFMultipleParametersInvoked);
}
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure;

@@ -163,10 +162,9 @@ public virtual TBuilder ExecutionStrategy(
/// </summary>
/// <remarks>
/// <para>
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
/// produce worse query plans for certain query types.
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
/// </para>
/// <para>
/// <see cref="TranslateParameterizedCollectionsToConstants" /> instructs EF to translate the collection to a set of constants:
@@ -176,36 +174,42 @@ public virtual TBuilder ExecutionStrategy(
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
/// the default. Likewise, you can translate a specific collection in a specific query to a single parameter by wrapping the
/// parameterized collection in <see cref="EF.Parameter{T}(T)" />: <c>Where(b => EF.Parameter(ids).Contains(b.Id)</c>. This
/// overrides the <see cref="TranslateParameterizedCollectionsToConstants" /> setting.
/// the default.
/// </para>
/// </remarks>
[Obsolete("Use UseParameterizedCollectionMode instead.")]
public virtual TBuilder TranslateParameterizedCollectionsToConstants()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Constantize));
=> UseParameterizedCollectionMode(ParameterizedCollectionMode.Constants);

/// <summary>
/// Configures the context to translate parameterized collections to parameters.
/// Configures the context to translate parameterized collections to a single array-like parameter.
/// </summary>
/// <remarks>
/// <para>
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
/// produce worse query plans for certain query types.
/// When a LINQ query contains a parameterized collection, by default EF Core translates as a multiple SQL parameters,
/// if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
/// </para>
/// <para>
/// <see cref="TranslateParameterizedCollectionsToParameters" /> explicitly instructs EF to perform the default translation
/// of parameterized collections, which is translating them to parameters.
/// <see cref="TranslateParameterizedCollectionsToParameters" /> instructs EF to translate the collection to a single array-like parameter:
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@ids) ...)</c>.
/// </para>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
/// parameterized collection in <see cref="EF.Parameter{T}" />: <c>Where(b => EF.Parameter(ids).Contains(b.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
[Obsolete("Use UseParameterizedCollectionMode instead.")]
public virtual TBuilder TranslateParameterizedCollectionsToParameters()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Parameterize));
=> UseParameterizedCollectionMode(ParameterizedCollectionMode.Parameter);

/// <summary>
/// Configures the <see cref="ParameterizedCollectionMode" /> to use when translating parameterized collections.
/// </summary>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public virtual TBuilder UseParameterizedCollectionMode(ParameterizedCollectionMode parameterizedCollectionMode)
=> WithOption(e => (TExtension)e.WithUseParameterizedCollectionMode(parameterizedCollectionMode));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure;

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

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

/// <summary>
@@ -387,20 +386,20 @@ public virtual RelationalOptionsExtension WithExecutionStrategyFactory(
/// <summary>
/// Configured translation mode for parameterized collections.
/// </summary>
public virtual ParameterizedCollectionTranslationMode? ParameterizedCollectionTranslationMode
=> _parameterizedCollectionTranslationMode;
public virtual ParameterizedCollectionMode ParameterizedCollectionMode
=> _parameterizedCollectionMode ?? ParameterizedCollectionMode.MultipleParameters;

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

clone._parameterizedCollectionTranslationMode = parameterizedCollectionTranslationMode;
clone._parameterizedCollectionMode = parameterizedCollectionMode;

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

if (Extension._parameterizedCollectionTranslationMode != null)
if (Extension._parameterizedCollectionMode != null)
{
builder.Append("ParameterizedCollectionTranslationMode=").Append(Extension._parameterizedCollectionTranslationMode)
builder.Append("ParameterizedCollectionTranslationMode=").Append(Extension._parameterizedCollectionMode)
.Append(' ');
}

This file was deleted.

56 changes: 56 additions & 0 deletions src/EFCore.Relational/ParameterizedCollectionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Indicates how parameterized collections are translated into SQL.
/// </summary>
public enum ParameterizedCollectionMode
{
/// <summary>
/// Instructs EF to translate the collection to a set of constants:
/// <c>WHERE [x].[Id] IN (1, 2, 3)</c>.
/// </summary>
/// <remarks>
/// <para>
/// This can produce better query plans for certain query types, but can also lead to query
/// plan bloat.
/// </para>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(x => EF.Constant(ids).Contains(x.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
Constants,

/// <summary>
/// Instructs EF to translate the collection to a single array-like parameter:
/// <c>WHERE [x].[Id] IN (SELECT [i].[value] FROM OPENJSON(@ids) ...)</c>.
/// </summary>
/// <remarks>
/// <para>
/// This can produce suboptimal query plans for certain query types.
/// </para>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
/// parameterized collection in <see cref="EF.Parameter{T}" />: <c>Where(x => EF.Parameter(ids).Contains(x.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
Parameter,

/// <summary>
/// Instructs EF to translate the collection to a set of parameters:
/// <c>WHERE [x].[Id] IN (@ids1, @ids2, @ids3)</c>.
/// </summary>
/// <remarks>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the
/// parameterized collection in <see cref="EFExtensions.MultipleParameters{T}" />: <c>Where(x => EF.MultipleParameters(ids).Contains(x.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
MultipleParameters,
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
@@ -355,6 +355,9 @@
<data name="DuplicateSeedDataSensitive" xml:space="preserve">
<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>
</data>
<data name="EFMultipleParametersInvoked" xml:space="preserve">
<value>The EF.MultipleParameters&lt;T&gt; method may only be used within Entity Framework LINQ queries.</value>
</data>
<data name="EmptyCollectionNotSupportedAsInlineQueryRoot" xml:space="preserve">
<value>Empty collections are not supported as inline query roots.</value>
</data>
Original file line number Diff line number Diff line change
@@ -34,13 +34,14 @@ public RelationalCommandCache(
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
Expression queryExpression,
bool useRelationalNulls)
bool useRelationalNulls,
ParameterizedCollectionMode parameterizedCollectionMode)
{
_memoryCache = memoryCache;
_querySqlGeneratorFactory = querySqlGeneratorFactory;
_queryExpression = queryExpression;
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls));
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parameterizedCollectionMode));
}

/// <summary>
@@ -49,7 +50,7 @@ public RelationalCommandCache(
/// 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 IRelationalCommandTemplate GetRelationalCommandTemplate(IReadOnlyDictionary<string, object?> parameters)
public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(Dictionary<string, object?> parameters)
{
var cacheKey = new CommandCacheKey(_queryExpression, parameters);

Original file line number Diff line number Diff line change
@@ -9,4 +9,4 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal;
/// 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 delegate IRelationalCommandTemplate RelationalCommandResolver(IReadOnlyDictionary<string, object?> parameters);
public delegate IRelationalCommandTemplate RelationalCommandResolver(Dictionary<string, object?> parameters);
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ public RelationalParameterBasedSqlProcessor(
/// <returns>An optimized query expression.</returns>
public virtual Expression Optimize(
Expression queryExpression,
IReadOnlyDictionary<string, object?> parametersValues,
Dictionary<string, object?> parametersValues,
out bool canCache)
{
canCache = true;
@@ -72,7 +72,7 @@ public virtual Expression Optimize(
/// <returns>A processed query expression.</returns>
protected virtual Expression ProcessSqlNullability(
Expression queryExpression,
IReadOnlyDictionary<string, object?> parametersValues,
Dictionary<string, object?> parametersValues,
out bool canCache)
=> new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache);

Loading