diff --git a/All.sln.DotSettings b/All.sln.DotSettings index a3684295d00..ea433c87d71 100644 --- a/All.sln.DotSettings +++ b/All.sln.DotSettings @@ -199,6 +199,7 @@ Licensed under the Apache License, Version 2.0. See License.txt in the project r True True True + True True True True diff --git a/src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs b/src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs new file mode 100644 index 00000000000..ea4cdf05e98 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +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 class CollectionMaterializationContext + { + public CollectionMaterializationContext(object parent, object collection, object[] parentIdentifier, object[] outerIdentifier) + { + Parent = parent; + Collection = collection; + ParentIdentifier = parentIdentifier; + OuterIdentifier = outerIdentifier; + ResultContext = new ResultContext(); + } + + public virtual ResultContext ResultContext { get; } + public virtual object Parent { get; } + public virtual object Collection { get; } + public virtual object[] ParentIdentifier { get; } + public virtual object[] OuterIdentifier { get; } + public virtual object[] SelfIdentifier { get; private set; } + + public virtual void UpdateSelfIdentifier(object[] selfIdentifier) + { + SelfIdentifier = selfIdentifier; + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/ParameterValueBasedSelectExpressionOptimizer.cs b/src/EFCore.Relational/Query/Internal/ParameterValueBasedSelectExpressionOptimizer.cs new file mode 100644 index 00000000000..2763811d2d9 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/ParameterValueBasedSelectExpressionOptimizer.cs @@ -0,0 +1,356 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +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 class ParameterValueBasedSelectExpressionOptimizer + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + private readonly bool _useRelationalNulls; + + public ParameterValueBasedSelectExpressionOptimizer( + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, + bool useRelationalNulls) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; + _useRelationalNulls = useRelationalNulls; + } + + public virtual (SelectExpression selectExpression, bool canCache) Optimize( + SelectExpression selectExpression, IReadOnlyDictionary parametersValues) + { + var canCache = true; + + var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor( + _sqlExpressionFactory, parametersValues).Visit(selectExpression); + + if (!ReferenceEquals(selectExpression, inExpressionOptimized)) + { + canCache = false; + } + + var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( + _sqlExpressionFactory, _useRelationalNulls, parametersValues).Visit(inExpressionOptimized); + + var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor( + _sqlExpressionFactory, + _parameterNameGeneratorFactory.Create(), + parametersValues).Visit(nullParametersOptimized); + + if (!ReferenceEquals(nullParametersOptimized, fromSqlParameterOptimized)) + { + canCache = false; + } + + return (selectExpression: (SelectExpression)fromSqlParameterOptimized, canCache); + } + + private class ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor : SqlExpressionOptimizingExpressionVisitor + { + private readonly IReadOnlyDictionary _parametersValues; + + public ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( + ISqlExpressionFactory sqlExpressionFactory, + bool useRelationalNulls, + IReadOnlyDictionary parametersValues) + : base(sqlExpressionFactory, useRelationalNulls) + { + _parametersValues = parametersValues; + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is SelectExpression selectExpression) + { + var newSelectExpression = (SelectExpression)base.VisitExtension(extensionExpression); + + // if predicate is optimized to true, we can simply remove it + var newPredicate = newSelectExpression.Predicate is SqlConstantExpression newSelectPredicateConstant + && !(selectExpression.Predicate is SqlConstantExpression) + ? (bool)newSelectPredicateConstant.Value + ? null + : SqlExpressionFactory.Equal( + newSelectPredicateConstant, + SqlExpressionFactory.Constant(true, newSelectPredicateConstant.TypeMapping)) + : newSelectExpression.Predicate; + + var newHaving = newSelectExpression.Having is SqlConstantExpression newSelectHavingConstant + && !(selectExpression.Having is SqlConstantExpression) + ? (bool)newSelectHavingConstant.Value + ? null + : SqlExpressionFactory.Equal( + newSelectHavingConstant, + SqlExpressionFactory.Constant(true, newSelectHavingConstant.TypeMapping)) + : newSelectExpression.Having; + + return !ReferenceEquals(newPredicate, newSelectExpression.Predicate) + || !ReferenceEquals(newHaving, newSelectExpression.Having) + ? newSelectExpression.Update( + newSelectExpression.Projection.ToList(), + newSelectExpression.Tables.ToList(), + newPredicate, + newSelectExpression.GroupBy.ToList(), + newHaving, + newSelectExpression.Orderings.ToList(), + newSelectExpression.Limit, + newSelectExpression.Offset, + newSelectExpression.IsDistinct, + newSelectExpression.Alias) + : newSelectExpression; + } + + return base.VisitExtension(extensionExpression); + } + + protected override Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression) + { + var result = base.VisitSqlUnaryExpression(sqlUnaryExpression); + if (result is SqlUnaryExpression newUnaryExpression + && newUnaryExpression.Operand is SqlParameterExpression parameterOperand) + { + var parameterValue = _parametersValues[parameterOperand.Name]; + if (sqlUnaryExpression.OperatorType == ExpressionType.Equal) + { + return SqlExpressionFactory.Constant(parameterValue == null, sqlUnaryExpression.TypeMapping); + } + + if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) + { + return SqlExpressionFactory.Constant(parameterValue != null, sqlUnaryExpression.TypeMapping); + } + } + + return result; + } + } + + private class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IReadOnlyDictionary _parametersValues; + + public InExpressionValuesExpandingExpressionVisitor( + ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is InExpression inExpression + && inExpression.Values != null) + { + var inValues = new List(); + var hasNullValue = false; + RelationalTypeMapping typeMapping = null; + + switch (inExpression.Values) + { + case SqlConstantExpression sqlConstant: + { + typeMapping = sqlConstant.TypeMapping; + var values = (IEnumerable)sqlConstant.Value; + foreach (var value in values) + { + if (value == null) + { + hasNullValue = true; + continue; + } + + inValues.Add(value); + } + + break; + } + + case SqlParameterExpression sqlParameter: + { + typeMapping = sqlParameter.TypeMapping; + var values = (IEnumerable)_parametersValues[sqlParameter.Name]; + foreach (var value in values) + { + if (value == null) + { + hasNullValue = true; + continue; + } + + inValues.Add(value); + } + + break; + } + } + + var updatedInExpression = inValues.Count > 0 + ? _sqlExpressionFactory.In( + (SqlExpression)Visit(inExpression.Item), + _sqlExpressionFactory.Constant(inValues, typeMapping), + inExpression.IsNegated) + : null; + + var nullCheckExpression = hasNullValue + ? inExpression.IsNegated + ? _sqlExpressionFactory.IsNotNull(inExpression.Item) + : _sqlExpressionFactory.IsNull(inExpression.Item) + : null; + + if (updatedInExpression != null + && nullCheckExpression != null) + { + return inExpression.IsNegated + ? _sqlExpressionFactory.AndAlso(updatedInExpression, nullCheckExpression) + : _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); + } + + if (updatedInExpression == null + && nullCheckExpression == null) + { + return _sqlExpressionFactory.Equal( + _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(inExpression.IsNegated)); + } + + return (SqlExpression)updatedInExpression ?? nullCheckExpression; + } + + return base.Visit(expression); + } + } + + private class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor + { + private readonly IDictionary _visitedFromSqlExpressions + = new Dictionary(ReferenceEqualityComparer.Instance); + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ParameterNameGenerator _parameterNameGenerator; + private readonly IReadOnlyDictionary _parametersValues; + + public FromSqlParameterApplyingExpressionVisitor( + ISqlExpressionFactory sqlExpressionFactory, + ParameterNameGenerator parameterNameGenerator, + IReadOnlyDictionary parametersValues) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGenerator = parameterNameGenerator; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is FromSqlExpression fromSql) + { + if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) + { + switch (fromSql.Arguments) + { + case ParameterExpression parameterExpression: + var parameterValues = (object[])_parametersValues[parameterExpression.Name]; + + var subParameters = new List(parameterValues.Length); + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < parameterValues.Length; i++) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (parameterValues[i] is DbParameter dbParameter) + { + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); + } + else + { + subParameters.Add( + new TypeMappedRelationalParameter( + parameterName, + parameterName, + _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), + parameterValues[i]?.GetType().IsNullableType())); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant( + new CompositeRelationalParameter( + parameterExpression.Name, + subParameters)), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + + case ConstantExpression constantExpression: + var existingValues = (object[])constantExpression.Value; + var constantValues = new object[existingValues.Length]; + for (var i = 0; i < existingValues.Length; i++) + { + var value = existingValues[i]; + if (value is DbParameter dbParameter) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); + } + else + { + constantValues[i] = _sqlExpressionFactory.Constant( + value, _sqlExpressionFactory.GetTypeMappingForValue(value)); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant(constantValues, typeof(object[])), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + } + } + + return updatedFromSql; + } + + return base.Visit(expression); + } + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs new file mode 100644 index 00000000000..fabb829eb93 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs @@ -0,0 +1,311 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Storage; + +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 class QueryingEnumerable : IEnumerable, IAsyncEnumerable + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; + private readonly Func _shaper; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + + public QueryingEnumerable( + RelationalQueryContext relationalQueryContext, + RelationalCommandCache relationalCommandCache, + IReadOnlyList columnNames, + Func shaper, + Type contextType, + IDiagnosticsLogger logger) + { + _relationalQueryContext = relationalQueryContext; + _relationalCommandCache = relationalCommandCache; + _columnNames = columnNames; + _shaper = shaper; + _contextType = contextType; + _logger = logger; + } + + public virtual IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => new AsyncEnumerator(this, cancellationToken); + + public virtual IEnumerator GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private sealed class Enumerator : IEnumerator + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; + private readonly Func _shaper; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + + private RelationalDataReader _dataReader; + private int[] _indexMap; + private ResultCoordinator _resultCoordinator; + + public Enumerator(QueryingEnumerable queryingEnumerable) + { + _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _columnNames = queryingEnumerable._columnNames; + _shaper = queryingEnumerable._shaper; + _contextType = queryingEnumerable._contextType; + _logger = queryingEnumerable._logger; + } + + public T Current { get; private set; } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + try + { + using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + { + if (_dataReader == null) + { + var relationalCommand = _relationalCommandCache.GetRelationalCommand( + _relationalQueryContext.ParameterValues); + + _dataReader + = relationalCommand.ExecuteReader( + new RelationalCommandParameterObject( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + _relationalQueryContext.Context, + _relationalQueryContext.CommandLogger)); + + // Non-Composed FromSql + if (_columnNames != null) + { + var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) + .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); + + _indexMap = new int[_columnNames.Count]; + for (var i = 0; i < _columnNames.Count; i++) + { + var columnName = _columnNames[i]; + if (!readerColumns.TryGetValue(columnName, out var ordinal)) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + } + + _indexMap[i] = ordinal; + } + } + else + { + _indexMap = null; + } + + _resultCoordinator = new ResultCoordinator(); + } + + var hasNext = _resultCoordinator.HasNext ?? _dataReader.Read(); + Current = default; + + if (hasNext) + { + while (true) + { + _resultCoordinator.ResultReady = true; + _resultCoordinator.HasNext = null; + Current = _shaper( + _relationalQueryContext, _dataReader.DbDataReader, + _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); + if (_resultCoordinator.ResultReady) + { + // We generated a result so null out previously stored values + _resultCoordinator.ResultContext.Values = null; + break; + } + + if (!_dataReader.Read()) + { + _resultCoordinator.HasNext = false; + // Enumeration has ended, materialize last element + _resultCoordinator.ResultReady = true; + Current = _shaper( + _relationalQueryContext, _dataReader.DbDataReader, + _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); + + break; + } + } + } + + return hasNext; + } + } + catch (Exception exception) + { + _logger.QueryIterationFailed(_contextType, exception); + + throw; + } + } + + public void Dispose() + { + _dataReader?.Dispose(); + _dataReader = null; + } + + public void Reset() => throw new NotImplementedException(); + } + + private sealed class AsyncEnumerator : IAsyncEnumerator + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly IReadOnlyList _columnNames; + private readonly Func _shaper; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + private readonly CancellationToken _cancellationToken; + + private RelationalDataReader _dataReader; + private int[] _indexMap; + private ResultCoordinator _resultCoordinator; + + public AsyncEnumerator( + QueryingEnumerable queryingEnumerable, + CancellationToken cancellationToken) + { + _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _columnNames = queryingEnumerable._columnNames; + _shaper = queryingEnumerable._shaper; + _contextType = queryingEnumerable._contextType; + _logger = queryingEnumerable._logger; + _cancellationToken = cancellationToken; + } + + public T Current { get; private set; } + + public async ValueTask MoveNextAsync() + { + try + { + using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + { + if (_dataReader == null) + { + var relationalCommand = _relationalCommandCache.GetRelationalCommand( + _relationalQueryContext.ParameterValues); + + _dataReader + = await relationalCommand.ExecuteReaderAsync( + new RelationalCommandParameterObject( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + _relationalQueryContext.Context, + _relationalQueryContext.CommandLogger), + _cancellationToken); + + // Non-Composed FromSql + if (_columnNames != null) + { + var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) + .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); + + _indexMap = new int[_columnNames.Count]; + for (var i = 0; i < _columnNames.Count; i++) + { + var columnName = _columnNames[i]; + if (!readerColumns.TryGetValue(columnName, out var ordinal)) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + } + + _indexMap[i] = ordinal; + } + } + else + { + _indexMap = null; + } + + _resultCoordinator = new ResultCoordinator(); + } + + var hasNext = _resultCoordinator.HasNext ?? await _dataReader.ReadAsync(_cancellationToken); + Current = default; + + if (hasNext) + { + while (true) + { + _resultCoordinator.ResultReady = true; + _resultCoordinator.HasNext = null; + Current = _shaper( + _relationalQueryContext, _dataReader.DbDataReader, + _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); + if (_resultCoordinator.ResultReady) + { + // We generated a result so null out previously stored values + _resultCoordinator.ResultContext.Values = null; + break; + } + + if (!await _dataReader.ReadAsync(_cancellationToken)) + { + _resultCoordinator.HasNext = false; + // Enumeration has ended, materialize last element + _resultCoordinator.ResultReady = true; + Current = _shaper( + _relationalQueryContext, _dataReader.DbDataReader, + _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); + + break; + } + } + } + + return hasNext; + } + } + catch (Exception exception) + { + _logger.QueryIterationFailed(_contextType, exception); + + throw; + } + } + + public ValueTask DisposeAsync() + { + if (_dataReader != null) + { + var dataReader = _dataReader; + _dataReader = null; + + return dataReader.DisposeAsync(); + } + + return default; + } + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs new file mode 100644 index 00000000000..af430104185 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs @@ -0,0 +1,133 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Caching.Memory; + +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 class RelationalCommandCache + { + private static readonly ConcurrentDictionary _syncObjects + = new ConcurrentDictionary(); + + private readonly IMemoryCache _memoryCache; + private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; + private readonly SelectExpression _selectExpression; + private readonly ParameterValueBasedSelectExpressionOptimizer _parameterValueBasedSelectExpressionOptimizer; + + public RelationalCommandCache( + IMemoryCache memoryCache, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, + IQuerySqlGeneratorFactory querySqlGeneratorFactory, + bool useRelationalNulls, + SelectExpression selectExpression) + { + _memoryCache = memoryCache; + _querySqlGeneratorFactory = querySqlGeneratorFactory; + _selectExpression = selectExpression; + + _parameterValueBasedSelectExpressionOptimizer = new ParameterValueBasedSelectExpressionOptimizer( + sqlExpressionFactory, + parameterNameGeneratorFactory, + useRelationalNulls); + } + + public virtual IRelationalCommand GetRelationalCommand(IReadOnlyDictionary parameters) + { + var cacheKey = new CommandCacheKey(_selectExpression, parameters); + + retry: + if (!_memoryCache.TryGetValue(cacheKey, out IRelationalCommand relationalCommand)) + { + if (!_syncObjects.TryAdd(cacheKey, value: null)) + { + goto retry; + } + + try + { + var (selectExpression, canCache) = + _parameterValueBasedSelectExpressionOptimizer.Optimize(_selectExpression, parameters); + relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + + if (canCache) + { + _memoryCache.Set(cacheKey, relationalCommand, new MemoryCacheEntryOptions { Size = 10 }); + } + } + finally + { + _syncObjects.TryRemove(cacheKey, out _); + } + } + + return relationalCommand; + } + + private readonly struct CommandCacheKey + { + private readonly SelectExpression _selectExpression; + private readonly IReadOnlyDictionary _parameterValues; + + public CommandCacheKey(SelectExpression selectExpression, IReadOnlyDictionary parameterValues) + { + _selectExpression = selectExpression; + _parameterValues = parameterValues; + } + + public override bool Equals(object obj) + => obj != null + && obj is CommandCacheKey commandCacheKey + && Equals(commandCacheKey); + + private bool Equals(CommandCacheKey commandCacheKey) + { + if (!ReferenceEquals(_selectExpression, commandCacheKey._selectExpression)) + { + return false; + } + + if (_parameterValues.Count > 0) + { + foreach (var parameterValue in _parameterValues) + { + var value = parameterValue.Value; + if (!commandCacheKey._parameterValues.TryGetValue(parameterValue.Key, out var otherValue)) + { + return false; + } + + // ReSharper disable once ArrangeRedundantParentheses + if ((value == null) != (otherValue == null)) + { + return false; + } + + if (value is IEnumerable + && value.GetType() == typeof(object[])) + { + // FromSql parameters must have the same number of elements + return ((object[])value).Length == (otherValue as object[])?.Length; + } + } + } + + return true; + } + + public override int GetHashCode() => 0; + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/ResultContext.cs b/src/EFCore.Relational/Query/Internal/ResultContext.cs new file mode 100644 index 00000000000..61f960eb793 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/ResultContext.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +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 class ResultContext + { + public virtual object[] Values { get; set; } + } +} diff --git a/src/EFCore.Relational/Query/Internal/ResultCoordinator.cs b/src/EFCore.Relational/Query/Internal/ResultCoordinator.cs new file mode 100644 index 00000000000..1dd9a6a9131 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/ResultCoordinator.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +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 class ResultCoordinator + { + public ResultCoordinator() + { + ResultContext = new ResultContext(); + } + + public virtual ResultContext ResultContext { get; } + public virtual bool ResultReady { get; set; } + public virtual bool? HasNext { get; set; } + public virtual IList Collections { get; } = new List(); + + public virtual void SetCollectionMaterializationContext( + int collectionId, CollectionMaterializationContext collectionMaterializationContext) + { + while (Collections.Count <= collectionId) + { + Collections.Add(null); + } + + Collections[collectionId] = collectionMaterializationContext; + } + } +} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs index 7beae24d2c6..c0aa24030e0 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs @@ -11,6 +11,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; namespace Microsoft.EntityFrameworkCore.Query { @@ -96,37 +97,10 @@ private static void PopulateCollection( return; } - void processCurrentElementRow() - { - var previousResultReady = resultCoordinator.ResultReady; - resultCoordinator.ResultReady = true; - var element = innerShaper( - queryContext, dbDataReader, collectionMaterializationContext.ResultContext, resultCoordinator); - if (resultCoordinator.ResultReady) - { - // related element is materialized - collectionMaterializationContext.ResultContext.Values = null; - ((TCollection)collectionMaterializationContext.Collection).Add(element); - } - - resultCoordinator.ResultReady &= previousResultReady; - } - - void generateCurrentElementIfPending() - { - if (collectionMaterializationContext.ResultContext.Values != null) - { - resultCoordinator.HasNext = false; - processCurrentElementRow(); - } - - collectionMaterializationContext.UpdateSelfIdentifier(null); - } - if (resultCoordinator.HasNext == false) { // Outer Enumerator has ended - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); return; } @@ -134,7 +108,7 @@ void generateCurrentElementIfPending() outerIdentifier(queryContext, dbDataReader), collectionMaterializationContext.OuterIdentifier)) { // Outer changed so collection has ended. Materialize last element. - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); // If parent also changed then this row is now pointing to element of next collection if (!StructuralComparisons.StructuralEqualityComparer.Equals( parentIdentifier(queryContext, dbDataReader), collectionMaterializationContext.ParentIdentifier)) @@ -161,7 +135,7 @@ void generateCurrentElementIfPending() // If it is pending materialization then it may have nested elements if (collectionMaterializationContext.ResultContext.Values != null) { - processCurrentElementRow(); + ProcessCurrentElementRow(); } resultCoordinator.ResultReady = false; @@ -170,7 +144,7 @@ void generateCurrentElementIfPending() // Row for new element which is not first element // So materialize the element - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); resultCoordinator.HasNext = null; collectionMaterializationContext.UpdateSelfIdentifier(innerKey); } @@ -180,8 +154,35 @@ void generateCurrentElementIfPending() collectionMaterializationContext.UpdateSelfIdentifier(innerKey); } - processCurrentElementRow(); + ProcessCurrentElementRow(); resultCoordinator.ResultReady = false; + + void ProcessCurrentElementRow() + { + var previousResultReady = resultCoordinator.ResultReady; + resultCoordinator.ResultReady = true; + var element = innerShaper( + queryContext, dbDataReader, collectionMaterializationContext.ResultContext, resultCoordinator); + if (resultCoordinator.ResultReady) + { + // related element is materialized + collectionMaterializationContext.ResultContext.Values = null; + ((TCollection)collectionMaterializationContext.Collection).Add(element); + } + + resultCoordinator.ResultReady &= previousResultReady; + } + + void GenerateCurrentElementIfPending() + { + if (collectionMaterializationContext.ResultContext.Values != null) + { + resultCoordinator.HasNext = false; + ProcessCurrentElementRow(); + } + + collectionMaterializationContext.UpdateSelfIdentifier(null); + } } private static readonly MethodInfo _populateIncludeCollectionMethodInfo @@ -204,44 +205,10 @@ private static void PopulateIncludeCollection var collectionMaterializationContext = resultCoordinator.Collections[collectionId]; if (collectionMaterializationContext.Parent is TIncludingEntity entity) { - void processCurrentElementRow() - { - var previousResultReady = resultCoordinator.ResultReady; - resultCoordinator.ResultReady = true; - var relatedEntity = innerShaper( - queryContext, dbDataReader, collectionMaterializationContext.ResultContext, resultCoordinator); - if (resultCoordinator.ResultReady) - { - // related entity is materialized - collectionMaterializationContext.ResultContext.Values = null; - if (!trackingQuery) - { - fixup(entity, relatedEntity); - if (inverseNavigation != null) - { - SetIsLoadedNoTracking(relatedEntity, inverseNavigation); - } - } - } - - resultCoordinator.ResultReady &= previousResultReady; - } - - void generateCurrentElementIfPending() - { - if (collectionMaterializationContext.ResultContext.Values != null) - { - resultCoordinator.HasNext = false; - processCurrentElementRow(); - } - - collectionMaterializationContext.UpdateSelfIdentifier(null); - } - if (resultCoordinator.HasNext == false) { // Outer Enumerator has ended - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); return; } @@ -249,7 +216,7 @@ void generateCurrentElementIfPending() outerIdentifier(queryContext, dbDataReader), collectionMaterializationContext.OuterIdentifier)) { // Outer changed so collection has ended. Materialize last element. - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); // If parent also changed then this row is now pointing to element of next collection if (!StructuralComparisons.StructuralEqualityComparer.Equals( parentIdentifier(queryContext, dbDataReader), collectionMaterializationContext.ParentIdentifier)) @@ -276,7 +243,7 @@ void generateCurrentElementIfPending() // If it is pending materialization then it may have nested elements if (collectionMaterializationContext.ResultContext.Values != null) { - processCurrentElementRow(); + ProcessCurrentElementRow(); } resultCoordinator.ResultReady = false; @@ -285,7 +252,7 @@ void generateCurrentElementIfPending() // Row for new element which is not first element // So materialize the element - generateCurrentElementIfPending(); + GenerateCurrentElementIfPending(); resultCoordinator.HasNext = null; collectionMaterializationContext.UpdateSelfIdentifier(innerKey); } @@ -295,9 +262,43 @@ void generateCurrentElementIfPending() collectionMaterializationContext.UpdateSelfIdentifier(innerKey); } - processCurrentElementRow(); + ProcessCurrentElementRow(); resultCoordinator.ResultReady = false; } + + void ProcessCurrentElementRow() + { + var previousResultReady = resultCoordinator.ResultReady; + resultCoordinator.ResultReady = true; + var relatedEntity = innerShaper( + queryContext, dbDataReader, collectionMaterializationContext.ResultContext, resultCoordinator); + if (resultCoordinator.ResultReady) + { + // related entity is materialized + collectionMaterializationContext.ResultContext.Values = null; + if (!trackingQuery) + { + fixup(entity, relatedEntity); + if (inverseNavigation != null) + { + SetIsLoadedNoTracking(relatedEntity, inverseNavigation); + } + } + } + + resultCoordinator.ResultReady &= previousResultReady; + } + + void GenerateCurrentElementIfPending() + { + if (collectionMaterializationContext.ResultContext.Values != null) + { + resultCoordinator.HasNext = false; + ProcessCurrentElementRow(); + } + + collectionMaterializationContext.UpdateSelfIdentifier(null); + } } private static readonly MethodInfo _initializeIncludeCollectionMethodInfo diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.FromSqlParameterApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.FromSqlParameterApplyingExpressionVisitor.cs deleted file mode 100644 index cefe503d676..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.FromSqlParameterApplyingExpressionVisitor.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor - { - private readonly IDictionary _visitedFromSqlExpressions - = new Dictionary(ReferenceEqualityComparer.Instance); - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly ParameterNameGenerator _parameterNameGenerator; - private readonly IReadOnlyDictionary _parametersValues; - - public FromSqlParameterApplyingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - ParameterNameGenerator parameterNameGenerator, - IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parameterNameGenerator = parameterNameGenerator; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is FromSqlExpression fromSql) - { - if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) - { - switch (fromSql.Arguments) - { - case ParameterExpression parameterExpression: - var parameterValues = (object[])_parametersValues[parameterExpression.Name]; - - var subParameters = new List(parameterValues.Length); - for (var i = 0; i < parameterValues.Length; i++) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (parameterValues[i] is DbParameter dbParameter) - { - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); - } - else - { - subParameters.Add( - new TypeMappedRelationalParameter( - parameterName, - parameterName, - _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), - parameterValues[i]?.GetType().IsNullableType())); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant( - new CompositeRelationalParameter( - parameterExpression.Name, - subParameters)), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - - case ConstantExpression constantExpression: - var existingValues = (object[])constantExpression.Value; - var constantValues = new object[existingValues.Length]; - for (var i = 0; i < existingValues.Length; i++) - { - var value = existingValues[i]; - if (value is DbParameter dbParameter) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); - } - else - { - constantValues[i] = _sqlExpressionFactory.Constant( - value, _sqlExpressionFactory.GetTypeMappingForValue(value)); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant(constantValues, typeof(object[])), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - } - } - - return updatedFromSql; - } - - return base.Visit(expression); - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs deleted file mode 100644 index 2f4690b1d3d..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.InExpressionValuesExpandingExpressionVisitor.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections; -using System.Collections.Generic; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IReadOnlyDictionary _parametersValues; - - public InExpressionValuesExpandingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is InExpression inExpression - && inExpression.Values != null) - { - var inValues = new List(); - var hasNullValue = false; - RelationalTypeMapping typeMapping = null; - - switch (inExpression.Values) - { - case SqlConstantExpression sqlConstant: - { - typeMapping = sqlConstant.TypeMapping; - var values = (IEnumerable)sqlConstant.Value; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - - case SqlParameterExpression sqlParameter: - { - typeMapping = sqlParameter.TypeMapping; - var values = (IEnumerable)_parametersValues[sqlParameter.Name]; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - } - - var updatedInExpression = inValues.Count > 0 - ? _sqlExpressionFactory.In( - (SqlExpression)Visit(inExpression.Item), - _sqlExpressionFactory.Constant(inValues, typeMapping), - inExpression.IsNegated) - : null; - - var nullCheckExpression = hasNullValue - ? inExpression.IsNegated - ? _sqlExpressionFactory.IsNotNull(inExpression.Item) - : _sqlExpressionFactory.IsNull(inExpression.Item) - : null; - - if (updatedInExpression != null - && nullCheckExpression != null) - { - return inExpression.IsNegated - ? _sqlExpressionFactory.AndAlso(updatedInExpression, nullCheckExpression) - : _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); - } - - if (updatedInExpression == null - && nullCheckExpression == null) - { - return _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(inExpression.IsNegated)); - } - - return (SqlExpression)updatedInExpression ?? nullCheckExpression; - } - - return base.Visit(expression); - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterNullabilityOptimizingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterNullabilityOptimizingExpressionVisitor.cs deleted file mode 100644 index 7314488ffa4..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterNullabilityOptimizingExpressionVisitor.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Query.Internal; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor : SqlExpressionOptimizingExpressionVisitor - { - private readonly IReadOnlyDictionary _parametersValues; - - public ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - bool useRelationalNulls, - IReadOnlyDictionary parametersValues) - : base(sqlExpressionFactory, useRelationalNulls) - { - _parametersValues = parametersValues; - } - - protected override Expression VisitExtension(Expression extensionExpression) - { - if (extensionExpression is SelectExpression selectExpression) - { - var newSelectExpression = (SelectExpression)base.VisitExtension(extensionExpression); - - // if predicate is optimized to true, we can simply remove it - var newPredicate = newSelectExpression.Predicate is SqlConstantExpression newSelectPredicateConstant - && !(selectExpression.Predicate is SqlConstantExpression) - ? (bool)newSelectPredicateConstant.Value - ? null - : SqlExpressionFactory.Equal( - newSelectPredicateConstant, - SqlExpressionFactory.Constant(true, newSelectPredicateConstant.TypeMapping)) - : newSelectExpression.Predicate; - - var newHaving = newSelectExpression.Having is SqlConstantExpression newSelectHavingConstant - && !(selectExpression.Having is SqlConstantExpression) - ? (bool)newSelectHavingConstant.Value - ? null - : SqlExpressionFactory.Equal( - newSelectHavingConstant, - SqlExpressionFactory.Constant(true, newSelectHavingConstant.TypeMapping)) - : newSelectExpression.Having; - - return newPredicate != newSelectExpression.Predicate - || newHaving != newSelectExpression.Having - ? newSelectExpression.Update( - newSelectExpression.Projection.ToList(), - newSelectExpression.Tables.ToList(), - newPredicate, - newSelectExpression.GroupBy.ToList(), - newHaving, - newSelectExpression.Orderings.ToList(), - newSelectExpression.Limit, - newSelectExpression.Offset, - newSelectExpression.IsDistinct, - newSelectExpression.Alias) - : newSelectExpression; - } - - return base.VisitExtension(extensionExpression); - } - - protected override Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression) - { - var result = base.VisitSqlUnaryExpression(sqlUnaryExpression); - if (result is SqlUnaryExpression newUnaryExpresion - && newUnaryExpresion.Operand is SqlParameterExpression parameterOperand) - { - var parameterValue = _parametersValues[parameterOperand.Name]; - if (sqlUnaryExpression.OperatorType == ExpressionType.Equal) - { - return SqlExpressionFactory.Constant(parameterValue == null, sqlUnaryExpression.TypeMapping); - } - - if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) - { - return SqlExpressionFactory.Constant(parameterValue != null, sqlUnaryExpression.TypeMapping); - } - } - - return result; - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterValueBasedSelectExpressionOptimizer.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterValueBasedSelectExpressionOptimizer.cs deleted file mode 100644 index 918c5e7386f..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ParameterValueBasedSelectExpressionOptimizer.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class ParameterValueBasedSelectExpressionOptimizer - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; - private readonly bool _useRelationalNulls; - - public ParameterValueBasedSelectExpressionOptimizer( - ISqlExpressionFactory sqlExpressionFactory, - IParameterNameGeneratorFactory parameterNameGeneratorFactory, - bool useRelationalNulls) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parameterNameGeneratorFactory = parameterNameGeneratorFactory; - _useRelationalNulls = useRelationalNulls; - } - - public (SelectExpression selectExpression, bool canCache) Optimize( - SelectExpression selectExpression, IReadOnlyDictionary parametersValues) - { - var canCache = true; - - var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor( - _sqlExpressionFactory, parametersValues).Visit(selectExpression); - - if (!ReferenceEquals(selectExpression, inExpressionOptimized)) - { - canCache = false; - } - - var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - _sqlExpressionFactory, _useRelationalNulls, parametersValues).Visit(inExpressionOptimized); - - var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor( - _sqlExpressionFactory, - _parameterNameGeneratorFactory.Create(), - parametersValues).Visit(nullParametersOptimized); - - if (!ReferenceEquals(nullParametersOptimized, fromSqlParameterOptimized)) - { - canCache = false; - } - - return (selectExpression: (SelectExpression)fromSqlParameterOptimized, canCache); - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs deleted file mode 100644 index d24f08c9656..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Storage; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class QueryingEnumerable : IEnumerable, IAsyncEnumerable - { - private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; - private readonly IReadOnlyList _columnNames; - private readonly Func _shaper; - private readonly Type _contextType; - private readonly IDiagnosticsLogger _logger; - - public QueryingEnumerable( - RelationalQueryContext relationalQueryContext, - RelationalCommandCache relationalCommandCache, - IReadOnlyList columnNames, - Func shaper, - Type contextType, - IDiagnosticsLogger logger) - { - _relationalQueryContext = relationalQueryContext; - _relationalCommandCache = relationalCommandCache; - _columnNames = columnNames; - _shaper = shaper; - _contextType = contextType; - _logger = logger; - } - - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - => new AsyncEnumerator(this, cancellationToken); - - public IEnumerator GetEnumerator() => new Enumerator(this); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private sealed class Enumerator : IEnumerator - { - private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; - private readonly IReadOnlyList _columnNames; - private readonly Func _shaper; - private readonly Type _contextType; - private readonly IDiagnosticsLogger _logger; - - private RelationalDataReader _dataReader; - private int[] _indexMap; - private ResultCoordinator _resultCoordinator; - - public Enumerator(QueryingEnumerable queryingEnumerable) - { - _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; - _columnNames = queryingEnumerable._columnNames; - _shaper = queryingEnumerable._shaper; - _contextType = queryingEnumerable._contextType; - _logger = queryingEnumerable._logger; - } - - public T Current { get; private set; } - - object IEnumerator.Current => Current; - - public bool MoveNext() - { - try - { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) - { - if (_dataReader == null) - { - var relationalCommand = _relationalCommandCache.GetRelationalCommand( - _relationalQueryContext.ParameterValues); - - _dataReader - = relationalCommand.ExecuteReader( - new RelationalCommandParameterObject( - _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, - _relationalQueryContext.Context, - _relationalQueryContext.CommandLogger)); - - // Non-Composed FromSql - if (_columnNames != null) - { - var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) - .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); - - _indexMap = new int[_columnNames.Count]; - for (var i = 0; i < _columnNames.Count; i++) - { - var columnName = _columnNames[i]; - if (!readerColumns.TryGetValue(columnName, out var ordinal)) - { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); - } - - _indexMap[i] = ordinal; - } - } - else - { - _indexMap = null; - } - - _resultCoordinator = new ResultCoordinator(); - } - - var hasNext = _resultCoordinator.HasNext ?? _dataReader.Read(); - Current = default; - - if (hasNext) - { - while (true) - { - _resultCoordinator.ResultReady = true; - _resultCoordinator.HasNext = null; - Current = _shaper( - _relationalQueryContext, _dataReader.DbDataReader, - _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); - if (_resultCoordinator.ResultReady) - { - // We generated a result so null out previously stored values - _resultCoordinator.ResultContext.Values = null; - break; - } - - if (!_dataReader.Read()) - { - _resultCoordinator.HasNext = false; - // Enumeration has ended, materialize last element - _resultCoordinator.ResultReady = true; - Current = _shaper( - _relationalQueryContext, _dataReader.DbDataReader, - _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); - - break; - } - } - } - - return hasNext; - } - } - catch (Exception exception) - { - _logger.QueryIterationFailed(_contextType, exception); - - throw; - } - } - - public void Dispose() - { - _dataReader?.Dispose(); - _dataReader = null; - } - - public void Reset() => throw new NotImplementedException(); - } - - private sealed class AsyncEnumerator : IAsyncEnumerator - { - private readonly RelationalQueryContext _relationalQueryContext; - private readonly RelationalCommandCache _relationalCommandCache; - private readonly IReadOnlyList _columnNames; - private readonly Func _shaper; - private readonly Type _contextType; - private readonly IDiagnosticsLogger _logger; - private readonly CancellationToken _cancellationToken; - - private RelationalDataReader _dataReader; - private int[] _indexMap; - private ResultCoordinator _resultCoordinator; - - public AsyncEnumerator( - QueryingEnumerable queryingEnumerable, - CancellationToken cancellationToken) - { - _relationalQueryContext = queryingEnumerable._relationalQueryContext; - _relationalCommandCache = queryingEnumerable._relationalCommandCache; - _columnNames = queryingEnumerable._columnNames; - _shaper = queryingEnumerable._shaper; - _contextType = queryingEnumerable._contextType; - _logger = queryingEnumerable._logger; - _cancellationToken = cancellationToken; - } - - public T Current { get; private set; } - - public async ValueTask MoveNextAsync() - { - try - { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) - { - if (_dataReader == null) - { - var relationalCommand = _relationalCommandCache.GetRelationalCommand( - _relationalQueryContext.ParameterValues); - - _dataReader - = await relationalCommand.ExecuteReaderAsync( - new RelationalCommandParameterObject( - _relationalQueryContext.Connection, - _relationalQueryContext.ParameterValues, - _relationalQueryContext.Context, - _relationalQueryContext.CommandLogger), - _cancellationToken); - - // Non-Composed FromSql - if (_columnNames != null) - { - var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) - .ToDictionary(i => _dataReader.DbDataReader.GetName(i), i => i, StringComparer.OrdinalIgnoreCase); - - _indexMap = new int[_columnNames.Count]; - for (var i = 0; i < _columnNames.Count; i++) - { - var columnName = _columnNames[i]; - if (!readerColumns.TryGetValue(columnName, out var ordinal)) - { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); - } - - _indexMap[i] = ordinal; - } - } - else - { - _indexMap = null; - } - - _resultCoordinator = new ResultCoordinator(); - } - - var hasNext = _resultCoordinator.HasNext ?? await _dataReader.ReadAsync(); - Current = default; - - if (hasNext) - { - while (true) - { - _resultCoordinator.ResultReady = true; - _resultCoordinator.HasNext = null; - Current = _shaper( - _relationalQueryContext, _dataReader.DbDataReader, - _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); - if (_resultCoordinator.ResultReady) - { - // We generated a result so null out previously stored values - _resultCoordinator.ResultContext.Values = null; - break; - } - - if (!await _dataReader.ReadAsync()) - { - _resultCoordinator.HasNext = false; - // Enumeration has ended, materialize last element - _resultCoordinator.ResultReady = true; - Current = _shaper( - _relationalQueryContext, _dataReader.DbDataReader, - _resultCoordinator.ResultContext, _indexMap, _resultCoordinator); - - break; - } - } - } - - return hasNext; - } - } - catch (Exception exception) - { - _logger.QueryIterationFailed(_contextType, exception); - - throw; - } - } - - public ValueTask DisposeAsync() - { - if (_dataReader != null) - { - var dataReader = _dataReader; - _dataReader = null; - - return dataReader.DisposeAsync(); - } - - return default; - } - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs deleted file mode 100644 index adf799fca37..00000000000 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.RelationalCommandCache.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.Extensions.Caching.Memory; - -namespace Microsoft.EntityFrameworkCore.Query -{ - public partial class RelationalShapedQueryCompilingExpressionVisitor - { - private class RelationalCommandCache - { - private static readonly ConcurrentDictionary _syncObjects - = new ConcurrentDictionary(); - - private readonly IMemoryCache _memoryCache; - private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; - private readonly SelectExpression _selectExpression; - private readonly ParameterValueBasedSelectExpressionOptimizer _parameterValueBasedSelectExpressionOptimizer; - - public RelationalCommandCache( - IMemoryCache memoryCache, - ISqlExpressionFactory sqlExpressionFactory, - IParameterNameGeneratorFactory parameterNameGeneratorFactory, - IQuerySqlGeneratorFactory querySqlGeneratorFactory, - bool useRelationalNulls, - SelectExpression selectExpression) - { - _memoryCache = memoryCache; - _querySqlGeneratorFactory = querySqlGeneratorFactory; - _selectExpression = selectExpression; - - _parameterValueBasedSelectExpressionOptimizer = new ParameterValueBasedSelectExpressionOptimizer( - sqlExpressionFactory, - parameterNameGeneratorFactory, - useRelationalNulls); - } - - public IRelationalCommand GetRelationalCommand(IReadOnlyDictionary parameters) - { - var cacheKey = new CommandCacheKey(_selectExpression, parameters); - - retry: - if (!_memoryCache.TryGetValue(cacheKey, out IRelationalCommand relationalCommand)) - { - if (!_syncObjects.TryAdd(cacheKey, value: null)) - { - goto retry; - } - - try - { - var (selectExpression, canCache) = - _parameterValueBasedSelectExpressionOptimizer.Optimize(_selectExpression, parameters); - relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); - - if (canCache) - { - _memoryCache.Set(cacheKey, relationalCommand, new MemoryCacheEntryOptions { Size = 10 }); - } - } - finally - { - _syncObjects.TryRemove(cacheKey, out _); - } - } - - return relationalCommand; - } - - private readonly struct CommandCacheKey - { - private readonly SelectExpression _selectExpression; - private readonly IReadOnlyDictionary _parameterValues; - - public CommandCacheKey(SelectExpression selectExpression, IReadOnlyDictionary parameterValues) - { - _selectExpression = selectExpression; - _parameterValues = parameterValues; - } - - public override bool Equals(object obj) - => obj != null - && (obj is CommandCacheKey commandCacheKey - && Equals(commandCacheKey)); - - private bool Equals(CommandCacheKey commandCacheKey) - { - if (!ReferenceEquals(_selectExpression, commandCacheKey._selectExpression)) - { - return false; - } - - if (_parameterValues.Count > 0) - { - foreach (var parameterValue in _parameterValues) - { - var value = parameterValue.Value; - if (!commandCacheKey._parameterValues.TryGetValue(parameterValue.Key, out var otherValue)) - { - return false; - } - - // ReSharper disable once ArrangeRedundantParentheses - if ((value == null) != (otherValue == null)) - { - return false; - } - - if (value is IEnumerable - && value.GetType() == typeof(object[])) - { - // FromSql parameters must have the same number of elements - return ((object[])value).Length == (otherValue as object[])?.Length; - } - } - } - - return true; - } - - public override int GetHashCode() => 0; - } - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperExpressionProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperExpressionProcessingExpressionVisitor.cs index 4a94e9129e0..f697a32f469 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperExpressionProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperExpressionProcessingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query @@ -84,7 +85,7 @@ public override Expression Visit(Expression expression) } } - public virtual Expression Inject(Expression expression) + public Expression Inject(Expression expression) { _containsCollectionMaterialization = new CollectionShaperFindingExpressionVisitor() .ContainsCollectionMaterialization(expression); @@ -194,34 +195,36 @@ protected override Expression VisitExtension(Expression extensionExpression) case ProjectionBindingExpression projectionBindingExpression: { var key = GenerateKey(projectionBindingExpression); - if (!_mapping.TryGetValue(key, out var accessor)) + if (_mapping.TryGetValue(key, out var accessor)) { - var valueParameter = Expression.Parameter(projectionBindingExpression.Type); - _variables.Add(valueParameter); - _expressions.Add(Expression.Assign(valueParameter, projectionBindingExpression)); + return accessor; + } - if (_containsCollectionMaterialization) - { - var expressionToAdd = (Expression)valueParameter; - if (expressionToAdd.Type.IsValueType) - { - expressionToAdd = Expression.Convert(expressionToAdd, typeof(object)); - } + var valueParameter = Expression.Parameter(projectionBindingExpression.Type); + _variables.Add(valueParameter); + _expressions.Add(Expression.Assign(valueParameter, projectionBindingExpression)); - _valuesArrayInitializers.Add(expressionToAdd); - accessor = Expression.Convert( - Expression.ArrayIndex( - _valuesArrayExpression, - Expression.Constant(_valuesArrayInitializers.Count - 1)), - projectionBindingExpression.Type); - } - else + if (_containsCollectionMaterialization) + { + var expressionToAdd = (Expression)valueParameter; + if (expressionToAdd.Type.IsValueType) { - accessor = valueParameter; + expressionToAdd = Expression.Convert(expressionToAdd, typeof(object)); } - _mapping[key] = accessor; + _valuesArrayInitializers.Add(expressionToAdd); + accessor = Expression.Convert( + Expression.ArrayIndex( + _valuesArrayExpression, + Expression.Constant(_valuesArrayInitializers.Count - 1)), + projectionBindingExpression.Type); } + else + { + accessor = valueParameter; + } + + _mapping[key] = accessor; return accessor; } @@ -266,44 +269,44 @@ protected override Expression VisitExtension(Expression extensionExpression) return entity; } - case RelationalCollectionShaperExpression relationalCollectionShaperExpression2: + case RelationalCollectionShaperExpression relationalCollectionShaperExpression: { - var key = GenerateKey(relationalCollectionShaperExpression2); + var key = GenerateKey(relationalCollectionShaperExpression); if (!_mapping.TryGetValue(key, out var accessor)) { var innerShaper = new ShaperExpressionProcessingExpressionVisitor( _selectExpression, _dataReaderParameter, _resultCoordinatorParameter, null) - .Inject(relationalCollectionShaperExpression2.InnerShaper); + .Inject(relationalCollectionShaperExpression.InnerShaper); _collectionPopulatingExpressions.Add( new CollectionPopulatingExpression( - relationalCollectionShaperExpression2.Update( - relationalCollectionShaperExpression2.ParentIdentifier, - relationalCollectionShaperExpression2.OuterIdentifier, - relationalCollectionShaperExpression2.SelfIdentifier, + relationalCollectionShaperExpression.Update( + relationalCollectionShaperExpression.ParentIdentifier, + relationalCollectionShaperExpression.OuterIdentifier, + relationalCollectionShaperExpression.SelfIdentifier, innerShaper), - relationalCollectionShaperExpression2.Type, + relationalCollectionShaperExpression.Type, false)); - var collectionParameter = Expression.Parameter(relationalCollectionShaperExpression2.Type); + var collectionParameter = Expression.Parameter(relationalCollectionShaperExpression.Type); _variables.Add(collectionParameter); _expressions.Add( Expression.Assign( collectionParameter, new CollectionInitializingExpression( - relationalCollectionShaperExpression2.CollectionId, + relationalCollectionShaperExpression.CollectionId, null, - relationalCollectionShaperExpression2.ParentIdentifier, - relationalCollectionShaperExpression2.OuterIdentifier, - relationalCollectionShaperExpression2.Navigation, - relationalCollectionShaperExpression2.Type))); + relationalCollectionShaperExpression.ParentIdentifier, + relationalCollectionShaperExpression.OuterIdentifier, + relationalCollectionShaperExpression.Navigation, + relationalCollectionShaperExpression.Type))); _valuesArrayInitializers.Add(collectionParameter); accessor = Expression.Convert( Expression.ArrayIndex( _valuesArrayExpression, Expression.Constant(_valuesArrayInitializers.Count - 1)), - relationalCollectionShaperExpression2.Type); + relationalCollectionShaperExpression.Type); _mapping[key] = accessor; } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 8b19cfa7dc1..6eb38022f00 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query @@ -109,59 +110,5 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return base.VisitMethodCall(methodCallExpression); } } - - private class ResultCoordinator - { - public ResultCoordinator() - { - ResultContext = new ResultContext(); - } - - public ResultContext ResultContext { get; } - public bool ResultReady { get; set; } - public bool? HasNext { get; set; } - public IList Collections { get; } = new List(); - - public void SetCollectionMaterializationContext( - int collectionId, CollectionMaterializationContext collectionMaterializationContext) - { - while (Collections.Count <= collectionId) - { - Collections.Add(null); - } - - Collections[collectionId] = collectionMaterializationContext; - } - } - - private class ResultContext - { - public object[] Values { get; set; } - } - - private class CollectionMaterializationContext - { - public CollectionMaterializationContext(object parent, object collection, object[] parentIdentifier, object[] outerIdentifier) - { - Parent = parent; - Collection = collection; - ParentIdentifier = parentIdentifier; - OuterIdentifier = outerIdentifier; - ResultContext = new ResultContext(); - } - - public ResultContext ResultContext { get; } - public object Parent { get; } - public object Collection { get; } - public object Current { get; private set; } - public object[] ParentIdentifier { get; } - public object[] OuterIdentifier { get; } - public object[] SelfIdentifier { get; private set; } - - public void UpdateSelfIdentifier(object[] selfIdentifier) - { - SelfIdentifier = selfIdentifier; - } - } } } diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 86080d8cdfd..440a9861a10 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -1080,7 +1080,7 @@ private void SetProperty( else { propertyIndex = -1; - equals = (l, r) => ReferenceEquals(l, r); + equals = ReferenceEquals; currentValueType = CurrentValueType.Normal; } diff --git a/src/EFCore/Storage/ValueBuffer.cs b/src/EFCore/Storage/ValueBuffer.cs index 3411f454211..9a56ee885b1 100644 --- a/src/EFCore/Storage/ValueBuffer.cs +++ b/src/EFCore/Storage/ValueBuffer.cs @@ -75,12 +75,9 @@ internal static readonly MethodInfo GetValueMethod /// True if the object is a and contains the same values, otherwise false. /// public override bool Equals(object obj) - { - return obj is null - ? false - : obj is ValueBuffer buffer + => !(obj is null) + && obj is ValueBuffer buffer && Equals(buffer); - } private bool Equals(ValueBuffer other) {