From 68ff04e871dadf637d57fdb4a8283bf81f029f67 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 14 Feb 2024 14:43:45 -0800 Subject: [PATCH] Fix to #33073 - JSON columns throws Invalid token type: 'StartObject' exception with AsNoTrackingWithIdentityResolution() Problem was that we were treating NoTrackingWithIdentityResolution queries as regular NoTracking in the context of JSON queries. However due to how JSON is materialized (streaming + nested includes are part of the parent materialization, rather than each entity materialized separately) we have special provisions when ChangeTracker is being used. In NoTrackingWithIdentityResolution, ChangeTracker is being used but the materializer didn't adjust to that, which lead to errors. Fix is to generate JSON materializer code based on whether query uses Change Tracker rather than if it's a Tracking/NoTracking query. However, this uncovered some issue with NoTrackingWithIdentityResolution - depending on the order in which entities are processed during materialization, they could cause data corruption (wrong order) when materializing JSON collections. Also, using queryable operators on JSON collections may cause errors or data corruption - we don't propagate key values of those queries to the materializer, so those entities end up with null keys. Adding a validator that makes sure that entities are visited in the correct order and issues exception instructing what to do, if the order is wrong. Also we disable usage of queryable operators, due to the issue mentioned above, and cases where parameters are being used to access collection element in the navigatio chain. For cases with parameters, we can't tell if the value is the same or different so can't properly validate those cases. Two different parameters can have the same value, leading to the same entity being materialized, but when we analyze their JSON path, those paths look different. Fixes #33073 --- .../Properties/RelationalStrings.Designer.cs | 24 + .../Properties/RelationalStrings.resx | 9 + ...sitor.ShaperProcessingExpressionVisitor.cs | 263 ++++- src/EFCore/Query/QueryContext.cs | 8 +- .../Query/JsonQueryTestBase.cs | 911 +++++++++++++++++- .../Query/AdHocMiscellaneousQueryTestBase.cs | 24 +- .../AdHocMiscellaneousQuerySqlServerTest.cs | 88 ++ .../Query/JsonQuerySqlServerTest.cs | 283 +++++- 8 files changed, 1561 insertions(+), 49 deletions(-) diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 64c5fcd3b60..41c457641f7 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1169,6 +1169,30 @@ public static string JsonErrorExtractingJsonProperty(object? entityType, object? public static string JsonNodeMustBeHandledByProviderSpecificVisitor => GetString("JsonNodeMustBeHandledByProviderSpecificVisitor"); + /// + /// Using parameter to access the element of a JSON collection '{entityTypeName}' is not supported when using '{asNoTrackingWithIdentityResolution}'. Use constant, or project the entire JSON entity collection instead. + /// + public static string JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution(object? entityTypeName, object? asNoTrackingWithIdentityResolution) + => string.Format( + GetString("JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution", nameof(entityTypeName), nameof(asNoTrackingWithIdentityResolution)), + entityTypeName, asNoTrackingWithIdentityResolution); + + /// + /// When using '{asNoTrackingWithIdentityResolution}' entities mapped to JSON must be projected in a particular order. Project entire collection of entities '{entityTypeName}' before its individual elements. + /// + public static string JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution(object? entityTypeName, object? asNoTrackingWithIdentityResolution) + => string.Format( + GetString("JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution", nameof(entityTypeName), nameof(asNoTrackingWithIdentityResolution)), + entityTypeName, asNoTrackingWithIdentityResolution); + + /// + /// Projecting queryable operations on JSON collection is not supported for '{asNoTrackingWithIdentityResolution}'. + /// + public static string JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(object? asNoTrackingWithIdentityResolution) + => string.Format( + GetString("JsonProjectingQueryableOperationNoTrackingWithIdentityResolution", nameof(asNoTrackingWithIdentityResolution)), + asNoTrackingWithIdentityResolution); + /// /// The JSON property name should only be configured on nested owned navigations. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index e791520a692..61ccb93f4e1 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -559,6 +559,15 @@ This node should be handled by provider-specific sql generator. + + Using parameter to access the element of a JSON collection '{entityTypeName}' is not supported for '{asNoTrackingWithIdentityResolution}'. Use constant, or project the entire JSON entity collection instead. + + + When using '{asNoTrackingWithIdentityResolution}' entities mapped to JSON must be projected in a particular order. Project entire collection of entities '{entityTypeName}' before its individual elements. + + + Projecting queryable operations on JSON collection is not supported for '{asNoTrackingWithIdentityResolution}'. + The JSON property name should only be configured on nested owned navigations. diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index b6f5aa95364..d15341160d4 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -80,6 +80,7 @@ private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisit private readonly RelationalShapedQueryCompilingExpressionVisitor _parentVisitor; private readonly ISet? _tags; private readonly bool _isTracking; + private readonly bool _queryStateManager; private readonly bool _isAsync; private readonly bool _splitQuery; private readonly bool _detailedErrorsEnabled; @@ -186,6 +187,8 @@ private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisit _generateCommandCache = true; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; + _queryStateManager = parentVisitor.QueryCompilationContext.QueryTrackingBehavior is QueryTrackingBehavior.TrackAll + or QueryTrackingBehavior.NoTrackingWithIdentityResolution; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; _splitQuery = splitQuery; @@ -212,6 +215,8 @@ private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisit _generateCommandCache = false; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; + _queryStateManager = parentVisitor.QueryCompilationContext.QueryTrackingBehavior is QueryTrackingBehavior.TrackAll + or QueryTrackingBehavior.NoTrackingWithIdentityResolution; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; _splitQuery = false; } @@ -241,6 +246,8 @@ private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisit _generateCommandCache = true; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; _isTracking = parentVisitor.QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll; + _queryStateManager = parentVisitor.QueryCompilationContext.QueryTrackingBehavior is QueryTrackingBehavior.TrackAll + or QueryTrackingBehavior.NoTrackingWithIdentityResolution; _isAsync = parentVisitor.QueryCompilationContext.IsAsync; _splitQuery = true; @@ -311,6 +318,17 @@ private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisit _containsCollectionMaterialization = new CollectionShaperFindingExpressionVisitor() .ContainsCollectionMaterialization(shaperExpression); + // for NoTrackingWithIdentityResolution we need to make sure we see JSON entities in the correct order + // specifically, if we project JSON collection, it needs to be projected before any individual element from that collection + // otherwise we store JSON entities in incorrect order in the Change Tracker, leading to possible data corruption + // we only need to do this once, on top level + // see issue #33073 for more context + if (_queryStateManager && !_isTracking && collectionId == 0) + { + var jsonCorrectOrderOfEntitiesForChangeTrackerValidator = new JsonCorrectOrderOfEntitiesForChangeTrackerValidator(_selectExpression); + jsonCorrectOrderOfEntitiesForChangeTrackerValidator.Validate(shaperExpression); + } + if (!_containsCollectionMaterialization) { var result = Visit(shaperExpression); @@ -1391,7 +1409,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var rewrittenEntityShaperMaterializer = new JsonEntityMaterializerRewriter( entityType, - _isTracking, + _queryStateManager, jsonReaderDataShaperLambdaParameter, innerShapersMap, innerFixupMap, @@ -1514,7 +1532,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp private sealed class JsonEntityMaterializerRewriter : ExpressionVisitor { private readonly IEntityType _entityType; - private readonly bool _isTracking; + private readonly bool _queryStateManager; private readonly ParameterExpression _jsonReaderDataParameter; private readonly IDictionary _innerShapersMap; private readonly IDictionary _innerFixupMap; @@ -1530,7 +1548,7 @@ private sealed class JsonEntityMaterializerRewriter : ExpressionVisitor public JsonEntityMaterializerRewriter( IEntityType entityType, - bool isTracking, + bool queryStateManager, ParameterExpression jsonReaderDataParameter, IDictionary innerShapersMap, IDictionary innerFixupMap, @@ -1538,7 +1556,7 @@ private sealed class JsonEntityMaterializerRewriter : ExpressionVisitor IDiagnosticsLogger queryLogger) { _entityType = entityType; - _isTracking = isTracking; + _queryStateManager = queryStateManager; _jsonReaderDataParameter = jsonReaderDataParameter; _innerShapersMap = innerShapersMap; _innerFixupMap = innerFixupMap; @@ -1702,9 +1720,9 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression) finalBlockExpressions.Add(propertyAssignmentReplacer.Visit(jsonEntityTypeInitializerBlockExpression)); } - // Fixup is only needed for non-tracking queries, in case of tracking - ChangeTracker does the job + // Fixup is only needed for non-tracking queries, in case of tracking (or NoTrackingWithIdentityResolution) - ChangeTracker does the job // or for empty/null collections of a tracking queries. - if (_isTracking) + if (_queryStateManager) { ProcessFixup(_trackingInnerFixupMap); } @@ -1881,7 +1899,7 @@ protected override Expression VisitConditional(ConditionalExpression conditional // the code here re-arranges the existing materializer so that even if we find parent in the change tracker // we still process all the child navigations, it's just that we use the parent instance from change tracker, rather than create new one #pragma warning disable EF1001 // Internal EF Core API usage. - if (_isTracking + if (_queryStateManager && visited is ConditionalExpression { Test: BinaryExpression @@ -2673,5 +2691,236 @@ public bool ContainsCollectionMaterialization(Expression expression) return base.Visit(expression); } } + + private sealed class JsonCorrectOrderOfEntitiesForChangeTrackerValidator(SelectExpression selectExpression) : ExpressionVisitor + { + private bool _insideCollection; + private bool _insideInclude; + private readonly List<(IEntityType JsonEntityType, List<(IProperty? KeyProperty, int? ConstantKeyValue, int? KeyProjectionIndex)> KeyAccessInfo)> _projectedKeyAccessInfos = []; + private readonly List _includedJsonEntityTypes = []; + + public void Validate(Expression expression) + { + // this visitor makes sure that we don't end up with data corruption in NoTrackingWithIdentityResolution mode + // In order to avoid it, we need to make sure entities land in Change Tracker in a correct order + // This is because until we have ordered collections, when we populate collection from Change Tracker, we do it in the same + // order that the entities landed there. So if, say, 3rd element of the collection is read first, if subsequently the entire + // collection is projected, that third element will appear at the start, and only after elements 1 and 2 will show up + _insideCollection = false; + _insideInclude = false; + Visit(expression); + + // all projections that are contained in any of the included are safe - we process all the includes first + // so the entries are guaranteed to land in Change Tracker in the right order + // for all the remaining - entities deeper in the structure must appear after entities/collections that are "shallower" + // i.e. entity.MyJsonCollection must appear before entity.MyJsonCollection[2] + // we can verify that by comparing key access infos + if (_projectedKeyAccessInfos.Count > 0 && _includedJsonEntityTypes.Count > 0) + { + for (var i = _projectedKeyAccessInfos.Count - 1; i >= 0; i--) + { + if (_includedJsonEntityTypes.Any(t => t == _projectedKeyAccessInfos[i].JsonEntityType + || _projectedKeyAccessInfos[i].JsonEntityType.IsInOwnershipPath(t))) + { + _projectedKeyAccessInfos.RemoveAt(i); + } + } + } + + // if there is only one thing projected, we are good no matter what + // if we project one thing only, the result will always land in correct order in ChangeTracker + if (_projectedKeyAccessInfos.Count > 1) + { + var projectedKeyAccessInfos = _projectedKeyAccessInfos.ToList(); + var i = 0; + + do + { + var outerKeyAccessInfo = projectedKeyAccessInfos[i].KeyAccessInfo; + var outerJsonEntityType = projectedKeyAccessInfos[i].JsonEntityType; + + // first key access info element is guaranteed to have KeyProperty - it's "borrowed" the PK of the owner entity + //var entityTypeName = outerKeyAccessInfo[0].KeyProperty!.DeclaringType.Name; + + // accessing collection element using parameter is not supported for NoTrackingWithIdentityResolution + // we can't always tell if the path is the same or not - e.g. when we use two different parameters with the same value + // or a constant and a parameter with the same value as the constant we would think it's different, but they are the same + // so we can't correctly flag this scenario as invalid and it could cause data corruption + // so we just disable it altogether + // consider this query: + // var prm1 = 0; + // var prm2 = 0 + // entities.Select(x => new + // { + // One = x.JsonCollection[prm1].NestedCollection[1], + // Two = x.JsonCollection[prm2].NestedCollection + // }) + if (outerKeyAccessInfo.Any(x => x.KeyProperty == null && x.KeyProjectionIndex != null)) + { + throw new InvalidOperationException(RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + outerJsonEntityType.DisplayName(), nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution))); + } + + for (var j = projectedKeyAccessInfos.Count -1; j > i; j--) + { + var innerKeyAccessInfo = projectedKeyAccessInfos[j].KeyAccessInfo; + var innerJsonEntityType = projectedKeyAccessInfos[j].JsonEntityType; + + var different = false; + for (var k = 0; k < Math.Min(outerKeyAccessInfo.Count, innerKeyAccessInfo.Count); k++) + { + if (!KeyAccessInfoElementEqual(outerKeyAccessInfo[k], innerKeyAccessInfo[k])) + { + different = true; + break; + } + } + + // if shared path is the same, we are ok if full paths are the same or if the outer path is shorter than inner + // in that case the inner entry can be removed from the list - it will land in ChangeTracker in the correct order + // if outer path is longer however, there is risk of data corruption so we throw + // if common paths are different, we don't do anything - outer and inner are different they won't clash in ChangeTracker + // just continue processing, inner will eventually become outer and we will validate if all is correct with it + if (!different) + { + if (outerJsonEntityType != innerJsonEntityType + && !innerJsonEntityType.IsInOwnershipPath(outerJsonEntityType) + && !outerJsonEntityType.IsInOwnershipPath(innerJsonEntityType)) + { + // inner and outer are on different ownership paths - they are not related so they won't clash + continue; + } + + if ((outerJsonEntityType == innerJsonEntityType || innerJsonEntityType.IsInOwnershipPath(outerJsonEntityType)) + && outerKeyAccessInfo.Count <= innerKeyAccessInfo.Count) + { + // outer and inner are on same ownership paths and outer is the owner of inner + // this is good - we can remove inner from the list now, because it will be materialized correctly + projectedKeyAccessInfos.RemoveAt(j); + continue; + } + + throw new InvalidOperationException(RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + outerJsonEntityType.DisplayName(), + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution))); + } + } + + i++; + } + while (i < projectedKeyAccessInfos.Count); + } + + static bool KeyAccessInfoElementEqual( + (IProperty? KeyProperty, int? ConstantKeyValue, int? KeyProjectionIndex) first, + (IProperty? KeyProperty, int? ConstantKeyValue, int? KeyProjectionIndex) second) + { + if (first.KeyProperty != null != (second.KeyProperty != null)) + { + return false; + } + + if (first.ConstantKeyValue != second.ConstantKeyValue) + { + return false; + } + + // key property itself could be different, as long as they map to the same index in data reader, it's the same key + // this could be a problem when index is a parameter (two different parameters but same value), + // but we disabled that scenario already + return first.KeyProjectionIndex == second.KeyProjectionIndex; + } + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is RelationalCollectionShaperExpression collectionShaperExpression) + { + var insideCollection = _insideCollection; + _insideCollection = true; + Visit(collectionShaperExpression.InnerShaper); + _insideCollection = insideCollection; + + return collectionShaperExpression; + } + + if (extensionExpression is RelationalSplitCollectionShaperExpression splitCollectionShaperExpression) + { + var insideCollection = _insideCollection; + _insideCollection = true; + Visit(splitCollectionShaperExpression.InnerShaper); + _insideCollection = insideCollection; + + return splitCollectionShaperExpression; + } + + if (extensionExpression is IncludeExpression includeExpression) + { + var insideInclude = _insideInclude; + _insideInclude = true; + Visit(includeExpression.NavigationExpression); + _insideInclude = insideInclude; + } + + if (extensionExpression is StructuralTypeShaperExpression { ValueBufferExpression: ProjectionBindingExpression entityProjectionBindingExpression } entityShaperExpression) + { + var entityProjection = selectExpression.GetProjection(entityProjectionBindingExpression).GetConstantValue(); + + if (entityProjection is QueryableJsonProjectionInfo || (_insideCollection && entityProjection is JsonProjectionInfo)) + { + throw new InvalidOperationException( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution))); + } + + if (entityProjection is JsonProjectionInfo jsonEntityProjectionInfo) + { + var jsonEntityType = (IEntityType)entityShaperExpression.StructuralType; + if (_insideInclude) + { + if (!_includedJsonEntityTypes.Contains(jsonEntityType)) + { + _includedJsonEntityTypes.Add(jsonEntityType); + } + } + else + { + _projectedKeyAccessInfos.Add((jsonEntityType, jsonEntityProjectionInfo.KeyAccessInfo)); + } + } + + return extensionExpression; + } + + if (extensionExpression is CollectionResultExpression { ProjectionBindingExpression: ProjectionBindingExpression collectionProjectionBindingExpression } collectionResultExpression) + { + var collectionProjection = selectExpression.GetProjection(collectionProjectionBindingExpression).GetConstantValue(); + if (collectionProjection is QueryableJsonProjectionInfo || (_insideCollection && collectionProjection is JsonProjectionInfo)) + { + throw new InvalidOperationException( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution))); + } + + if (collectionProjection is JsonProjectionInfo jsonCollectionProjectionInfo) + { + var jsonEntityType = collectionResultExpression.Navigation!.TargetEntityType; + if (_insideInclude) + { + if (!_includedJsonEntityTypes.Contains(jsonEntityType)) + { + _includedJsonEntityTypes.Add(jsonEntityType); + } + } + else + { + _projectedKeyAccessInfos.Add((jsonEntityType, jsonCollectionProjectionInfo.KeyAccessInfo)); + } + } + + return extensionExpression; + } + + return base.VisitExtension(extensionExpression); + } + } } } diff --git a/src/EFCore/Query/QueryContext.cs b/src/EFCore/Query/QueryContext.cs index 9237d7ff653..ef368d3a2f2 100644 --- a/src/EFCore/Query/QueryContext.cs +++ b/src/EFCore/Query/QueryContext.cs @@ -125,10 +125,10 @@ public virtual void InitializeStateManager(bool standAlone = false) /// [EntityFrameworkInternal] public virtual InternalEntityEntry? TryGetEntry( - IKey key, - object[] keyValues, - bool throwOnNullKey, - out bool hasNullKey) + IKey key, + object[] keyValues, + bool throwOnNullKey, + out bool hasNullKey) // InitializeStateManager will populate the field before calling here => _stateManager!.TryGetEntry(key, keyValues, throwOnNullKey, out hasNullKey); diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs index 78254b96b8b..4ae0bdfdaf7 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryTestBase.cs @@ -27,6 +27,13 @@ public virtual Task Basic_json_projection_owner_entity_NoTracking(bool async) async, ss => ss.Set().AsNoTracking()); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().AsNoTrackingWithIdentityResolution()); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owner_entity_duplicated(bool async) @@ -53,6 +60,19 @@ public virtual Task Basic_json_projection_owner_entity_duplicated_NoTracking(boo AssertEqual(e.Second, a.Second); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owner_entity_duplicated_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new { First = x, Second = x }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.First.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.First, a.First); + AssertEqual(e.Second, a.Second); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owner_entity_twice(bool async) @@ -79,6 +99,19 @@ public virtual Task Basic_json_projection_owner_entity_twice_NoTracking(bool asy AssertEqual(e.Second, a.Second); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owner_entity_twice_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => new { First = x, Second = x }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.First.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.First, a.First); + AssertEqual(e.Second, a.Second); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Project_json_reference_in_tracking_query_fails(bool async) @@ -137,6 +170,61 @@ public virtual Task Basic_json_projection_owned_reference_root(bool async) async, ss => ss.Set().Select(x => x.OwnedReferenceRoot).AsNoTracking()); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_root_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot).AsNoTrackingWithIdentityResolution()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_duplicated(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select( + x => new + { + Root1 = x.OwnedReferenceRoot, + Branch1 = x.OwnedReferenceRoot.OwnedReferenceBranch, + Root2 = x.OwnedReferenceRoot, + Branch2 = x.OwnedReferenceRoot.OwnedReferenceBranch, + }).AsNoTracking(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.Root1, a.Root1); + AssertEqual(e.Root2, a.Root2); + AssertEqual(e.Branch1, a.Branch1); + AssertEqual(e.Branch2, a.Branch2); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_duplicated_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select( + x => new + { + Root1 = x.OwnedReferenceRoot, + Branch1 = x.OwnedReferenceRoot.OwnedReferenceBranch, + Root2 = x.OwnedReferenceRoot, + Branch2 = x.OwnedReferenceRoot.OwnedReferenceBranch, + }).AsNoTrackingWithIdentityResolution(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.Root1, a.Root1); + AssertEqual(e.Root2, a.Root2); + AssertEqual(e.Branch1, a.Branch1); + AssertEqual(e.Branch2, a.Branch2); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owned_reference_duplicated2(bool async) @@ -163,7 +251,7 @@ public virtual Task Basic_json_projection_owned_reference_duplicated2(bool async [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Basic_json_projection_owned_reference_duplicated(bool async) + public virtual Task Basic_json_projection_owned_reference_duplicated2_NoTrackingWithIdentityResolution(bool async) => AssertQuery( async, ss => ss.Set() @@ -172,17 +260,17 @@ public virtual Task Basic_json_projection_owned_reference_duplicated(bool async) x => new { Root1 = x.OwnedReferenceRoot, - Branch1 = x.OwnedReferenceRoot.OwnedReferenceBranch, + Leaf1 = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf, Root2 = x.OwnedReferenceRoot, - Branch2 = x.OwnedReferenceRoot.OwnedReferenceBranch, - }).AsNoTracking(), + Leaf2 = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf, + }).AsNoTrackingWithIdentityResolution(), assertOrder: true, elementAsserter: (e, a) => { AssertEqual(e.Root1, a.Root1); AssertEqual(e.Root2, a.Root2); - AssertEqual(e.Branch1, a.Branch1); - AssertEqual(e.Branch2, a.Branch2); + AssertEqual(e.Leaf1, a.Leaf1); + AssertEqual(e.Leaf2, a.Leaf2); }); [ConditionalTheory] @@ -193,6 +281,14 @@ public virtual Task Basic_json_projection_owned_collection_root(bool async) ss => ss.Set().Select(x => x.OwnedCollectionRoot).AsNoTracking(), elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_collection_root_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedCollectionRoot).AsNoTrackingWithIdentityResolution(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owned_reference_branch(bool async) @@ -200,6 +296,13 @@ public virtual Task Basic_json_projection_owned_reference_branch(bool async) async, ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch).AsNoTracking()); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_reference_branch_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch).AsNoTrackingWithIdentityResolution()); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owned_collection_branch(bool async) @@ -208,6 +311,14 @@ public virtual Task Basic_json_projection_owned_collection_branch(bool async) ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedCollectionBranch).AsNoTracking(), elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_json_projection_owned_collection_branch_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select(x => x.OwnedReferenceRoot.OwnedCollectionBranch).AsNoTrackingWithIdentityResolution(), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Basic_json_projection_owned_reference_leaf(bool async) @@ -1384,7 +1495,8 @@ public virtual Task Json_branch_collection_distinct_and_other_collection(bool as .Select( x => new { - First = x.OwnedReferenceRoot.OwnedCollectionBranch.Distinct().ToList(), Second = x.EntityCollection.ToList() + First = x.OwnedReferenceRoot.OwnedCollectionBranch.Distinct().ToList(), + Second = x.EntityCollection.ToList() }) .AsNoTracking(), assertOrder: true, @@ -2589,4 +2701,789 @@ public virtual Task FromSql_on_entity_with_json_inheritance_project_reference_on ss => ss.Set().OrderBy(x => x.Id).Select(x => x.CollectionOnDerived), elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => (ee.Date, ee.Enum, ee.Fraction)), assertOrder: true); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_using_queryable_methods_on_top_of_JSON_collection_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Skip = x.OwnedCollectionRoot.Skip(1).ToList(), + Take = x.OwnedCollectionRoot.Take(2).ToList(), + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Skip, a.Skip); + AssertCollection(e.Take, a.Take); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_nested_collection_anonymous_projection_in_projection_NoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select( + x => x.OwnedCollectionRoot + .Select( + xx => xx.OwnedCollectionBranch.Select( + xxx => new + { + xxx.Date, + xxx.Enum, + xxx.Enums, + xxx.Fraction, + xxx.OwnedReferenceLeaf, + xxx.OwnedCollectionLeaf + }).ToList())) + .AsNoTrackingWithIdentityResolution(), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection( + e, a, ordered: true, elementAsserter: (ee, aa) => AssertCollection( + ee, aa, ordered: true, elementAsserter: (eee, aaa) => + { + AssertEqual(eee.Date, aaa.Date); + AssertEqual(eee.Enum, aaa.Enum); + AssertCollection(eee.Enums, aaa.Enums, ordered: true); + AssertEqual(eee.Fraction, aaa.Fraction); + AssertEqual(eee.OwnedReferenceLeaf, aaa.OwnedReferenceLeaf); + AssertCollection(eee.OwnedCollectionLeaf, aaa.OwnedCollectionLeaf, ordered: true); + }))))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 0; + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Duplicate, a.Duplicate); + AssertCollection(e.Original, a.Original, ordered: true); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_nested_collection_and_element_using_parameter_AsNoTrackingWithIdentityResolution2(bool async) + { + var prm1 = 0; + var prm2 = 0; + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm1].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm2].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Duplicate, a.Duplicate); + AssertCollection(e.Original, a.Original, ordered: true); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_through_collection_element_parameter_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var prm1 = 0; + var prm2 = 1; + + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm1].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm2].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 0; + + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_through_collection_element_parameter_projected_before_owner_nested_AsNoTrackingWithIdentityResolution2(bool async) + { + var prm1 = 0; + var prm2 = 0; + + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm1].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm2].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_through_collection_element_parameter_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 0; + + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingCollectionElementAccessedUsingParmeterNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_through_collection_element_constant_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_branch_collection_distinct_and_other_collection_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select( + x => new + { + First = x.EntityCollection.ToList(), + Second = x.OwnedReferenceRoot.OwnedCollectionBranch.Distinct().ToList() + }) + .AsNoTrackingWithIdentityResolution(), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e.First, a.First, ordered: true); + AssertCollection(e.Second, a.Second, elementSorter: ee => ee.Fraction); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_collection_SelectMany_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set() + .SelectMany(x => x.OwnedCollectionRoot) + .AsNoTrackingWithIdentityResolution(), + elementSorter: e => (e.Number, e.Name)))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_deduplication_with_collection_indexer_in_target_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 1; + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate1 = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Original = x.OwnedReferenceRoot, + Duplicate2 = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf[prm] + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate1, a.Duplicate1); + AssertEqual(e.Duplicate2, a.Duplicate2); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_nested_collection_and_element_wrong_order_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Duplicate, a.Duplicate); + AssertCollection(e.Original, a.Original, ordered: true); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_projected_before_entire_collection_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_projected_before_owner_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Original = x.OwnedReferenceRoot, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedCollectionBranch#JsonOwnedBranch", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Json_projection_second_element_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var message = (await Assert.ThrowsAsync(() => + AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf, + Parent = x.OwnedReferenceRoot.OwnedReferenceBranch, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }))).Message; + + Assert.Equal( + RelationalStrings.JsonProjectingEntitiesIncorrectOrderNoTrackingWithIdentityResolution( + "JsonEntityBasic.OwnedReferenceRoot#JsonOwnedRoot.OwnedReferenceBranch#JsonOwnedBranch.OwnedCollectionLeaf#JsonOwnedLeaf", + nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), + message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_collection_element_and_reference_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + CollectionElement = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Reference = x.OwnedReferenceRoot.OwnedReferenceBranch, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.CollectionElement, a.CollectionElement); + AssertEqual(e.Reference, a.Reference); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_nothing_interesting_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + x.Name + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_owner_entity_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + x + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.x, a.x); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_nested_collection_anonymous_projection_of_primitives_in_projection_NoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set() + .OrderBy(x => x.Id) + .Select( + x => x.OwnedCollectionRoot + .Select( + xx => xx.OwnedCollectionBranch.Select( + xxx => new + { + xxx.Date, + xxx.Enum, + xxx.Enums, + xxx.Fraction, + }).ToList())) + .AsNoTrackingWithIdentityResolution(), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection( + e, a, ordered: true, elementAsserter: (ee, aa) => AssertCollection( + ee, aa, ordered: true, elementAsserter: (eee, aaa) => + { + AssertEqual(eee.Date, aaa.Date); + AssertEqual(eee.Enum, aaa.Enum); + AssertCollection(eee.Enums, aaa.Enums, ordered: true); + AssertEqual(eee.Fraction, aaa.Fraction); + }))); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_second_element_through_collection_element_constant_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_reference_collection_and_collection_element_nested_AsNoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Reference = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedReferenceLeaf, + Collection = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + CollectionElement = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Reference, a.Reference); + AssertCollection(e.Collection, a.Collection, ordered: true); + AssertEqual(e.CollectionElement, a.CollectionElement); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_second_element_through_collection_element_parameter_correctly_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 1; + + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[prm], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_only_second_element_through_collection_element_constant_projected_nested_AsNoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Element = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Element, a.Element); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_only_second_element_through_collection_element_parameter_projected_nested_AsNoTrackingWithIdentityResolution(bool async) + { + var prm1 = 0; + var prm2 = 1; + + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Element = x.OwnedReferenceRoot.OwnedCollectionBranch[prm1].OwnedCollectionLeaf[prm2], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Element, a.Element); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_second_element_through_collection_element_constant_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[1].OwnedCollectionLeaf, + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Duplicate, a.Duplicate); + AssertCollection(e.Original, a.Original, ordered: true); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_nested_collection_and_element_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[0].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Duplicate, a.Duplicate); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_nested_collection_element_using_parameter_and_the_owner_in_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + var prm = 0; + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Original = x.OwnedReferenceRoot, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[prm].OwnedCollectionLeaf[1], + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_second_element_projected_before_owner_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedCollectionBranch[1], + Original = x.OwnedReferenceRoot, + Owned = x + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Original, a.Original); + AssertEqual(e.Duplicate, a.Duplicate); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Json_projection_second_element_projected_before_owner_nested_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + => AssertQuery( + async, + ss => ss.Set().Select( + x => new + { + x.Id, + Duplicate = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf[1], + Original = x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedCollectionLeaf, + Parent = x.OwnedReferenceRoot.OwnedReferenceBranch, + Owner = x + }).AsNoTrackingWithIdentityResolution(), + elementSorter: e => e.Id, + elementAsserter: (e, a) => + { + AssertEqual(e.Id, a.Id); + AssertEqual(e.Duplicate, a.Duplicate); + AssertCollection(e.Original, a.Original, ordered: true); + AssertEqual(e.Owner, a.Owner); + }); } diff --git a/test/EFCore.Specification.Tests/Query/AdHocMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocMiscellaneousQueryTestBase.cs index 505cb12e975..fe633bfc9dd 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocMiscellaneousQueryTestBase.cs @@ -160,20 +160,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet Customers { get; set; } public DbSet Postcodes { get; set; } - } - public class Customer - { - public int CustomerID { get; set; } - public string CustomerName { get; set; } - public int? PostcodeID { get; set; } - } + public class Customer + { + public int CustomerID { get; set; } + public string CustomerName { get; set; } + public int? PostcodeID { get; set; } + } - public class Postcode - { - public int PostcodeID { get; set; } - public string PostcodeValue { get; set; } - public string TownName { get; set; } + public class Postcode + { + public int PostcodeID { get; set; } + public string PostcodeValue { get; set; } + public string TownName { get; set; } + } } #endregion diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs index 143ca36cc88..0872f8a05bc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs @@ -2414,4 +2414,92 @@ ELSE NULL END = N'COUNTRY' """); } + + + [ConditionalFact] + public void Test_NTWIR_owned_type() + { + using (var ctx = new MyContext()) + { + ctx.Database.EnsureDeleted(); + ctx.Database.EnsureCreated(); + var e = new MyEntity + { + Owned = new OwnedRoot + { + Number = 10, + Nested = new List + { + new OwnedBranch { Foo = "f1", InnerNested = new List { new OwnedLeaf { Bar = 55 } } }, + new OwnedBranch { Foo = "f2", InnerNested = new List { new OwnedLeaf { Bar = 266 }, new OwnedLeaf { Bar = 277 } } } + + //new OwnedBranch { Foo = "f1" }, + //new OwnedBranch { Foo = "f2" } + } + } + }; + ctx.Entities.Add(e); + ctx.SaveChanges(); + } + + using (var ctx = new MyContext()) + { + var result = ctx.Entities + .OrderBy(x => x.Id) + .Select( + x => new + { + First = x.Owned.Nested.ToList(), + }) + .AsNoTrackingWithIdentityResolution().ToList(); + } + } + + public class MyContext : DbContext + { + public DbSet Entities { get; set; } + + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(x => x.Owned, b => + { + b.ToJson(); + b.OwnsMany(xx => xx.Nested, bb => bb.OwnsMany(xxx => xxx.InnerNested)); + //b.OwnsMany(xx => xx.Nested); + }); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Repro;Trusted_Connection=True;MultipleActiveResultSets=true"); + } + } + + public class MyEntity + { + public int Id { get; set; } + public string Name { get; set; } + + public OwnedRoot Owned { get; set; } + } + + public class OwnedRoot + { + public int Number { get; set; } + + public List Nested { get; set; } + } + + public class OwnedBranch + { + public string Foo { get; set; } + + public List InnerNested { get; set; } + } + + public class OwnedLeaf + { + public int Bar { get; set; } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 266ea31db78..6d00da96556 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -37,6 +37,17 @@ public override async Task Basic_json_projection_owner_entity_NoTracking(bool as """); } + public override async Task Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + public override async Task Basic_json_projection_owner_entity_duplicated(bool async) { await base.Basic_json_projection_owner_entity_duplicated(async); @@ -59,6 +70,17 @@ public override async Task Basic_json_projection_owner_entity_duplicated_NoTrack """); } + public override async Task Basic_json_projection_owner_entity_duplicated_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_duplicated_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[Id], [j].[Name], [j].[OwnedCollection], [j].[OwnedCollection] +FROM [JsonEntitiesSingleOwned] AS [j] +"""); + } + public override async Task Basic_json_projection_owner_entity_twice(bool async) { await base.Basic_json_projection_owner_entity_twice(async); @@ -81,6 +103,17 @@ public override async Task Basic_json_projection_owner_entity_twice_NoTracking(b """); } + public override async Task Basic_json_projection_owner_entity_twice_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owner_entity_twice_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + public override async Task Basic_json_projection_owned_reference_root(bool async) { await base.Basic_json_projection_owned_reference_root(async); @@ -92,6 +125,41 @@ public override async Task Basic_json_projection_owned_reference_root(bool async """); } + public override async Task Basic_json_projection_owned_reference_root_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_root_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[OwnedReferenceRoot], [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Basic_json_projection_owned_reference_duplicated(bool async) + { + await base.Basic_json_projection_owned_reference_duplicated(async); + + AssertSql( + """ +SELECT [j].[OwnedReferenceRoot], [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch') +FROM [JsonEntitiesBasic] AS [j] +ORDER BY [j].[Id] +"""); + } + + public override async Task Basic_json_projection_owned_reference_duplicated_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_duplicated_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[OwnedReferenceRoot], [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch') +FROM [JsonEntitiesBasic] AS [j] +ORDER BY [j].[Id] +"""); + } + public override async Task Basic_json_projection_owned_reference_duplicated2(bool async) { await base.Basic_json_projection_owned_reference_duplicated2(async); @@ -104,13 +172,13 @@ public override async Task Basic_json_projection_owned_reference_duplicated2(boo """); } - public override async Task Basic_json_projection_owned_reference_duplicated(bool async) + public override async Task Basic_json_projection_owned_reference_duplicated2_NoTrackingWithIdentityResolution(bool async) { - await base.Basic_json_projection_owned_reference_duplicated(async); + await base.Basic_json_projection_owned_reference_duplicated2_NoTrackingWithIdentityResolution(async); AssertSql( """ -SELECT [j].[OwnedReferenceRoot], [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch') +SELECT [j].[OwnedReferenceRoot], [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedReferenceLeaf'), [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedReferenceLeaf') FROM [JsonEntitiesBasic] AS [j] ORDER BY [j].[Id] """); @@ -127,6 +195,17 @@ public override async Task Basic_json_projection_owned_collection_root(bool asyn """); } + public override async Task Basic_json_projection_owned_collection_root_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_collection_root_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT [j].[OwnedCollectionRoot], [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + public override async Task Basic_json_projection_owned_reference_branch(bool async) { await base.Basic_json_projection_owned_reference_branch(async); @@ -138,6 +217,17 @@ public override async Task Basic_json_projection_owned_reference_branch(bool asy """); } + public override async Task Basic_json_projection_owned_reference_branch_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_reference_branch_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + public override async Task Basic_json_projection_owned_collection_branch(bool async) { await base.Basic_json_projection_owned_collection_branch(async); @@ -149,6 +239,17 @@ public override async Task Basic_json_projection_owned_collection_branch(bool as """); } + public override async Task Basic_json_projection_owned_collection_branch_NoTrackingWithIdentityResolution(bool async) + { + await base.Basic_json_projection_owned_collection_branch_NoTrackingWithIdentityResolution(async); + + AssertSql( + """ +SELECT JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch'), [j].[Id] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + public override async Task Basic_json_projection_owned_reference_leaf(bool async) { await base.Basic_json_projection_owned_reference_leaf(async); @@ -2725,8 +2826,6 @@ WHERE CAST(JSON_VALUE([j].[Reference], '$.StringYNConvertedToBool') AS bit) = CA """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_basic(bool async) { await base.FromSql_on_entity_with_json_basic(async); @@ -2740,8 +2839,6 @@ public override async Task FromSql_on_entity_with_json_basic(bool async) """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public virtual async Task FromSqlInterpolated_on_entity_with_json_with_predicate(bool async) { var parameter = new SqlParameter { ParameterName = "prm", Value = 1 }; @@ -2763,8 +2860,6 @@ public virtual async Task FromSqlInterpolated_on_entity_with_json_with_predicate """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_project_json_reference(bool async) { await base.FromSql_on_entity_with_json_project_json_reference(async); @@ -2778,8 +2873,6 @@ public override async Task FromSql_on_entity_with_json_project_json_reference(bo """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_project_json_collection(bool async) { await base.FromSql_on_entity_with_json_project_json_collection(async); @@ -2793,8 +2886,6 @@ public override async Task FromSql_on_entity_with_json_project_json_collection(b """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_on_base(bool async) { await base.FromSql_on_entity_with_json_inheritance_on_base(async); @@ -2808,8 +2899,6 @@ public override async Task FromSql_on_entity_with_json_inheritance_on_base(bool """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_on_derived(bool async) { await base.FromSql_on_entity_with_json_inheritance_on_derived(async); @@ -2824,8 +2913,6 @@ public override async Task FromSql_on_entity_with_json_inheritance_on_derived(bo """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_project_reference_on_base(bool async) { await base.FromSql_on_entity_with_json_inheritance_project_reference_on_base(async); @@ -2840,8 +2927,6 @@ public override async Task FromSql_on_entity_with_json_inheritance_project_refer """); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] public override async Task FromSql_on_entity_with_json_inheritance_project_reference_on_derived(bool async) { await base.FromSql_on_entity_with_json_inheritance_project_reference_on_derived(async); @@ -2857,6 +2942,166 @@ public override async Task FromSql_on_entity_with_json_inheritance_project_refer """); } + + public override async Task Json_projection_nothing_interesting_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nothing_interesting_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], [j].[Name] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_owner_entity_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_owner_entity_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_nested_collection_anonymous_projection_of_primitives_in_projection_NoTrackingWithIdentityResolution(bool async) + { + await base.Json_nested_collection_anonymous_projection_of_primitives_in_projection_NoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], [s].[key], [s].[c], [s].[c0], [s].[c1], [s].[c2], [s].[key0] +FROM [JsonEntitiesBasic] AS [j] +OUTER APPLY ( + SELECT [o].[key], CAST(JSON_VALUE([o0].[value], '$.Date') AS datetime2) AS [c], CAST(JSON_VALUE([o0].[value], '$.Enum') AS int) AS [c0], JSON_QUERY([o0].[value], '$.Enums') AS [c1], CAST(JSON_VALUE([o0].[value], '$.Fraction') AS decimal(18,2)) AS [c2], [o0].[key] AS [key0], CAST([o].[key] AS int) AS [c3], CAST([o0].[key] AS int) AS [c4] + FROM OPENJSON([j].[OwnedCollectionRoot], '$') AS [o] + OUTER APPLY OPENJSON(JSON_QUERY([o].[value], '$.OwnedCollectionBranch'), '$') AS [o0] +) AS [s] +ORDER BY [j].[Id], [s].[c3], [s].[key], [s].[c4] +"""); + } + + public override async Task Json_projection_second_element_through_collection_element_constant_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_through_collection_element_constant_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[1]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_reference_collection_and_collection_element_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_reference_collection_and_collection_element_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedReferenceLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[1]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)] + public override async Task Json_projection_second_element_through_collection_element_parameter_correctly_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_through_collection_element_parameter_correctly_projected_after_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +@__prm_0='1' + +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[' + CAST(@__prm_0 AS nvarchar(max)) + ']'), @__prm_0 +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_only_second_element_through_collection_element_constant_projected_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_only_second_element_through_collection_element_constant_projected_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[1]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)] + public override async Task Json_projection_only_second_element_through_collection_element_parameter_projected_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_only_second_element_through_collection_element_parameter_projected_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +@__prm1_0='0' +@__prm2_1='1' + +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[' + CAST(@__prm1_0 AS nvarchar(max)) + '].OwnedCollectionLeaf[' + CAST(@__prm2_1 AS nvarchar(max)) + ']'), @__prm1_0, @__prm2_1 +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_second_element_through_collection_element_constant_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_through_collection_element_constant_different_values_projected_before_owner_nested_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[1]'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[1].OwnedCollectionLeaf') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_nested_collection_and_element_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_and_element_correct_order_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[0].OwnedCollectionLeaf[1]') +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsJsonPathExpressions)] + public override async Task Json_projection_nested_collection_element_using_parameter_and_the_owner_in_correct_order_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_nested_collection_element_using_parameter_and_the_owner_in_correct_order_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +@__prm_0='0' + +SELECT [j].[Id], [j].[OwnedReferenceRoot], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[' + CAST(@__prm_0 AS nvarchar(max)) + '].OwnedCollectionLeaf[1]'), @__prm_0 +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_second_element_projected_before_owner_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_as_well_as_root_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedCollectionBranch[1]'), [j].[OwnedReferenceRoot], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + + public override async Task Json_projection_second_element_projected_before_owner_nested_as_well_as_root_AsNoTrackingWithIdentityResolution(bool async) + { + await base.Json_projection_second_element_projected_before_owner_nested_as_well_as_root_AsNoTrackingWithIdentityResolution(async); + + AssertSql( +""" +SELECT [j].[Id], JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedCollectionLeaf[1]'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch.OwnedCollectionLeaf'), JSON_QUERY([j].[OwnedReferenceRoot], '$.OwnedReferenceBranch'), [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }