Skip to content

Mechanism for accessing parameters and disabling SQL caching #36267

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

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ or nameof(EntityFrameworkQueryableExtensions.ExecuteUpdateAsync),
{
// 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}", {parameterName});""");
code.AppendLine($"""queryContext.Parameters.Add("{evaluatableRootPaths.ParameterName}", {parameterName});""");
continue;
}

Expand Down Expand Up @@ -849,7 +849,7 @@ void GenerateCapturedVariableExtractors(
// (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(");
code.AppendLine("queryContext.Parameters.Add(");
using (code.Indent())
{
code
Expand Down Expand Up @@ -893,7 +893,7 @@ void GenerateCapturedVariableExtractors(
};

code.AppendLine(
$"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {argumentsParameter});""");
$"""queryContext.Parameters.Add("{evaluatableRootPaths.ParameterName}", {argumentsParameter});""");

break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static IRelationalCommand RentAndPopulateRelationalCommand(
this RelationalCommandResolver relationalCommandResolver,
RelationalQueryContext queryContext)
{
var relationalCommandTemplate = relationalCommandResolver(queryContext.ParameterValues);
var relationalCommandTemplate = relationalCommandResolver(queryContext.Parameters);
var relationalCommand = queryContext.Connection.RentCommand();
relationalCommand.PopulateFrom(relationalCommandTemplate);
return relationalCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,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:
Expand All @@ -176,37 +175,57 @@ 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>
public virtual TBuilder TranslateParameterizedCollectionsToConstants()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Constantize));

/// <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>
public virtual TBuilder TranslateParameterizedCollectionsToParameters()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Parameterize));

/// <summary>
/// Configures the context to translate parameterized collections to expanded parameters.
/// </summary>
/// <remarks>
/// <para>
/// 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="TranslateParameterizedCollectionsToExpandedParameters" /> instructs EF to translate the collection to a set of parameters:
/// <c>WHERE [b].[Id] IN (@ids1, @ids2, @ids3)</c>.
/// </para>
/// </remarks>
//TODO: When appropriate EF method is implemented, mention it here.
// <para>
// Note that it's possible to cause EF to translate a specific collection in a specific query to expanded parameters by wrapping the
// parameterized collection in <see cref="EF.???{T}" />: <c>Where(b => EF.Parameter(ids).???(b.Id)</c>. This overrides
// the default.
// </para>
public virtual TBuilder TranslateParameterizedCollectionsToExpandedParameters()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.ParameterizeExpanded));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
/// does not modify options that are already in use elsewhere.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ public enum ParameterizedCollectionTranslationMode
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
Parameterize,

/// <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>
ParameterizeExpanded,
}
45 changes: 45 additions & 0 deletions src/EFCore.Relational/Query/CacheSafeParameterFacade.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.EntityFrameworkCore.Query;

/// <summary>
/// A facade over <see cref="QueryContext.Parameters" /> which provides cache-safe way to access parameters after the SQL cache.
/// </summary>
/// <remarks>
/// The SQL cache only includes then nullability of parameters in its cache key. Accordingly, this type exposes an API for checking
/// the nullability of a parameter. It also allows retrieving the full parameter dictionary for arbitrary checks, but when this
/// API is called, the facade records this fact, and the resulting SQL will not get cached.
/// </remarks>
public sealed class CacheSafeParameterFacade(Dictionary<string, object?> parameters)
{
/// <summary>
/// Returns whether the parameter with the given name is null.
/// </summary>
/// <remarks>
/// The method assumes that the parameter with the given name exists in the dictionary,
/// and otherwise throws <see cref="UnreachableException" />.
/// </remarks>
public bool IsParameterNull(string parameterName)
=> !parameters.TryGetValue(parameterName, out var value)
? throw new UnreachableException($"Parameter with name '{parameterName}' does not exist.")
: value is null;

/// <summary>
/// Returns the full dictionary of parameters, and disables caching for the generated SQL.
/// </summary>
public Dictionary<string, object?> GetParametersAndDisableSqlCaching()
{
CanCache = false;

return parameters;
}

/// <summary>
/// Whether the SQL generated using this facade can be cached, i.e. whether the full dictionary of parameters
/// has been accessed.
/// </summary>
public bool CanCache { get; private set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ IEnumerator IEnumerable.GetEnumerator()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual DbCommand CreateDbCommand()
=> _relationalCommandResolver(_relationalQueryContext.ParameterValues)
=> _relationalCommandResolver(_relationalQueryContext.Parameters)
.CreateDbCommand(
new RelationalCommandParameterObject(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues,
_relationalQueryContext.Parameters,
null,
null,
null,
Expand Down Expand Up @@ -269,7 +269,7 @@ private static bool InitializeReader(Enumerator enumerator)
enumerator._dataReader = relationalCommand.ExecuteReader(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down Expand Up @@ -384,7 +384,7 @@ private static async Task<bool> InitializeReaderAsync(AsyncEnumerator enumerator
enumerator._dataReader = await relationalCommand.ExecuteReaderAsync(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ IEnumerator IEnumerable.GetEnumerator()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual DbCommand CreateDbCommand()
=> _relationalCommandResolver(_relationalQueryContext.ParameterValues)
=> _relationalCommandResolver(_relationalQueryContext.Parameters)
.CreateDbCommand(
new RelationalCommandParameterObject(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues,
_relationalQueryContext.Parameters,
null,
null,
null, CommandSource.LinqQuery),
Expand Down Expand Up @@ -339,7 +339,7 @@ private static bool InitializeReader(Enumerator enumerator)
var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down Expand Up @@ -519,7 +519,7 @@ private static async Task<bool> InitializeReaderAsync(AsyncEnumerator enumerator
var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ IEnumerator IEnumerable.GetEnumerator()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual DbCommand CreateDbCommand()
=> _relationalCommandResolver(_relationalQueryContext.ParameterValues)
=> _relationalCommandResolver(_relationalQueryContext.Parameters)
.CreateDbCommand(
new RelationalCommandParameterObject(
_relationalQueryContext.Connection,
_relationalQueryContext.ParameterValues,
_relationalQueryContext.Parameters,
null,
null,
null, CommandSource.LinqQuery),
Expand Down Expand Up @@ -339,7 +339,7 @@ private static bool InitializeReader(Enumerator enumerator)
var dataReader = enumerator._dataReader = relationalCommand.ExecuteReader(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down Expand Up @@ -510,7 +510,7 @@ private static async Task<bool> InitializeReaderAsync(AsyncEnumerator enumerator
var dataReader = enumerator._dataReader = await relationalCommand.ExecuteReaderAsync(
new RelationalCommandParameterObject(
enumerator._relationalQueryContext.Connection,
enumerator._relationalQueryContext.ParameterValues,
enumerator._relationalQueryContext.Parameters,
enumerator._readerColumns,
enumerator._relationalQueryContext.Context,
enumerator._relationalQueryContext.CommandLogger,
Expand Down
10 changes: 6 additions & 4 deletions src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Caching.Memory;

namespace Microsoft.EntityFrameworkCore.Query.Internal;
Expand Down Expand Up @@ -34,13 +35,14 @@ public RelationalCommandCache(
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
Expression queryExpression,
bool useRelationalNulls)
bool useRelationalNulls,
ParameterizedCollectionTranslationMode? parameterizedCollectionTranslationMode)
{
_memoryCache = memoryCache;
_querySqlGeneratorFactory = querySqlGeneratorFactory;
_queryExpression = queryExpression;
_relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create(
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls));
new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parameterizedCollectionTranslationMode));
}

/// <summary>
Expand All @@ -49,7 +51,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);

Expand All @@ -69,7 +71,7 @@ public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(IReadOnly
{
if (!_memoryCache.TryGetValue(cacheKey, out relationalCommandTemplate))
{
var queryExpression = _relationalParameterBasedSqlProcessor.Optimize(
var queryExpression = _relationalParameterBasedSqlProcessor.Process(
_queryExpression, parameters, out var canCache);
relationalCommandTemplate = _querySqlGeneratorFactory.Create().GetCommand(queryExpression);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Loading