diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 750957cbdd8..38ceb4f0942 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -745,5 +745,5 @@ private static Expression MatchTypes(Expression expression, Type targetType) [UsedImplicitly] private static T GetParameterValue(QueryContext queryContext, string parameterName) - => (T)queryContext.ParameterValues[parameterName]!; + => (T)queryContext.Parameters[parameterName]!; } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs index 6e84ffa7827..2766b35a3c9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.PagingQueryingEnumerable.cs @@ -67,7 +67,7 @@ public PagingQueryingEnumerable( _cosmosContainer = rootEntityType.GetContainer() ?? throw new UnreachableException("Root entity type without a Cosmos container."); _cosmosPartitionKey = GeneratePartitionKey( - rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.Parameters); } public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -77,9 +77,9 @@ private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( (SelectExpression)new ParameterInliner( _sqlExpressionFactory, - _cosmosQueryContext.ParameterValues) + _cosmosQueryContext.Parameters) .Visit(_selectExpression), - _cosmosQueryContext.ParameterValues); + _cosmosQueryContext.Parameters); private sealed class AsyncEnumerator : IAsyncEnumerator> { @@ -135,11 +135,11 @@ public async ValueTask MoveNextAsync() _hasExecuted = true; - var maxItemCount = (int)_cosmosQueryContext.ParameterValues[_queryingEnumerable._maxItemCountParameterName]; + var maxItemCount = (int)_cosmosQueryContext.Parameters[_queryingEnumerable._maxItemCountParameterName]; var continuationToken = - (string)_cosmosQueryContext.ParameterValues[_queryingEnumerable._continuationTokenParameterName]; + (string)_cosmosQueryContext.Parameters[_queryingEnumerable._continuationTokenParameterName]; var responseContinuationTokenLimitInKb = (int?) - _cosmosQueryContext.ParameterValues[_queryingEnumerable._responseContinuationTokenLimitInKbParameterName]; + _cosmosQueryContext.Parameters[_queryingEnumerable._responseContinuationTokenLimitInKbParameterName]; var sqlQuery = _queryingEnumerable.GenerateQuery(); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 26c7d885cdc..ced9ff54222 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -57,7 +57,7 @@ public QueryingEnumerable( _cosmosContainer = rootEntityType.GetContainer() ?? throw new UnreachableException("Root entity type without a Cosmos container."); _cosmosPartitionKey = GeneratePartitionKey( - rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.Parameters); } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -73,9 +73,9 @@ private CosmosSqlQuery GenerateQuery() => _querySqlGeneratorFactory.Create().GetSqlQuery( (SelectExpression)new ParameterInliner( _sqlExpressionFactory, - _cosmosQueryContext.ParameterValues) + _cosmosQueryContext.Parameters) .Visit(_selectExpression), - _cosmosQueryContext.ParameterValues); + _cosmosQueryContext.Parameters); public string ToQueryString() { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index 9e833adbb3c..68843902984 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -53,7 +53,7 @@ public ReadItemQueryingEnumerable( _cosmosContainer = rootEntityType.GetContainer() ?? throw new UnreachableException("Root entity type without a Cosmos container."); _cosmosPartitionKey = GeneratePartitionKey( - rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.ParameterValues); + rootEntityType, partitionKeyPropertyValues, _cosmosQueryContext.Parameters); } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -83,7 +83,7 @@ private bool TryGetResourceId(out string resourceId) { var value = _readItemInfo.PropertyValues[property] switch { - SqlParameterExpression { Name: var parameterName } => _cosmosQueryContext.ParameterValues[parameterName], + SqlParameterExpression { Name: var parameterName } => _cosmosQueryContext.Parameters[parameterName], SqlConstantExpression { Value: var constantValue } => constantValue, _ => throw new UnreachableException() }; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index e46d7344f52..deda6fe2a3d 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -1179,7 +1179,7 @@ when memberInitExpression.Bindings.SingleOrDefault( private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { - var baseParameter = context.ParameterValues[baseParameterName]; + var baseParameter = context.Parameters[baseParameterName]; return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); } @@ -1188,7 +1188,7 @@ when memberInitExpression.Bindings.SingleOrDefault( string baseParameterName, IProperty property) { - if (context.ParameterValues[baseParameterName] is not IEnumerable baseListParameter) + if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter) { return null; } diff --git a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs index 9b60e2a1d3c..d11cf7f07f2 100644 --- a/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs +++ b/src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs @@ -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; } @@ -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 @@ -893,7 +893,7 @@ void GenerateCapturedVariableExtractors( }; code.AppendLine( - $"""queryContext.AddParameter("{evaluatableRootPaths.ParameterName}", {argumentsParameter});"""); + $"""queryContext.Parameters.Add("{evaluatableRootPaths.ParameterName}", {argumentsParameter});"""); break; } diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 4c451233f7f..9ccba1f9659 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -1216,7 +1216,7 @@ private static Expression ProcessSingleResultScalar( [UsedImplicitly] private static T GetParameterValue(QueryContext queryContext, string parameterName) - => (T)queryContext.ParameterValues[parameterName]!; + => (T)queryContext.Parameters[parameterName]!; private static bool IsConvertedToNullable(Expression result, Expression original) => result.Type.IsNullableType() @@ -1480,7 +1480,7 @@ when CanEvaluate(memberInitExpression): private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { - var baseParameter = context.ParameterValues[baseParameterName]; + var baseParameter = context.Parameters[baseParameterName]; return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); } @@ -1489,7 +1489,7 @@ when CanEvaluate(memberInitExpression): string baseParameterName, IProperty property) { - if (!(context.ParameterValues[baseParameterName] is IEnumerable baseListParameter)) + if (!(context.Parameters[baseParameterName] is IEnumerable baseListParameter)) { return null; } diff --git a/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs b/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs index 5723ef60360..5dab1a56b80 100644 --- a/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs +++ b/src/EFCore.Relational/Extensions/Internal/RelationalCommandResolverExtensions.cs @@ -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; diff --git a/src/EFCore.Relational/Query/CacheSafeParameterFacade.cs b/src/EFCore.Relational/Query/CacheSafeParameterFacade.cs new file mode 100644 index 00000000000..2fca19d9023 --- /dev/null +++ b/src/EFCore.Relational/Query/CacheSafeParameterFacade.cs @@ -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; + +/// +/// A facade over which provides cache-safe way to access parameters after the SQL cache. +/// +/// +/// 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. +/// +public sealed class CacheSafeParameterFacade(Dictionary parameters) +{ + /// + /// Returns whether the parameter with the given name is null. + /// + /// + /// The method assumes that the parameter with the given name exists in the dictionary, + /// and otherwise throws . + /// + public bool IsParameterNull(string parameterName) + => parameters.TryGetValue(parameterName, out var value) + ? value is null + : throw new UnreachableException($"Parameter with name '{parameterName}' does not exist."); + + /// + /// Returns the full dictionary of parameters, and disables caching for the generated SQL. + /// + public Dictionary GetParametersAndDisableSqlCaching() + { + CanCache = false; + + return parameters; + } + + /// + /// Whether the SQL generated using this facade can be cached, i.e. whether the full dictionary of parameters + /// has been accessed. + /// + public bool CanCache { get; private set; } = true; +} diff --git a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs index 3bdd96e16b3..f57f117b049 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs @@ -128,11 +128,11 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandResolver(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.Parameters) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, + _relationalQueryContext.Parameters, null, null, null, @@ -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, @@ -384,7 +384,7 @@ private static async Task 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, diff --git a/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs index 7caf9498228..af0eb423bff 100644 --- a/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/GroupBySingleQueryingEnumerable.cs @@ -139,11 +139,11 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandResolver(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.Parameters) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, + _relationalQueryContext.Parameters, null, null, null, CommandSource.LinqQuery), @@ -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, @@ -519,7 +519,7 @@ private static async Task 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, diff --git a/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs index 982b59dcdb6..e8b67872908 100644 --- a/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/GroupBySplitQueryingEnumerable.cs @@ -149,11 +149,11 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandResolver(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.Parameters) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, + _relationalQueryContext.Parameters, null, null, null, CommandSource.LinqQuery), @@ -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, @@ -510,7 +510,7 @@ private static async Task 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, diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs index 15435beed35..4d19a2b3eaa 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs @@ -70,7 +70,7 @@ public virtual IRelationalCommandTemplate GetRelationalCommandTemplate(Dictionar { if (!_memoryCache.TryGetValue(cacheKey, out relationalCommandTemplate)) { - var queryExpression = _relationalParameterBasedSqlProcessor.Optimize( + var queryExpression = _relationalParameterBasedSqlProcessor.Process( _queryExpression, parameters, out var canCache); relationalCommandTemplate = _querySqlGeneratorFactory.Create().GetCommand(queryExpression); diff --git a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs index 4c4f2c85122..b955ccae286 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs @@ -35,9 +35,8 @@ private readonly IDictionary _visitedFromSqlExpre private readonly Dictionary _sqlParameters = new(); - private IReadOnlyDictionary _parametersValues; + private CacheSafeParameterFacade _parametersFacade; private ParameterNameGenerator _parameterNameGenerator; - private bool _canCache; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -54,7 +53,7 @@ public RelationalParameterProcessor( _typeMappingSource = dependencies.TypeMappingSource; _parameterNameGeneratorFactory = dependencies.ParameterNameGeneratorFactory; _sqlGenerationHelper = dependencies.SqlGenerationHelper; - _parametersValues = default!; + _parametersFacade = default!; _parameterNameGenerator = default!; } @@ -69,20 +68,15 @@ public RelationalParameterProcessor( /// 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. /// - public virtual Expression Expand( - Expression queryExpression, - IReadOnlyDictionary parameterValues, - out bool canCache) + public virtual Expression Expand(Expression queryExpression, CacheSafeParameterFacade parametersFacade) { _visitedFromSqlExpressions.Clear(); _prefixedParameterNames.Clear(); _sqlParameters.Clear(); _parameterNameGenerator = _parameterNameGeneratorFactory.Create(); - _parametersValues = parameterValues; - _canCache = true; + _parametersFacade = parametersFacade; var result = Visit(queryExpression); - canCache = _canCache; return result; } @@ -146,8 +140,8 @@ private FromSqlExpression VisitFromSql(FromSqlExpression fromSql) { case QueryParameterExpression queryParameter: // parameter value will never be null. It could be empty object?[] - var parameterValues = (object?[])_parametersValues[queryParameter.Name]!; - _canCache = false; + var parameters = _parametersFacade.GetParametersAndDisableSqlCaching(); + var parameterValues = (object?[])parameters[queryParameter.Name]!; var subParameters = new List(parameterValues.Length); // ReSharper disable once ForCanBeConvertedToForeach diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index dc2831a8d5d..937410dd762 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -661,7 +661,7 @@ private ProjectionBindingExpression AddClientProjection(Expression expression, T /// public static T GetParameterValue(QueryContext queryContext, string parameterName) #pragma warning restore IDE0052 // Remove unread private members - => (T)queryContext.ParameterValues[parameterName]!; + => (T)queryContext.Parameters[parameterName]!; private sealed class IncludeFindingExpressionVisitor : ExpressionVisitor { diff --git a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs index d3037ae67f8..5a79eb34b9e 100644 --- a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs @@ -123,11 +123,11 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandResolver(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.Parameters) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, + _relationalQueryContext.Parameters, null, null, null, CommandSource.LinqQuery), @@ -266,7 +266,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, @@ -420,7 +420,7 @@ private static async Task 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, diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs index 5a8e99b477c..2a7140594ac 100644 --- a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs @@ -133,11 +133,11 @@ IEnumerator IEnumerable.GetEnumerator() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual DbCommand CreateDbCommand() - => _relationalCommandResolver(_relationalQueryContext.ParameterValues) + => _relationalCommandResolver(_relationalQueryContext.Parameters) .CreateDbCommand( new RelationalCommandParameterObject( _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, + _relationalQueryContext.Parameters, null, null, null, @@ -260,7 +260,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, @@ -408,7 +408,7 @@ private static async Task 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, diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs index 3bf2c39d8cc..990e718f952 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs @@ -41,23 +41,30 @@ public RelationalParameterBasedSqlProcessor( protected virtual RelationalParameterBasedSqlProcessorParameters Parameters { get; } /// - /// Optimizes the query expression for given parameter values. + /// 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. /// - /// A query expression to optimize. - /// A dictionary of parameter values to use. - /// A bool value indicating if the query expression can be cached. - /// An optimized query expression. - public virtual Expression Optimize( - Expression queryExpression, - Dictionary parametersValues, - out bool canCache) + [EntityFrameworkInternal] + public virtual Expression Process(Expression queryExpression, Dictionary parameters, out bool canCache) { - canCache = true; - queryExpression = ProcessSqlNullability(queryExpression, parametersValues, out var sqlNullabilityCanCache); - canCache &= sqlNullabilityCanCache; + var parametersFacade = new CacheSafeParameterFacade(parameters); + var result = Process(queryExpression, parametersFacade); + canCache = parametersFacade.CanCache; + + return result; + } - queryExpression = ExpandFromSqlParameter(queryExpression, parametersValues, out var fromSqlParameterCanCache); - canCache &= fromSqlParameterCanCache; + /// + /// Performs final query processing that takes parameter values into account. + /// + /// A query expression to process. + /// A facade allowing access to parameters in a cache-safe way. + public virtual Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) + { + queryExpression = ProcessSqlNullability(queryExpression, parametersFacade); + queryExpression = ExpandFromSqlParameter(queryExpression, parametersFacade); return queryExpression; } @@ -67,25 +74,31 @@ public virtual Expression Optimize( /// optimize it for given parameter values. /// /// A query expression to optimize. - /// A dictionary of parameter values to use. - /// A bool value indicating if the query expression can be cached. + /// A facade allowing access to parameters in a cache-safe way. /// A processed query expression. - protected virtual Expression ProcessSqlNullability( - Expression queryExpression, - Dictionary parametersValues, - out bool canCache) - => new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache); + protected virtual Expression ProcessSqlNullability(Expression queryExpression, CacheSafeParameterFacade parametersFacade) + => new SqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersFacade); /// /// Expands the parameters to inside the query expression for given parameter values. /// /// A query expression to optimize. + /// A facade allowing access to parameters in a cache-safe way. + /// A processed query expression. + protected virtual Expression ExpandFromSqlParameter(Expression queryExpression, CacheSafeParameterFacade parametersFacade) + => new RelationalParameterProcessor(Dependencies).Expand(queryExpression, parametersFacade); + + /// + /// Optimizes the query expression for given parameter values. + /// + /// A query expression to optimize. /// A dictionary of parameter values to use. /// A bool value indicating if the query expression can be cached. - /// A processed query expression. - protected virtual Expression ExpandFromSqlParameter( + /// An optimized query expression. + [Obsolete("Override Process() instead", error: true)] + public virtual Expression Optimize( Expression queryExpression, IReadOnlyDictionary parametersValues, out bool canCache) - => new RelationalParameterProcessor(Dependencies).Expand(queryExpression, parametersValues, out canCache); + => throw new UnreachableException(); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs index 51e2693b62d..308cf9489b1 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs @@ -560,7 +560,7 @@ protected virtual bool IsValidSelectExpressionForExecuteUpdate( List? complexPropertyChain, IProperty property) { - var baseValue = context.ParameterValues[baseParameterName]; + var baseValue = context.Parameters[baseParameterName]; if (complexPropertyChain is not null) { diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs index 664151585da..419e8e7bbea 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs @@ -447,7 +447,7 @@ static RelationalDataReader InitializeReader( return relationalCommand.ExecuteReader( new RelationalCommandParameterObject( queryContext.Connection, - queryContext.ParameterValues, + queryContext.Parameters, readerColumns, queryContext.Context, queryContext.CommandLogger, @@ -543,7 +543,7 @@ static async Task InitializeReaderAsync( return await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( queryContext.Connection, - queryContext.ParameterValues, + queryContext.Parameters, readerColumns, queryContext.Context, queryContext.CommandLogger, @@ -812,7 +812,7 @@ static RelationalDataReader InitializeReader( return relationalCommand.ExecuteReader( new RelationalCommandParameterObject( queryContext.Connection, - queryContext.ParameterValues, + queryContext.Parameters, readerColumns, queryContext.Context, queryContext.CommandLogger, @@ -903,7 +903,7 @@ static async Task InitializeReaderAsync( return await relationalCommand.ExecuteReaderAsync( new RelationalCommandParameterObject( queryContext.Connection, - queryContext.ParameterValues, + queryContext.Parameters, readerColumns, queryContext.Context, queryContext.CommandLogger, diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index bcd74192e88..aec6e3cc14d 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -147,7 +147,7 @@ public static int NonQueryResult( return relationalCommand.ExecuteNonQuery( new RelationalCommandParameterObject( state.relationalQueryContext.Connection, - state.relationalQueryContext.ParameterValues, + state.relationalQueryContext.Parameters, null, state.relationalQueryContext.Context, state.relationalQueryContext.CommandLogger, @@ -214,7 +214,7 @@ public static Task NonQueryResultAsync( return relationalCommand.ExecuteNonQueryAsync( new RelationalCommandParameterObject( state.relationalQueryContext.Connection, - state.relationalQueryContext.ParameterValues, + state.relationalQueryContext.Parameters, null, state.relationalQueryContext.Context, state.relationalQueryContext.CommandLogger, @@ -662,7 +662,7 @@ static object GenerateNonNullParameterValue(Type type) Expression GenerateRelationalCommandExpression(Dictionary parameters, out bool canCache) { - var queryExpression = _relationalParameterBasedSqlProcessor.Optimize(select, parameters, out canCache); + var queryExpression = _relationalParameterBasedSqlProcessor.Process(select, parameters, out canCache); if (!canCache) { return null!; diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 3a2fdd6b725..c8f4ec36332 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -2097,7 +2097,7 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == compl List? complexPropertyChain, IProperty property) { - var baseValue = context.ParameterValues[baseParameterName]; + var baseValue = context.Parameters[baseParameterName]; if (complexPropertyChain is not null) { @@ -2127,7 +2127,7 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == compl string baseParameterName, IProperty property) { - if (context.ParameterValues[baseParameterName] is not IEnumerable baseListParameter) + if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter) { return null; } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index cb6690997fb..48c1602fbaa 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -24,7 +24,7 @@ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typ /// /// Creates a new instance of the class. /// - /// The name of the parameter as it is recorded in . + /// The name of the parameter as it is recorded in . /// /// The name of the parameter as it will be set on and inside the SQL as a placeholder /// (before any additional placeholder character prefixing). @@ -49,7 +49,7 @@ public SqlParameterExpression( } /// - /// The name of the parameter as it is recorded in . + /// The name of the parameter as it is recorded in . /// public string InvariantName { get; } diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 062a79f1bea..1f5cb874be2 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -23,11 +23,11 @@ public class SqlNullabilityProcessor : ExpressionVisitor private readonly List _nonNullableColumns; private readonly List _nullValueColumns; private readonly ISqlExpressionFactory _sqlExpressionFactory; + /// /// Tracks parameters for collection expansion, allowing reuse. /// private readonly Dictionary> _collectionParameterExpansionMap; - private bool _canCache; /// /// Creates a new instance of the class. @@ -46,7 +46,7 @@ public SqlNullabilityProcessor( _nonNullableColumns = []; _nullValueColumns = []; _collectionParameterExpansionMap = []; - ParameterValues = null!; + ParametersFacade = null!; } /// @@ -67,39 +67,26 @@ public SqlNullabilityProcessor( /// /// Dictionary of current parameter values in use. /// - protected virtual Dictionary ParameterValues { get; private set; } + protected virtual CacheSafeParameterFacade ParametersFacade { get; private set; } /// /// Processes a query expression to apply null semantics and optimize it. /// /// A query expression to process. - /// A dictionary of parameter values in use. - /// A bool value indicating whether the query expression can be cached. + /// A facade allowing access to parameters in a cache-safe way. /// An optimized query expression. - public virtual Expression Process( - Expression queryExpression, - Dictionary parameterValues, - out bool canCache) + public virtual Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) { - _canCache = true; _nonNullableColumns.Clear(); _nullValueColumns.Clear(); _collectionParameterExpansionMap.Clear(); - ParameterValues = parameterValues; + ParametersFacade = parametersFacade; var result = Visit(queryExpression); - canCache = _canCache; - return result; } - /// - /// Marks the select expression being processed as cannot be cached. - /// - protected virtual void DoNotCache() - => _canCache = false; - /// /// Adds a column to non nullable columns list to further optimizations can take the column as non-nullable. /// @@ -128,11 +115,11 @@ protected override Expression VisitExtension(Expression node) case ValuesExpression { ValuesParameter: SqlParameterExpression valuesParameter } valuesExpression: { - DoNotCache(); Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); var elementTypeMapping = (RelationalTypeMapping)valuesParameter.TypeMapping.ElementTypeMapping; - var values = ((IEnumerable?)ParameterValues[valuesParameter.Name])?.Cast().ToList() ?? []; + var queryParameters = ParametersFacade.GetParametersAndDisableSqlCaching(); + var values = ((IEnumerable?)queryParameters[valuesParameter.Name])?.Cast().ToList() ?? []; var intTypeMapping = (IntTypeMapping?)Dependencies.TypeMappingSource.FindMapping(typeof(int)); Check.DebugAssert(intTypeMapping is not null); @@ -152,8 +139,8 @@ protected override Expression VisitExtension(Expression node) // otherwise reuse it. if (expandedParameters.Count <= i) { - var parameterName = Uniquifier.Uniquify(valuesParameter.Name, ParameterValues, int.MaxValue); - ParameterValues.Add(parameterName, values[i]); + var parameterName = Uniquifier.Uniquify(valuesParameter.Name, queryParameters, int.MaxValue); + queryParameters.Add(parameterName, values[i]); var parameterExpression = new SqlParameterExpression(parameterName, values[i]?.GetType() ?? typeof(object), elementTypeMapping); expandedParameters.Add(parameterExpression); } @@ -624,7 +611,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt case (false, true): { - // If the item is non-nullable but the projection is nullable, NULL will only be returned if the item wasn't found + // If the item is non-nullable but the subquery projection is nullable, NULL will only be returned if the item wasn't found // (as with the above case). // Use as-is in optimized expansion (NULL is interpreted as false anyway), or compensate by coalescing NULL to false: // WHERE NonNullable IN (SELECT Nullable FROM foo) -> WHERE COALESCE(NonNullable IN (SELECT Nullable FROM foo), false) @@ -827,9 +814,9 @@ InExpression ProcessInExpressionValues( { // The InExpression has a values parameter. Expand it out, embedding its values as constants into the SQL; disable SQL // caching. - DoNotCache(); var elementTypeMapping = (RelationalTypeMapping)inExpression.ValuesParameter.TypeMapping!.ElementTypeMapping!; - var values = ((IEnumerable?)ParameterValues[valuesParameter.Name])?.Cast().ToList() ?? []; + var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); + var values = ((IEnumerable?)parameters[valuesParameter.Name])?.Cast().ToList() ?? []; processedValues = []; @@ -861,8 +848,8 @@ ParameterizedCollectionMode is ParameterizedCollectionMode.Constants // otherwise reuse it. if (expandedParameters.Count <= i) { - var parameterName = Uniquifier.Uniquify(valuesParameter.Name, ParameterValues, int.MaxValue); - ParameterValues.Add(parameterName, values[i]); + var parameterName = Uniquifier.Uniquify(valuesParameter.Name, parameters, int.MaxValue); + parameters.Add(parameterName, values[i]); var parameterExpression = new SqlParameterExpression(parameterName, values[i]?.GetType() ?? typeof(object), elementTypeMapping); expandedParameters.Add(parameterExpression); } @@ -1423,28 +1410,24 @@ protected virtual SqlExpression VisitSqlParameter( bool allowOptimizedExpansion, out bool nullable) { - if (!ParameterValues.TryGetValue(sqlParameterExpression.Name, out var parameterValue)) + if (ParametersFacade.IsParameterNull(sqlParameterExpression.Name)) { - throw new UnreachableException( - $"Encountered SqlParameter with name '{sqlParameterExpression.Name}', but such a parameter does not exist."); - } + nullable = true; - nullable = parameterValue == null; - - if (nullable) - { return _sqlExpressionFactory.Constant( null, sqlParameterExpression.Type, sqlParameterExpression.TypeMapping); } + nullable = false; + if (sqlParameterExpression.ShouldBeConstantized) { - DoNotCache(); + var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); return _sqlExpressionFactory.Constant( - parameterValue, + parameters[sqlParameterExpression.Name], sqlParameterExpression.Type, sensitive: true, sqlParameterExpression.TypeMapping); @@ -1522,7 +1505,7 @@ protected virtual bool PreferExistsToInWithCoalesce // Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool. private bool IsNull(SqlExpression? expression) => expression is SqlConstantExpression { Value: null } - || expression is SqlParameterExpression { Name: string parameterName } && ParameterValues[parameterName] is null; + || expression is SqlParameterExpression { Name: string parameterName } && ParametersFacade.IsParameterNull(parameterName); private bool IsTrue(SqlExpression? expression) => expression is SqlConstantExpression { Value: true }; @@ -1820,11 +1803,14 @@ protected virtual bool TryMakeNonNullable( } && projectedColumn.TableAlias == collectionTable.Alias && IsCollectionTable(collectionTable, out var collection) - && collection is SqlParameterExpression collectionParameter - && ParameterValues[collectionParameter.Name] is IList values) + && collection is SqlParameterExpression collectionParameter) { - // We're looking at a parameter beyond its simple nullability, so we can't use the 2nd-level cache for this query. - DoNotCache(); + // We're looking at a parameter beyond its simple nullability, so we can't use the SQL cache for this query. + var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); + if (parameters[collectionParameter.Name] is not IList values) + { + throw new UnreachableException($"Parameter '{collectionParameter.Name}' is not an IList."); + } IList? processedValues = null; @@ -1864,7 +1850,7 @@ protected virtual bool TryMakeNonNullable( var rewrittenParameter = new SqlParameterExpression( collectionParameter.Name + "_without_nulls", collectionParameter.Type, collectionParameter.TypeMapping); - ParameterValues[rewrittenParameter.Name] = processedValues; + parameters[rewrittenParameter.Name] = processedValues; var rewrittenCollectionTable = UpdateParameterCollection(collectionTable, rewrittenParameter); // We clone the select expression since Update below doesn't create a pure copy, mutating the original as well (because of @@ -1946,7 +1932,7 @@ private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool opera // not_null_value_parameter is null -> false // not_null_value_parameter is not null -> true return _sqlExpressionFactory.Constant( - ParameterValues[sqlParameterOperand.Name] == null ^ sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, + ParametersFacade.IsParameterNull(sqlParameterOperand.Name) ^ sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, sqlUnaryExpression.TypeMapping); case ColumnExpression columnOperand diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs index c932ab16655..ed57e0fee82 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs @@ -36,31 +36,21 @@ public SqlServerParameterBasedSqlProcessor( /// 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. /// - public override Expression Optimize( - Expression queryExpression, - Dictionary parametersValues, - out bool canCache) + public override Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) { - var optimizedQueryExpression = new SkipTakeCollapsingExpressionVisitor(Dependencies.SqlExpressionFactory) - .Process(queryExpression, parametersValues, out var canCache2); + var afterZeroLimitConversion = new SqlServerZeroLimitConverter(Dependencies.SqlExpressionFactory) + .Process(queryExpression, parametersFacade); - optimizedQueryExpression = base.Optimize(optimizedQueryExpression, parametersValues, out canCache); + var afterBaseProcessing = base.Process(afterZeroLimitConversion, parametersFacade); - canCache &= canCache2; + var afterSearchConditionConversion = new SearchConditionConverter(Dependencies.SqlExpressionFactory) + .Visit(afterBaseProcessing); - return new SearchConditionConverter(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression); + return afterSearchConditionConversion; } /// - protected override Expression ProcessSqlNullability( - Expression selectExpression, - Dictionary parametersValues, - out bool canCache) - { - Check.NotNull(selectExpression); - Check.NotNull(parametersValues); - - return new SqlServerSqlNullabilityProcessor(Dependencies, Parameters, _sqlServerSingletonOptions).Process( - selectExpression, parametersValues, out canCache); - } + protected override Expression ProcessSqlNullability(Expression selectExpression, CacheSafeParameterFacade parametersFacade) + => new SqlServerSqlNullabilityProcessor(Dependencies, Parameters, _sqlServerSingletonOptions).Process( + selectExpression, parametersFacade); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 0fb3d4f51e6..3527665d143 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -53,9 +53,9 @@ public SqlServerSqlNullabilityProcessor( /// 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. /// - public override Expression Process(Expression queryExpression, Dictionary parameterValues, out bool canCache) + public override Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) { - var result = base.Process(queryExpression, parameterValues, out canCache); + var result = base.Process(queryExpression, parametersFacade); _openJsonAliasCounter = 0; return result; } @@ -291,8 +291,9 @@ private bool TryHandleOverLimitParameters( out List? constantsResult, out bool? containsNulls) { - DoNotCache(); - var values = ((IEnumerable?)ParameterValues[valuesParameter.Name])?.Cast().ToList() ?? []; + var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); + var values = ((IEnumerable?)parameters[valuesParameter.Name])?.Cast().ToList() ?? []; + // SQL Server has limit on number of parameters in a query. // If we're over that limit, we switch to using single parameter // and processing it through JSON functions. diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 8e4edd47f06..8b1abfc068d 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -482,7 +482,7 @@ SqlExpression CharIndexGreaterThanZero() QueryContext queryContext, string baseParameterName, StartsEndsWithContains methodType) - => queryContext.ParameterValues[baseParameterName] switch + => queryContext.Parameters[baseParameterName] switch { null => null, diff --git a/src/EFCore.SqlServer/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerZeroLimitConverter.cs similarity index 68% rename from src/EFCore.SqlServer/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs rename to src/EFCore.SqlServer/Query/Internal/SqlServerZeroLimitConverter.cs index 00ecaab9ee9..f4eea232d21 100644 --- a/src/EFCore.SqlServer/Query/Internal/SkipTakeCollapsingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerZeroLimitConverter.cs @@ -11,12 +11,11 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.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. /// -public class SkipTakeCollapsingExpressionVisitor : ExpressionVisitor +public class SqlServerZeroLimitConverter : ExpressionVisitor { private readonly ISqlExpressionFactory _sqlExpressionFactory; - private IReadOnlyDictionary _parameterValues; - private bool _canCache; + private CacheSafeParameterFacade _parametersFacade; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,10 +23,10 @@ public class SkipTakeCollapsingExpressionVisitor : ExpressionVisitor /// 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. /// - public SkipTakeCollapsingExpressionVisitor(ISqlExpressionFactory sqlExpressionFactory) + public SqlServerZeroLimitConverter(ISqlExpressionFactory sqlExpressionFactory) { _sqlExpressionFactory = sqlExpressionFactory; - _parameterValues = null!; + _parametersFacade = null!; } /// @@ -36,19 +35,11 @@ public SkipTakeCollapsingExpressionVisitor(ISqlExpressionFactory sqlExpressionFa /// 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. /// - public virtual Expression Process( - Expression queryExpression, - IReadOnlyDictionary parametersValues, - out bool canCache) + public virtual Expression Process(Expression queryExpression, CacheSafeParameterFacade parametersFacade) { - _parameterValues = parametersValues; - _canCache = true; + _parametersFacade = parametersFacade; - var result = Visit(queryExpression); - - canCache = _canCache; - - return result; + return Visit(queryExpression); } /// @@ -59,11 +50,11 @@ public virtual Expression Process( /// protected override Expression VisitExtension(Expression extensionExpression) { + // SQL Server doesn't support 0 in the FETCH NEXT x ROWS ONLY clause. We use this clause when translating LINQ Take(), but + // only if there's also a Skip(), otherwise we translate to SQL TOP(x), which does allow 0. + // Check for this case, and replace with a false predicate (since no rows should be returned). if (extensionExpression is SelectExpression { Offset: not null, Limit: not null } selectExpression) { - // SQL Server doesn't support 0 in the FETCH NEXT x ROWS ONLY clause. We use this clause when translating LINQ Take(), but - // only if there's also a Skip(), otherwise we translate to SQL TOP(x), which does allow 0. - // Check for this case, and replace with a false predicate (since no rows should be returned). if (IsZero(selectExpression.Limit)) { return selectExpression.Update( @@ -72,25 +63,18 @@ protected override Expression VisitExtension(Expression extensionExpression) selectExpression.GroupBy, selectExpression.GroupBy.Count > 0 ? _sqlExpressionFactory.Constant(false) : null, selectExpression.Projection, - new List(0), + orderings: [], offset: null, limit: null); } bool IsZero(SqlExpression? sqlExpression) - { - switch (sqlExpression) + => sqlExpression switch { - case SqlConstantExpression { Value: int intValue }: - return intValue == 0; - case SqlParameterExpression parameter: - _canCache = false; - return _parameterValues[parameter.Name] is 0; - - default: - return false; - } - } + SqlConstantExpression { Value: int i } => i == 0, + SqlParameterExpression p => _parametersFacade.GetParametersAndDisableSqlCaching()[p.Name] is 0, + _ => false + }; } return base.VisitExtension(extensionExpression); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs index 3e62bf8e9a4..5833215b9c0 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteParameterBasedSqlProcessor.cs @@ -30,9 +30,6 @@ public SqliteParameterBasedSqlProcessor( /// 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. /// - protected override Expression ProcessSqlNullability( - Expression queryExpression, - Dictionary parametersValues, - out bool canCache) - => new SqliteSqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersValues, out canCache); + protected override Expression ProcessSqlNullability(Expression queryExpression, CacheSafeParameterFacade parametersFacade) + => new SqliteSqlNullabilityProcessor(Dependencies, Parameters).Process(queryExpression, parametersFacade); } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs index 0d04114979f..ea89ab030fe 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs @@ -448,7 +448,7 @@ bool TryTranslateStartsEndsWith( QueryContext queryContext, string baseParameterName, bool startsWith) - => queryContext.ParameterValues[baseParameterName] switch + => queryContext.Parameters[baseParameterName] switch { null => null, diff --git a/src/EFCore/Query/Internal/CompiledQueryBase.cs b/src/EFCore/Query/Internal/CompiledQueryBase.cs index 609f5f0e50b..a7c8c478d3f 100644 --- a/src/EFCore/Query/Internal/CompiledQueryBase.cs +++ b/src/EFCore/Query/Internal/CompiledQueryBase.cs @@ -67,9 +67,10 @@ protected virtual TResult ExecuteCore( queryContext.CancellationToken = cancellationToken; + var queryParameters = queryContext.Parameters; for (var i = 0; i < parameters.Length; i++) { - queryContext.AddParameter(_queryExpression.Parameters[i + 1].Name!, parameters[i]); + queryParameters.Add(_queryExpression.Parameters[i + 1].Name!, parameters[i]); } return _executor.Executor(queryContext); diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 3319a97ef4f..44fdec7dbc0 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -96,7 +96,7 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor private IQueryProvider? _currentQueryProvider; private State _state; - private IParameterValues _parameterValues = null!; + private Dictionary _parameters = null!; private readonly IModel _model; private readonly ContextParameterReplacer _contextParameterReplacer; @@ -154,10 +154,10 @@ public ExpressionTreeFuncletizer( /// public virtual Expression ExtractParameters( Expression expression, - IParameterValues parameterValues, + Dictionary parameters, bool parameterize, bool clearParameterizedValues) - => ExtractParameters(expression, parameterValues, parameterize, clearParameterizedValues, precompiledQuery: false); + => ExtractParameters(expression, parameters, parameterize, clearParameterizedValues, precompiledQuery: false); /// /// Processes an expression tree, extracting parameters and evaluating evaluatable fragments as part of the pass. @@ -172,13 +172,13 @@ public virtual Expression ExtractParameters( [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public virtual Expression ExtractParameters( Expression expression, - IParameterValues parameterValues, + Dictionary parameters, bool parameterize, bool clearParameterizedValues, bool precompiledQuery) { Reset(clearParameterizedValues); - _parameterValues = parameterValues; + _parameters = parameters; _parameterize = parameterize; _calculatingPath = false; _precompiledQuery = precompiledQuery; @@ -210,7 +210,7 @@ public virtual void ResetPathCalculation() // In precompilation mode we don't actually extract parameter values; but we do need to generate the parameter names, using the // same logic (and via the same code) used in parameter extraction, and that logic requires _parameterValues. - _parameterValues = new DummyParameterValues(); + _parameters = new Dictionary(); } /// @@ -1955,7 +1955,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state }; // We still maintain _parameterValues since later parameter names are generated based on already-populated names. - _parameterValues.AddParameter(parameterName, null); + _parameters.Add(parameterName, null); return evaluatableRoot; } @@ -1972,7 +1972,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state && !evaluatableRoot.Type.IsValueType && evaluatableRoot is MemberExpression { Member: IParameterNullabilityInfo { IsNonNullableReferenceType: true } }; - _parameterValues.AddParameter(parameterName, value); + _parameters.Add(parameterName, value); return _parameterizedValues[evaluatableRoot] = new QueryParameterExpression( parameterName, @@ -2330,15 +2330,4 @@ private sealed class ContextParameterReplacer(Type contextType) : ExpressionVisi ? ContextParameterExpression : base.Visit(expression); } - - private sealed class DummyParameterValues : IParameterValues - { - private readonly Dictionary _parameterValues = new(); - - public Dictionary ParameterValues - => _parameterValues; - - public void AddParameter(string name, object? value) - => _parameterValues.Add(name, value); - } } diff --git a/src/EFCore/Query/Internal/IParameterValues.cs b/src/EFCore/Query/Internal/IParameterValues.cs deleted file mode 100644 index ec5a3eeb338..00000000000 --- a/src/EFCore/Query/Internal/IParameterValues.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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.Query.Internal; - -/// -/// 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. -/// -public interface IParameterValues -{ - /// - /// 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. - /// - Dictionary ParameterValues { get; } - - /// - /// 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. - /// - void AddParameter(string name, object? value); -} diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 48211e9b303..5f9baa1a5c7 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -56,7 +56,7 @@ private static readonly PropertyInfo QueryContextContextPropertyInfo private readonly Dictionary _parameterizedQueryFilterPredicateCache = []; - private readonly Parameters _parameters = new(); + private readonly Dictionary _parameters = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -168,7 +168,7 @@ public virtual Expression Expand(Expression query) QueryContextContextPropertyInfo), _queryCompilationContext.ContextType); - foreach (var (key, value) in _parameters.ParameterValues) + foreach (var (key, value) in _parameters) { var lambda = (LambdaExpression)value!; var remappedLambdaBody = ReplacingExpressionVisitor.Replace( @@ -2281,35 +2281,15 @@ private static Expression SnapshotExpression(Expression selector) } private static EntityReference? UnwrapEntityReference(Expression? expression) - { - switch (expression) + => expression switch { - case EntityReference entityReference: - return entityReference; - - case NavigationTreeExpression navigationTreeExpression: - return UnwrapEntityReference(navigationTreeExpression.Value); - - case NavigationExpansionExpression navigationExpansionExpression - when navigationExpansionExpression.CardinalityReducingGenericMethodInfo != null: - return UnwrapEntityReference(navigationExpansionExpression.PendingSelector); - - case OwnedNavigationReference ownedNavigationReference: - return ownedNavigationReference.EntityReference; + EntityReference entityReference => entityReference, + NavigationTreeExpression navigationTreeExpression => UnwrapEntityReference(navigationTreeExpression.Value), + NavigationExpansionExpression navigationExpansionExpression + when navigationExpansionExpression.CardinalityReducingGenericMethodInfo is not null + => UnwrapEntityReference(navigationExpansionExpression.PendingSelector), + OwnedNavigationReference ownedNavigationReference => ownedNavigationReference.EntityReference, - default: - return null; - } - } - - private sealed class Parameters : IParameterValues - { - private readonly Dictionary _parameterValues = new Dictionary(); - - public Dictionary ParameterValues - => _parameterValues; - - public void AddParameter(string name, object? value) - => _parameterValues.Add(name, value); - } + _ => null, + }; } diff --git a/src/EFCore/Query/Internal/QueryCompiler.cs b/src/EFCore/Query/Internal/QueryCompiler.cs index 950c7bebde3..4476f54b6f5 100644 --- a/src/EFCore/Query/Internal/QueryCompiler.cs +++ b/src/EFCore/Query/Internal/QueryCompiler.cs @@ -74,7 +74,7 @@ private TResult ExecuteCore(Expression query, bool async, CancellationT queryContext.CancellationToken = cancellationToken; - var queryAfterExtraction = ExtractParameters(query, queryContext, _logger); + var queryAfterExtraction = ExtractParameters(query, queryContext.Parameters, _logger); var compiledQuery = _compiledQueryCache @@ -95,7 +95,9 @@ var compiledQuery /// public virtual Func CreateCompiledQuery(Expression query) { - var queryAfterExtraction = ExtractParameters(query, _queryContextFactory.Create(), _logger, compiledQuery: true); + var queryContext = _queryContextFactory.Create(); + + var queryAfterExtraction = ExtractParameters(query, queryContext.Parameters, _logger, compiledQuery: true); return CompileQueryCore(_database, queryAfterExtraction, _model, false); } @@ -108,7 +110,9 @@ public virtual Func CreateCompiledQuery(Expressi /// public virtual Func CreateCompiledAsyncQuery(Expression query) { - var queryAfterExtraction = ExtractParameters(query, _queryContextFactory.Create(), _logger, compiledQuery: true); + var queryContext = _queryContextFactory.Create(); + + var queryAfterExtraction = ExtractParameters(query, queryContext.Parameters, _logger, compiledQuery: true); return CompileQueryCore(_database, queryAfterExtraction, _model, true); } @@ -135,9 +139,10 @@ public virtual Func CompileQueryCore( [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] public virtual Expression> PrecompileQuery(Expression query, bool async) { + var queryContext = _queryContextFactory.Create(); + query = new ExpressionTreeFuncletizer(_model, _evaluatableExpressionFilter, _contextType, generateContextAccessors: false, _logger) - .ExtractParameters( - query, _queryContextFactory.Create(), parameterize: true, clearParameterizedValues: true, precompiledQuery: true); + .ExtractParameters(query, queryContext.Parameters, parameterize: true, clearParameterizedValues: true, precompiledQuery: true); return _database.CompileQueryExpression(query, async); } @@ -150,10 +155,10 @@ public virtual Expression> PrecompileQuery( /// public virtual Expression ExtractParameters( Expression query, - IParameterValues parameterValues, + Dictionary parameters, IDiagnosticsLogger logger, bool compiledQuery = false, bool generateContextAccessors = false) => new ExpressionTreeFuncletizer(_model, _evaluatableExpressionFilter, _contextType, generateContextAccessors: false, logger) - .ExtractParameters(query, parameterValues, parameterize: !compiledQuery, clearParameterizedValues: true); + .ExtractParameters(query, parameters, parameterize: !compiledQuery, clearParameterizedValues: true); } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 177ce563251..88545f5ee40 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -258,14 +258,19 @@ private Expression InsertRuntimeParameters(Expression query) .Select( kv => Expression.Call( - QueryContextParameter, - QueryContextAddParameterMethodInfo, + Expression.Property( + QueryContextParameter, + QueryContextParametersProperty), + ParameterDictionaryAddMethod, Expression.Constant(kv.Key), Expression.Convert(Expression.Invoke(kv.Value, QueryContextParameter), typeof(object)))) .Append(query)); - private static readonly MethodInfo QueryContextAddParameterMethodInfo - = typeof(QueryContext).GetTypeInfo().GetDeclaredMethod(nameof(QueryContext.AddParameter))!; + private static readonly PropertyInfo QueryContextParametersProperty + = typeof(QueryContext).GetProperty(nameof(QueryContext.Parameters))!; + + private static readonly MethodInfo ParameterDictionaryAddMethod + = typeof(Dictionary).GetMethod(nameof(Dictionary.Add))!; [DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] private sealed class NotTranslatedExpressionType : Expression, IPrintableExpression diff --git a/src/EFCore/Query/QueryContext.cs b/src/EFCore/Query/QueryContext.cs index 87724f24890..f66888cff33 100644 --- a/src/EFCore/Query/QueryContext.cs +++ b/src/EFCore/Query/QueryContext.cs @@ -19,9 +19,8 @@ namespace Microsoft.EntityFrameworkCore.Query; /// See Implementation of database providers and extensions /// and How EF Core queries work for more information and examples. /// -public abstract class QueryContext : IParameterValues +public abstract class QueryContext { - private readonly Dictionary _parameterValues = new Dictionary(); private IStateManager? _stateManager; /// @@ -41,10 +40,15 @@ protected QueryContext(QueryContextDependencies dependencies) } /// - /// The current DbContext in using while executing the query. + /// The current in using while executing the query. /// public virtual DbContext Context { get; } + /// + /// The query parameter used in the query query. + /// + public virtual Dictionary Parameters { get; } = new(); + /// /// Dependencies for this service. /// @@ -94,20 +98,6 @@ public virtual IDiagnosticsLogger CommandLogg public virtual IDiagnosticsLogger QueryLogger => Dependencies.QueryLogger; - /// - /// The parameter values to use while executing the query. - /// - public virtual Dictionary ParameterValues - => _parameterValues; - - /// - /// Adds a parameter to for this query. - /// - /// The name. - /// The value. - public virtual void AddParameter(string name, object? value) - => _parameterValues.Add(name, value); - /// /// Initializes the to be used with this QueryContext. ///