diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index bd84285e0eb..cc319dda23b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -33,6 +33,7 @@ private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable private readonly Type _contextType; private readonly string _partitionKey; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; public QueryingEnumerable( CosmosQueryContext cosmosQueryContext, @@ -42,7 +43,8 @@ public QueryingEnumerable( Func shaper, Type contextType, string partitionKeyFromExtension, - IDiagnosticsLogger logger) + IDiagnosticsLogger logger, + bool performIdentityResolution) { _cosmosQueryContext = cosmosQueryContext; _sqlExpressionFactory = sqlExpressionFactory; @@ -51,9 +53,9 @@ public QueryingEnumerable( _shaper = shaper; _contextType = contextType; _logger = logger; + _performIdentityResolution = performIdentityResolution; var partitionKey = selectExpression.GetPartitionKey(cosmosQueryContext.ParameterValues); - if (partitionKey != null && partitionKeyFromExtension != null && partitionKeyFromExtension != partitionKey) { throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch(partitionKeyFromExtension, partitionKey)); @@ -61,7 +63,7 @@ public QueryingEnumerable( _partitionKey = partitionKey ?? partitionKeyFromExtension; } - + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new AsyncEnumerator(this, cancellationToken); @@ -107,6 +109,7 @@ private sealed class Enumerator : IEnumerator private readonly Type _contextType; private readonly string _partitionKey; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private IEnumerator _enumerator; @@ -119,6 +122,7 @@ public Enumerator(QueryingEnumerable queryingEnumerable) _contextType = queryingEnumerable._contextType; _partitionKey = queryingEnumerable._partitionKey; _logger = queryingEnumerable._logger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; } public T Current { get; private set; } @@ -141,6 +145,7 @@ public bool MoveNext() _partitionKey, sqlQuery) .GetEnumerator(); + _cosmosQueryContext.InitializeStateManager(_performIdentityResolution); } var hasNext = _enumerator.MoveNext(); @@ -179,6 +184,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly Type _contextType; private readonly string _partitionKey; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private readonly CancellationToken _cancellationToken; private IAsyncEnumerator _enumerator; @@ -192,6 +198,7 @@ public AsyncEnumerator(QueryingEnumerable queryingEnumerable, CancellationTok _contextType = queryingEnumerable._contextType; _partitionKey = queryingEnumerable._partitionKey; _logger = queryingEnumerable._logger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; _cancellationToken = cancellationToken; } @@ -213,6 +220,7 @@ public async ValueTask MoveNextAsync() _partitionKey, sqlQuery) .GetAsyncEnumerator(_cancellationToken); + _cosmosQueryContext.InitializeStateManager(_performIdentityResolution); } var hasNext = await _enumerator.MoveNextAsync(); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index 0c915035e0a..5b3747726f7 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Query; @@ -34,19 +35,22 @@ private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnume private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; public ReadItemQueryingEnumerable( CosmosQueryContext cosmosQueryContext, ReadItemExpression readItemExpression, Func shaper, Type contextType, - IDiagnosticsLogger logger) + IDiagnosticsLogger logger, + bool performIdentityResolution) { _cosmosQueryContext = cosmosQueryContext; _readItemExpression = readItemExpression; _shaper = shaper; _contextType = contextType; _logger = logger; + _performIdentityResolution = performIdentityResolution; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -68,6 +72,7 @@ private sealed class Enumerator : IEnumerator, IAsyncEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private readonly CancellationToken _cancellationToken; private JObject _item; @@ -80,6 +85,7 @@ public Enumerator(ReadItemQueryingEnumerable readItemEnumerable, Cancellation _shaper = readItemEnumerable._shaper; _contextType = readItemEnumerable._contextType; _logger = readItemEnumerable._logger; + _performIdentityResolution = readItemEnumerable._performIdentityResolution; _cancellationToken = cancellationToken; } @@ -182,6 +188,8 @@ private bool ShapeResult() { var hasNext = !(_item is null); + _cosmosQueryContext.InitializeStateManager(_performIdentityResolution); + Current = hasNext ? _shaper(_cosmosQueryContext, _item) @@ -246,10 +254,10 @@ private bool TryGenerateIdFromKeys(IProperty idProperty, out object value) { var entityEntry = Activator.CreateInstance(_readItemExpression.EntityType.ClrType); -#pragma warning disable EF1001 // Internal EF Core API usage. +#pragma warning disable EF1001 var internalEntityEntry = new InternalEntityEntryFactory().Create( - _cosmosQueryContext.StateManager, _readItemExpression.EntityType, entityEntry); -#pragma warning restore EF1001 // Internal EF Core API usage. + _cosmosQueryContext.Context.GetDependencies().StateManager, _readItemExpression.EntityType, entityEntry); +#pragma warning restore EF1001 foreach (var keyProperty in _readItemExpression.EntityType.FindPrimaryKey().Properties) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index b4cec070738..7ef64ef2e0e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -87,7 +87,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), Expression.Constant(_partitionKeyFromExtension, typeof(string)), - Expression.Constant(_logger)); + Expression.Constant(_logger), + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); case ReadItemExpression readItemExpression: @@ -108,7 +109,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(readItemExpression), Expression.Constant(shaperReadItemLambda.Compile()), Expression.Constant(_contextType), - Expression.Constant(_logger)); + Expression.Constant(_logger), + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); default: throw new NotImplementedException(); diff --git a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs index a9bfd99e772..3eb808eea67 100644 --- a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs @@ -133,7 +133,7 @@ public SelectExpression( public virtual Expression GetMappedProjection([NotNull] ProjectionMember projectionMember) => _projectionMapping[projectionMember]; - + /// /// 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 diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 98618080d57..44a3567c1ce 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -29,19 +29,22 @@ private sealed class QueryingEnumerable : IAsyncEnumerable, IEnumerable private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; public QueryingEnumerable( QueryContext queryContext, IEnumerable innerEnumerable, Func shaper, Type contextType, - IDiagnosticsLogger logger) + IDiagnosticsLogger logger, + bool performIdentityResolution) { _queryContext = queryContext; _innerEnumerable = innerEnumerable; _shaper = shaper; _contextType = contextType; _logger = logger; + _performIdentityResolution = performIdentityResolution; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -61,6 +64,7 @@ private sealed class Enumerator : IEnumerator, IAsyncEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private readonly CancellationToken _cancellationToken; public Enumerator(QueryingEnumerable queryingEnumerable, CancellationToken cancellationToken = default) @@ -70,6 +74,7 @@ public Enumerator(QueryingEnumerable queryingEnumerable, CancellationToken ca _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; _cancellationToken = cancellationToken; } @@ -118,6 +123,7 @@ private bool MoveNextHelper() if (_enumerator == null) { _enumerator = _innerEnumerable.GetEnumerator(); + _queryContext.InitializeStateManager(_performIdentityResolution); } var hasNext = _enumerator.MoveNext(); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs index d7c227ab056..c710c504d0a 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs @@ -92,7 +92,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery innerEnumerable, Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), - Expression.Constant(_logger)); + Expression.Constant(_logger), + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); } private static readonly MethodInfo _tableMethodInfo diff --git a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs index a26ce4761e4..cf559c0e8aa 100644 --- a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs @@ -29,6 +29,7 @@ public class QueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelat private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,7 +44,8 @@ public QueryingEnumerable( [NotNull] IReadOnlyList readerColumns, [NotNull] Func shaper, [NotNull] Type contextType, - [NotNull] IDiagnosticsLogger logger) + [NotNull] IDiagnosticsLogger logger, + bool performIdentityResolution) { _relationalQueryContext = relationalQueryContext; _relationalCommandCache = relationalCommandCache; @@ -52,6 +54,7 @@ public QueryingEnumerable( _shaper = shaper; _contextType = contextType; _logger = logger; + _performIdentityResolution = performIdentityResolution; } /// @@ -148,6 +151,7 @@ private sealed class Enumerator : IEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private RelationalDataReader _dataReader; private int[] _indexMap; @@ -163,6 +167,7 @@ public Enumerator(QueryingEnumerable queryingEnumerable) _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; } public T Current { get; private set; } @@ -246,6 +251,8 @@ private bool InitializeReader(DbContext _, bool result) _resultCoordinator = new ResultCoordinator(); + _relationalQueryContext.InitializeStateManager(_performIdentityResolution); + return result; } @@ -267,6 +274,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly bool _performIdentityResolution; private readonly CancellationToken _cancellationToken; private RelationalDataReader _dataReader; @@ -285,6 +293,7 @@ public AsyncEnumerator( _shaper = queryingEnumerable._shaper; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; _cancellationToken = cancellationToken; } @@ -368,6 +377,8 @@ private async Task InitializeReaderAsync(DbContext _, bool result, Cancell _resultCoordinator = new ResultCoordinator(); + _relationalQueryContext.InitializeStateManager(_performIdentityResolution); + return result; } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index bbe57eee56c..4d25c2dd01e 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -99,7 +99,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(projectionColumns, typeof(IReadOnlyList)), Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), - Expression.Constant(_logger)); + Expression.Constant(_logger), + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); } } } diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 62f19a7200c..bbced8eb07f 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2530,6 +2530,44 @@ public static IQueryable AsTracking( ? source.AsTracking() : source.AsNoTracking(); + internal static readonly MethodInfo PerformIdentityResolutionMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetTypeInfo().GetDeclaredMethod(nameof(PerformIdentityResolution)); + + /// + /// + /// Returns a new query where the change tracker will not track any of the entities that are return + /// but query will perform identity resolution in results. + /// If the entity instances are modified, this will not be detected by the change tracker and + /// will not persist those changes to the database. + /// + /// + /// Perfoming identity resolution in no tracking query can be useful if creating several instances of same object is + /// expensive than re-using same object. It should be kept in mind that in order to perform identity resolution, + /// all previously created objects will be kept in memory which can lead to high memory usage. + /// + /// + /// The type of entity being queried. + /// The source query. + /// + /// A new query where the result set will not be tracked by the context. + /// + public static IQueryable PerformIdentityResolution( + [NotNull] this IQueryable source) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: PerformIdentityResolutionMethodInfo.MakeGenericMethod(typeof(TEntity)), + arguments: source.Expression)) + : source; + } + #endregion #region Tagging diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 2fd40222e8b..c16f87809a7 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2628,6 +2628,12 @@ public static string MissingInverseManyToManyNavigation([CanBeNull] object princ GetString("MissingInverseManyToManyNavigation", nameof(principalEntityType), nameof(declaringEntityType)), principalEntityType, declaringEntityType); + /// + /// InitializeStateManager method has been called multiple times on current query context. This method is intended to be called only once before query enumeration starts. + /// + public static string QueryContextAlreadyInitializedStateManager + => GetString("QueryContextAlreadyInitializedStateManager"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index c9cbb00e898..e32292d21df 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1387,4 +1387,7 @@ Unable to set up a many-to-many relationship between the entity types '{principalEntityType}' and '{declaringEntityType}' because one of the navigations was not specified. Please provide a navigation property in the HasMany() call. + + InitializeStateManager method has been called multiple times on current query context. This method is intended to be called only once before query enumeration starts. + \ No newline at end of file diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 4729f0ba43e..4dcb185386f 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -297,14 +297,15 @@ outerKey is NewArrayExpression newArrayExpression /// private sealed class IncludeExpandingExpressionVisitor : ExpandingExpressionVisitor { - private readonly bool _isTracking; + private readonly bool _allowCycles; public IncludeExpandingExpressionVisitor( NavigationExpandingExpressionVisitor navigationExpandingExpressionVisitor, NavigationExpansionExpression source) : base(navigationExpandingExpressionVisitor, source) { - _isTracking = navigationExpandingExpressionVisitor._queryCompilationContext.IsTracking; + _allowCycles = navigationExpandingExpressionVisitor._queryCompilationContext.IsTracking + || navigationExpandingExpressionVisitor._queryCompilationContext.PerformIdentityResolution; } protected override Expression VisitExtension(Expression extensionExpression) @@ -459,7 +460,7 @@ private bool ReconstructAnonymousType(Expression currentRoot, NewExpression newE private Expression ExpandInclude(Expression root, EntityReference entityReference) { - if (!_isTracking) + if (!_allowCycles) { VerifyNoCycles(entityReference.IncludePaths); } diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 3d8f0ac7553..126a642b9bc 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -135,6 +135,15 @@ private Expression ExtractQueryMetadata(MethodCallExpression methodCallExpressio return visitedExpression; } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.PerformIdentityResolutionMethodInfo) + { + var visitedExpression = Visit(methodCallExpression.Arguments[0]); + _queryCompilationContext.IsTracking = false; + _queryCompilationContext.PerformIdentityResolution = true; + + return visitedExpression; + } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.TagWithMethodInfo) { var visitedExpression = Visit(methodCallExpression.Arguments[0]); diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index 916af416660..f677184b008 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -92,6 +92,16 @@ public QueryCompilationContext( /// A value indicating whether it is tracking query. /// public virtual bool IsTracking { get; internal set; } + + /// + /// + /// A value indicating whether query should perform identity resolution for the entities returned in the results. + /// + /// + /// This value is only set when is to . + /// + /// + public virtual bool PerformIdentityResolution { get; internal set; } /// /// A value indicating whether the underlying server query needs to pre-buffer all data. /// diff --git a/src/EFCore/Query/QueryContext.cs b/src/EFCore/Query/QueryContext.cs index 537df6d21af..60380aa0fe1 100644 --- a/src/EFCore/Query/QueryContext.cs +++ b/src/EFCore/Query/QueryContext.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Threading; @@ -21,6 +22,7 @@ namespace Microsoft.EntityFrameworkCore.Query public abstract class QueryContext : IParameterValues { private readonly IDictionary _parameterValues = new Dictionary(); + private IStateManager _stateManager; /// /// @@ -50,16 +52,6 @@ protected QueryContext( /// protected virtual QueryContextDependencies Dependencies { 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. - /// - [EntityFrameworkInternal] - public virtual IStateManager StateManager - => Dependencies.StateManager; - /// /// Sets the navigation as loaded. /// @@ -70,7 +62,7 @@ public virtual void SetNavigationIsLoaded([NotNull] object entity, [NotNull] INa Check.NotNull(entity, nameof(entity)); Check.NotNull(navigation, nameof(navigation)); - Dependencies.StateManager.TryGetEntry(entity).SetIsLoaded(navigation); + _stateManager.TryGetEntry(entity).SetIsLoaded(navigation); } /// @@ -144,6 +136,36 @@ public virtual void AddParameter(string name, object value) _parameterValues.Add(name, value); } + /// + /// Initializes the to be used with this QueryContext. + /// + /// Whether a stand-alone should be created to perform identity resolution. + public virtual void InitializeStateManager(bool standAlone = false) + { + if (_stateManager != null) + { + throw new InvalidOperationException(CoreStrings.QueryContextAlreadyInitializedStateManager); + } + + _stateManager = standAlone + ? new StateManager(Dependencies.StateManager.Dependencies) + : Dependencies.StateManager; + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + public virtual InternalEntityEntry TryGetEntry( + [NotNull] IKey key, + [NotNull] object[] keyValues, + bool throwOnNullKey, + out bool hasNullKey) + => _stateManager.TryGetEntry(key, keyValues, throwOnNullKey, out hasNullKey); + /// /// 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 @@ -155,6 +177,6 @@ public virtual InternalEntityEntry StartTracking( [NotNull] IEntityType entityType, [NotNull] object entity, ValueBuffer valueBuffer) - => StateManager.StartTrackingFromQuery(entityType, entity, valueBuffer); + => _stateManager.StartTrackingFromQuery(entityType, entity, valueBuffer); } } diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs index 1aad0949850..e90d9178144 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs @@ -64,7 +64,8 @@ protected ShapedQueryCompilingExpressionVisitor( _entityMaterializerInjectingExpressionVisitor = new EntityMaterializerInjectingExpressionVisitor( dependencies.EntityMaterializerSource, - queryCompilationContext.IsTracking); + queryCompilationContext.IsTracking, + queryCompilationContext.PerformIdentityResolution); _constantVerifyingExpressionVisitor = new ConstantVerifyingExpressionVisitor(dependencies.TypeMappingSource); @@ -288,9 +289,6 @@ private static readonly ConstructorInfo _valueBufferConstructor private static readonly PropertyInfo _dbContextMemberInfo = typeof(QueryContext).GetProperty(nameof(QueryContext.Context)); - private static readonly PropertyInfo _stateManagerMemberInfo - = typeof(QueryContext).GetProperty(nameof(QueryContext.StateManager)); - private static readonly PropertyInfo _entityMemberInfo = typeof(InternalEntityEntry).GetProperty(nameof(InternalEntityEntry.Entity)); @@ -298,7 +296,7 @@ private static readonly PropertyInfo _entityTypeMemberInfo = typeof(InternalEntityEntry).GetProperty(nameof(InternalEntityEntry.EntityType)); private static readonly MethodInfo _tryGetEntryMethodInfo - = typeof(IStateManager).GetTypeInfo().GetDeclaredMethods(nameof(IStateManager.TryGetEntry)) + = typeof(QueryContext).GetTypeInfo().GetDeclaredMethods(nameof(QueryContext.TryGetEntry)) .Single(mi => mi.GetParameters().Length == 4); private static readonly MethodInfo _startTrackingMethodInfo @@ -306,21 +304,25 @@ private static readonly MethodInfo _startTrackingMethodInfo nameof(QueryContext.StartTracking), new[] { typeof(IEntityType), typeof(object), typeof(ValueBuffer) }); private readonly IEntityMaterializerSource _entityMaterializerSource; - private readonly bool _trackQueryResults; + private readonly bool _trackingQuery; + private readonly bool _queryStateMananger; private readonly ISet _visitedEntityTypes = new HashSet(); private int _currentEntityIndex; public EntityMaterializerInjectingExpressionVisitor( - IEntityMaterializerSource entityMaterializerSource, bool trackQueryResults) + IEntityMaterializerSource entityMaterializerSource, + bool trackingQuery, + bool performIdentityResolution) { _entityMaterializerSource = entityMaterializerSource; - _trackQueryResults = trackQueryResults; + _trackingQuery = trackingQuery; + _queryStateMananger = trackingQuery || performIdentityResolution; } public Expression Inject(Expression expression) { var result = Visit(expression); - if (_trackQueryResults) + if (_trackingQuery) { foreach (var entityType in _visitedEntityTypes) { @@ -386,7 +388,7 @@ private Expression ProcessEntityShaper(EntityShaperExpression entityShaperExpres instanceVariable, Expression.Constant(null, entityType.ClrType))); - if (_trackQueryResults + if (_queryStateMananger && primaryKey != null) { var entryVariable = Expression.Variable(typeof(InternalEntityEntry), "entry" + _currentEntityIndex); @@ -398,9 +400,7 @@ private Expression ProcessEntityShaper(EntityShaperExpression entityShaperExpres Expression.Assign( entryVariable, Expression.Call( - Expression.MakeMemberAccess( - QueryCompilationContext.QueryContextParameter, - _stateManagerMemberInfo), + QueryCompilationContext.QueryContextParameter, _tryGetEntryMethodInfo, Expression.Constant(primaryKey), Expression.NewArrayInit( @@ -513,7 +513,7 @@ private Expression MaterializeEntity( expressions.Add(Expression.Assign(instanceVariable, materializationExpression)); - if (_trackQueryResults + if (_queryStateMananger && entityType.FindPrimaryKey() != null) { foreach (var et in entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive())) @@ -560,7 +560,7 @@ private BlockExpression CreateFullMaterializeExpression( var materializer = _entityMaterializerSource .CreateMaterializeExpression(concreteEntityType, "instance", materializationContextVariable); - if (_trackQueryResults + if (_queryStateMananger && concreteEntityType.ShadowPropertyCount() > 0) { var valueBufferExpression = Expression.Call( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 551870c001a..8d3059fd816 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4113,6 +4113,18 @@ FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""CustomerID""] IN (""ALFKI"") OR (c[""CustomerID""] = null)))"); } + [ConditionalTheory(Skip = "Issue #17246")] + public override Task Perform_identity_resolution_reuses_same_instances(bool async) + { + return base.Perform_identity_resolution_reuses_same_instances(async); + } + + [ConditionalTheory(Skip = "Issue #17246")] + public override Task Perform_identity_resolution_reuses_same_instances_across_joins(bool async) + { + return base.Perform_identity_resolution_reuses_same_instances_across_joins(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index b8f75696638..085fac0434d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -451,6 +451,12 @@ public override async Task Can_query_indexer_property_on_owned_collection(bool i AssertSql(" "); } + [ConditionalTheory(Skip = "No SelectMany, No Ability to Include navigation back to owner #17246")] + public override Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(bool async) + { + return base.NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs index 82dd184b187..01f12db1bbb 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs @@ -3736,6 +3736,55 @@ orderby c.CustomerID Assert.Equal(2, result.Orders.First().OrderDetails.Count); } + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task NoTracking_Include_with_cycles_throws(bool useString, bool async) + { + using var context = CreateContext(); + var query = (from o in (useString + ? context.Orders.Include("Customer.Orders") + : context.Orders.Include(o => o.Customer.Orders)) + where o.OrderID < 10800 + select o) + .AsNoTracking(); + + Assert.Equal( + CoreStrings.IncludeWithCycle("Customer", "Orders"), + async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(bool useString, bool async) + { + using var context = CreateContext(); + var query = (from o in (useString + ? context.Orders.Include("Customer.Orders") + : context.Orders.Include(o => o.Customer.Orders)) + where o.OrderID < 10800 + select o) + .PerformIdentityResolution(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Empty(context.ChangeTracker.Entries()); + foreach (var order in result) + { + Assert.NotNull(order.Customer); + Assert.Same(order, order.Customer.Orders.First(o => o.OrderID == order.OrderID)); + } + } + private static void CheckIsLoaded( NorthwindContext context, Customer customer, diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index f44f0131546..a93cd8a45a3 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -5844,7 +5844,7 @@ public Dto(string value) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Null_parameter_name_works(bool isAsync) + public virtual async Task Null_parameter_name_works(bool async) { using var context = CreateContext(); var customerDbSet = context.Set().AsQueryable(); @@ -5858,7 +5858,7 @@ public virtual async Task Null_parameter_name_works(bool isAsync) var query = ((IAsyncQueryProvider)customerDbSet.Provider).CreateQuery(queryExpression); - var result = isAsync + var result = async ? (await query.ToListAsync()) : query.ToList(); @@ -5872,5 +5872,52 @@ public virtual Task String_include_on_incorrect_property_throws(bool async) return Assert.ThrowsAsync( async () => await AssertQuery(async, ss => ss.Set().Include("OrderDetails"))); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Perform_identity_resolution_reuses_same_instances(bool async) + { + using var context = CreateContext(); + var orderIds = context.Customers.Where(c => c.CustomerID == "ALFKI") + .SelectMany(c => c.Orders).Select(o => o.OrderID).ToList(); + + var query = context.Orders.Where(o => orderIds.Contains(o.OrderID)) + .Select(o => o.Customer) + .PerformIdentityResolution(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(6, result.Count); + var firstCustomer = result[0]; + Assert.All(result, t => Assert.Same(firstCustomer, t)); + Assert.Empty(context.ChangeTracker.Entries()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Perform_identity_resolution_reuses_same_instances_across_joins(bool async) + { + using var context = CreateContext(); + + var query = (from c in context.Customers.Where(c => c.CustomerID.StartsWith("A")) + join o in context.Orders.Where(o => o.OrderID < 10500).Include(o => o.Customer) + on c.CustomerID equals o.CustomerID + select new { c, o }) + .PerformIdentityResolution(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var arouts = result.Where(t => t.c.CustomerID == "AROUT").Select(t => t.c) + .Concat(result.Where(t => t.o.CustomerID == "AROUT").Select(t => t.o.Customer)) + .ToList(); + + var firstArout = arouts[0]; + Assert.All(arouts, t => Assert.Same(firstArout, t)); + Assert.Empty(context.ChangeTracker.Entries()); + } } } diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index 42d912258f8..f117d106de6 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -373,15 +373,25 @@ public virtual async Task Throw_for_owned_entities_without_owner_in_tracking_que Assert.Equal(4, result.Count); Assert.Empty(context.ChangeTracker.Entries()); - if (async) - { - await Assert.ThrowsAsync(() => asTrackingQuery.ToListAsync()); - } - else - { - Assert.Throws(() => asTrackingQuery.ToList()); - } + var message = async + ? (await Assert.ThrowsAsync(() => asTrackingQuery.ToListAsync())).Message + : Assert.Throws(() => asTrackingQuery.ToList()).Message; + Assert.Empty(context.ChangeTracker.Entries()); + Assert.Equal(CoreStrings.OwnedEntitiesCannotBeTrackedWithoutTheirOwner, message); + } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_entity_without_owner_does_not_throw_for_identity_resolution(bool async) + { + using var context = CreateContext(); + var query = context.Set().Select(e => e.PersonAddress).PerformIdentityResolution(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(4, result.Count); Assert.Empty(context.ChangeTracker.Entries()); } @@ -742,6 +752,39 @@ public virtual Task Can_query_indexer_property_on_owned_collection(bool isAsync) .Select(c => (string)c["Name"])); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task NoTracking_Include_with_cycles_throws(bool async) + { + using var context = CreateContext(); + var query = context.Set().SelectMany(op => op.Orders).Include(o => o.Client).AsNoTracking(); + + Assert.Equal( + CoreStrings.IncludeWithCycle("Client", "Orders"), + async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(bool async) + { + using var context = CreateContext(); + var query = context.Set().SelectMany(op => op.Orders).Include(o => o.Client).PerformIdentityResolution(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Empty(context.ChangeTracker.Entries()); + foreach (var order in result) + { + Assert.NotNull(order.Client); + Assert.Same(order, order.Client.Orders.First(o => o.Id == order.Id)); + } + } + protected virtual DbContext CreateContext() => Fixture.CreateContext(); public abstract class OwnedQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase diff --git a/tools/Resources.tt b/tools/Resources.tt index e61ed05a3da..d66d35d84a3 100644 --- a/tools/Resources.tt +++ b/tools/Resources.tt @@ -184,7 +184,7 @@ namespace <#= model.Namespace.EndsWith(".Internal") ? model.Namespace : (model.N { #> [Obsolete] -<# +<# } #> public static EventDefinition<#= genericTypes #> <#= resource.Name #>([NotNull] IDiagnosticsLogger logger) @@ -226,7 +226,7 @@ namespace <#= model.Namespace.EndsWith(".Internal") ? model.Namespace : (model.N { result.AccessModifier = (string)Session["AccessModifier"]; }; - + if (Session.ContainsKey("LoggingDefinitionsClass")) { result.LoggingDefinitionsClass = (string)Session["LoggingDefinitionsClass"]; @@ -266,7 +266,7 @@ namespace <#= model.Namespace.EndsWith(".Internal") ? model.Namespace : (model.N result.Class = Path.GetFileNameWithoutExtension(resourceFile); result.DiagnosticsClass = result.Class.Replace("Strings", "Resources"); - + result.ResourceName = resourceNamespace + "." + result.Class; using (var reader = new ResXResourceReader(resourceFile))