@@ -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