Skip to content

Commit 9d217c7

Browse files
authored
Fix to #31365 - Query: add support for projecting JSON entities that have been composed on (#31391)
Adding new materialization path for JSON entities that have been converted to query roots - they look like normal entity, but materialization is somewhat different. We need to prune synthesized key properties from materializer code (as they are not associated with any column, but rather made up on the fly) Also added code to include all the child navigations. For normal entities we have IncludeExpressions in the shaper expression, but for JSON we prune all includes and build them in the materializer. Fix is to recognize the scenario (entity projection but the entity is mapped to JSON) during apply projection phase and generate all the necessary info needed rather than just dictionary of property to index maps like we do for regular entity. Then when we build materializer we use that info to prune the synthesized properties from key check and re-use existing include JSON entity code to add child navigations. Fixes #31365
1 parent 30319d5 commit 9d217c7

File tree

11 files changed

+807
-74
lines changed

11 files changed

+807
-74
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.Internal;
5+
6+
/// <summary>
7+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
8+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
9+
/// any release. You should only use it directly in your code with extreme caution and knowing that
10+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
11+
/// </summary>
12+
public readonly struct QueryableJsonProjectionInfo
13+
{
14+
/// <summary>
15+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
16+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
17+
/// any release. You should only use it directly in your code with extreme caution and knowing that
18+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// </summary>
20+
public QueryableJsonProjectionInfo(
21+
Dictionary<IProperty, int> propertyIndexMap,
22+
List<(JsonProjectionInfo, INavigation)> childrenProjectionInfo)
23+
{
24+
PropertyIndexMap = propertyIndexMap;
25+
ChildrenProjectionInfo = childrenProjectionInfo;
26+
}
27+
28+
/// <summary>
29+
/// Map between entity properties and corresponding column indexes.
30+
/// </summary>
31+
/// <remarks>
32+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
33+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
34+
/// any release. You should only use it directly in your code with extreme caution and knowing that
35+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
36+
/// </remarks>
37+
public IDictionary<IProperty, int> PropertyIndexMap { get; }
38+
39+
/// <summary>
40+
/// Information needed to construct each child JSON entity.
41+
/// - JsonProjection info (same one we use for simple JSON projection),
42+
/// - navigation between parent and the child JSON entity.
43+
/// </summary>
44+
/// <remarks>
45+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
46+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
47+
/// any release. You should only use it directly in your code with extreme caution and knowing that
48+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
49+
/// </remarks>
50+
public IList<(JsonProjectionInfo JsonProjectionInfo, INavigation Navigation)> ChildrenProjectionInfo { get; }
51+
}

src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,11 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
433433

434434
if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression)
435435
{
436-
var propertyMap = (IDictionary<IProperty, int>)GetProjectionIndex(projectionBindingExpression);
436+
var projectionIndex = GetProjectionIndex(projectionBindingExpression);
437+
var propertyMap = projectionIndex is IDictionary<IProperty, int>
438+
? (IDictionary<IProperty, int>)projectionIndex
439+
: ((QueryableJsonProjectionInfo)projectionIndex).PropertyIndexMap;
440+
437441
_materializationContextBindings[parameterExpression] = propertyMap;
438442
_entityTypeIdentifyingExpressionInfo[parameterExpression] =
439443
// If single entity type is being selected in hierarchy then we use the value directly else we store the offset
@@ -535,6 +539,50 @@ protected override Expression VisitExtension(Expression extensionExpression)
535539
visitedShaperResultParameter,
536540
shaper.Type);
537541
}
542+
else if (GetProjectionIndex(projectionBindingExpression) is QueryableJsonProjectionInfo queryableJsonEntityProjectionInfo)
543+
{
544+
if (_isTracking)
545+
{
546+
throw new InvalidOperationException(
547+
RelationalStrings.JsonEntityOrCollectionProjectedAtRootLevelInTrackingQuery(nameof(EntityFrameworkQueryableExtensions.AsNoTracking)));
548+
}
549+
550+
// json entity converted to query root and projected
551+
var entityParameter = Parameter(shaper.Type);
552+
_variables.Add(entityParameter);
553+
var entityMaterializationExpression = (BlockExpression)_parentVisitor.InjectEntityMaterializers(shaper);
554+
555+
var mappedProperties = queryableJsonEntityProjectionInfo.PropertyIndexMap.Keys.ToList();
556+
var rewrittenEntityMaterializationExpression = new QueryableJsonEntityMaterializerRewriter(mappedProperties)
557+
.Rewrite(entityMaterializationExpression);
558+
559+
var visitedEntityMaterializationExpression = Visit(rewrittenEntityMaterializationExpression);
560+
_expressions.Add(Assign(entityParameter, visitedEntityMaterializationExpression));
561+
562+
foreach (var childProjectionInfo in queryableJsonEntityProjectionInfo.ChildrenProjectionInfo)
563+
{
564+
var (jsonReaderDataVariable, keyValuesParameter) = JsonShapingPreProcess(
565+
childProjectionInfo.JsonProjectionInfo,
566+
childProjectionInfo.Navigation.TargetEntityType,
567+
childProjectionInfo.Navigation.IsCollection);
568+
569+
var shaperResult = CreateJsonShapers(
570+
childProjectionInfo.Navigation.TargetEntityType,
571+
nullable: true,
572+
jsonReaderDataVariable,
573+
keyValuesParameter,
574+
parentEntityExpression: entityParameter,
575+
navigation: childProjectionInfo.Navigation);
576+
577+
var visitedShaperResult = Visit(shaperResult);
578+
579+
_includeExpressions.Add(visitedShaperResult);
580+
}
581+
582+
accessor = CompensateForCollectionMaterialization(
583+
entityParameter,
584+
shaper.Type);
585+
}
538586
else
539587
{
540588
var entityParameter = Parameter(shaper.Type);
@@ -2141,6 +2189,62 @@ ParameterExpression ExtractAndCacheNonConstantJsonArrayElementAccessValue(int in
21412189
}
21422190
}
21432191

2192+
private sealed class QueryableJsonEntityMaterializerRewriter : ExpressionVisitor
2193+
{
2194+
private readonly List<IProperty> _mappedProperties;
2195+
2196+
public QueryableJsonEntityMaterializerRewriter(List<IProperty> mappedProperties)
2197+
{
2198+
_mappedProperties = mappedProperties;
2199+
}
2200+
2201+
public BlockExpression Rewrite(BlockExpression jsonEntityShaperMaterializer)
2202+
=> (BlockExpression)VisitBlock(jsonEntityShaperMaterializer);
2203+
2204+
protected override Expression VisitBinary(BinaryExpression binaryExpression)
2205+
{
2206+
// here we try to pattern match part of the shaper code that checks if key values are null
2207+
// if they are all non-null then we generate the entity
2208+
// problem for JSON entities is that some of the keys are synthesized and should be omitted
2209+
// if the key is one of the mapped ones, we leave the expression as is, otherwise replace with Constant(true)
2210+
// i.e. removing it
2211+
if (binaryExpression is
2212+
{
2213+
NodeType: ExpressionType.NotEqual,
2214+
Left: MethodCallExpression
2215+
{
2216+
Method: { IsGenericMethod: true } method,
2217+
Arguments: [_, _, ConstantExpression { Value: IProperty property }]
2218+
},
2219+
Right: ConstantExpression { Value: null }
2220+
}
2221+
&& method.GetGenericMethodDefinition() == Infrastructure.ExpressionExtensions.ValueBufferTryReadValueMethod)
2222+
{
2223+
return _mappedProperties.Contains(property)
2224+
? binaryExpression
2225+
: Constant(true);
2226+
}
2227+
2228+
return base.VisitBinary(binaryExpression);
2229+
}
2230+
2231+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
2232+
{
2233+
if (methodCallExpression is
2234+
{
2235+
Method: { IsGenericMethod: true } method,
2236+
Arguments: [_, _, ConstantExpression { Value: IProperty property }]
2237+
}
2238+
&& method.GetGenericMethodDefinition() == Infrastructure.ExpressionExtensions.ValueBufferTryReadValueMethod
2239+
&& !_mappedProperties.Contains(property))
2240+
{
2241+
return Default(methodCallExpression.Type);
2242+
}
2243+
2244+
return base.VisitMethodCall(methodCallExpression);
2245+
}
2246+
}
2247+
21442248
private static LambdaExpression GenerateFixup(
21452249
Type entityType,
21462250
Type relatedEntityType,

0 commit comments

Comments
 (0)