Skip to content

Commit 5f505fd

Browse files
authored
Support value types in complex JSON shaper (#36557)
Closes #36552
1 parent 76096c1 commit 5f505fd

File tree

13 files changed

+443
-127
lines changed

13 files changed

+443
-127
lines changed

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

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ private static readonly MethodInfo IncludeJsonEntityReferenceMethodInfo
7070
private static readonly MethodInfo IncludeJsonEntityCollectionMethodInfo
7171
= typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(IncludeJsonEntityCollection))!;
7272

73-
private static readonly MethodInfo MaterializeJsonEntityMethodInfo
74-
= typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonEntity))!;
73+
private static readonly MethodInfo MaterializeJsonStructuralTypeMethodInfo
74+
= typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonStructuralType))!;
75+
76+
private static readonly MethodInfo MaterializeJsonNullableValueStructuralTypeMethodInfo
77+
= typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonNullableValueStructuralType))!;
7578

7679
private static readonly MethodInfo MaterializeJsonEntityCollectionMethodInfo
7780
= typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonEntityCollection))!;
@@ -959,20 +962,64 @@ static async Task<RelationalDataReader> InitializeReaderAsync(
959962
/// doing so can result in application failures when updating to a new Entity Framework Core release.
960963
/// </summary>
961964
[EntityFrameworkInternal]
962-
public static TEntity? MaterializeJsonEntity<TEntity>(
965+
public static TStructural? MaterializeJsonStructuralType<TStructural>(
963966
QueryContext queryContext,
964967
object[]? keyPropertyValues,
965968
JsonReaderData? jsonReaderData,
966969
bool nullable,
967-
Func<QueryContext, object[]?, JsonReaderData, TEntity> shaper)
968-
where TEntity : class
970+
Func<QueryContext, object[]?, JsonReaderData, TStructural> shaper)
971+
{
972+
if (jsonReaderData == null)
973+
{
974+
return nullable
975+
? default
976+
: throw new InvalidOperationException(
977+
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TStructural).Name));
978+
}
979+
980+
var manager = new Utf8JsonReaderManager(jsonReaderData, queryContext.QueryLogger);
981+
var tokenType = manager.CurrentReader.TokenType;
982+
983+
switch (tokenType)
984+
{
985+
case JsonTokenType.Null:
986+
return nullable
987+
? default
988+
: throw new InvalidOperationException(
989+
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TStructural).Name));
990+
991+
case not JsonTokenType.StartObject:
992+
throw new InvalidOperationException(
993+
CoreStrings.JsonReaderInvalidTokenType(tokenType.ToString()));
994+
}
995+
996+
manager.CaptureState();
997+
var result = shaper(queryContext, keyPropertyValues, jsonReaderData);
998+
999+
return result;
1000+
}
1001+
1002+
/// <summary>
1003+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1004+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1005+
/// any release. You should only use it directly in your code with extreme caution and knowing that
1006+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1007+
/// </summary>
1008+
[EntityFrameworkInternal]
1009+
public static TStructural? MaterializeJsonNullableValueStructuralType<TStructural>(
1010+
QueryContext queryContext,
1011+
object[]? keyPropertyValues,
1012+
JsonReaderData? jsonReaderData,
1013+
bool nullable,
1014+
Func<QueryContext, object[]?, JsonReaderData, TStructural> shaper)
1015+
where TStructural : struct
9691016
{
9701017
if (jsonReaderData == null)
9711018
{
9721019
return nullable
9731020
? null
9741021
: throw new InvalidOperationException(
975-
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TEntity).Name));
1022+
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TStructural).Name));
9761023
}
9771024

9781025
var manager = new Utf8JsonReaderManager(jsonReaderData, queryContext.QueryLogger);
@@ -984,7 +1031,7 @@ static async Task<RelationalDataReader> InitializeReaderAsync(
9841031
return nullable
9851032
? null
9861033
: throw new InvalidOperationException(
987-
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TEntity).Name));
1034+
RelationalStrings.JsonRequiredEntityWithNullJson(typeof(TStructural).Name));
9881035

9891036
case not JsonTokenType.StartObject:
9901037
throw new InvalidOperationException(
@@ -1081,16 +1128,14 @@ static async Task<RelationalDataReader> InitializeReaderAsync(
10811128
/// doing so can result in application failures when updating to a new Entity Framework Core release.
10821129
/// </summary>
10831130
[EntityFrameworkInternal]
1084-
public static void IncludeJsonEntityReference<TIncludingEntity, TIncludedEntity>(
1131+
public static void IncludeJsonEntityReference<TStructural, TRelatedStructural>(
10851132
QueryContext queryContext,
10861133
object[]? keyPropertyValues,
10871134
JsonReaderData? jsonReaderData,
1088-
TIncludingEntity entity,
1089-
Func<QueryContext, object[]?, JsonReaderData, TIncludedEntity> innerShaper,
1090-
Action<TIncludingEntity, TIncludedEntity> fixup,
1135+
TStructural structuralType,
1136+
Func<QueryContext, object[]?, JsonReaderData, TRelatedStructural> innerShaper,
1137+
Action<TStructural, TRelatedStructural> fixup,
10911138
bool performFixup)
1092-
where TIncludingEntity : class
1093-
where TIncludedEntity : class
10941139
{
10951140
if (jsonReaderData == null)
10961141
{
@@ -1114,7 +1159,7 @@ public static void IncludeJsonEntityReference<TIncludingEntity, TIncludedEntity>
11141159

11151160
if (performFixup)
11161161
{
1117-
fixup(entity, included);
1162+
fixup(structuralType, included);
11181163
}
11191164
}
11201165

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

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,7 +1672,7 @@ private Expression CreateJsonShapers(
16721672
var elementFixup = Lambda(
16731673
Block(
16741674
typeof(void),
1675-
AssignReferenceRelationship(
1675+
AssignStructuralProperty(
16761676
innerFixupCollectionElementParameter,
16771677
innerFixupParentParameter,
16781678
inverseNavigation)),
@@ -1709,7 +1709,7 @@ private Expression CreateJsonShapers(
17091709
{
17101710
var fixup = GenerateReferenceFixupForJson(
17111711
structuralType.ClrType,
1712-
relatedStructuralType.ClrType,
1712+
nestedRelationship.ClrType,
17131713
nestedRelationship,
17141714
inverseNavigation);
17151715

@@ -1847,8 +1847,27 @@ private Expression CreateJsonShapers(
18471847
return materializeJsonEntityCollectionMethodCall;
18481848
}
18491849

1850+
1851+
// Return the materializer for this JSON object, including null checks which would return null.
1852+
MethodInfo method;
1853+
1854+
if (relationship is not null && Nullable.GetUnderlyingType(relationship.ClrType) is { } underlyingType)
1855+
{
1856+
// The association property into which we're assigning has a nullable value type, so generate
1857+
// a materializer that returns that nullable value type (note that the shaperLambda that
1858+
// we pass itself always returns a non-nullable value (the null checks are outside of it.))
1859+
Check.DebugAssert(nullable, "On non-nullable relationship but the relationship's ClrType is Nullable<T>");
1860+
Check.DebugAssert(underlyingType == structuralType.ClrType);
1861+
1862+
method = MaterializeJsonNullableValueStructuralTypeMethodInfo.MakeGenericMethod(structuralType.ClrType);
1863+
}
1864+
else
1865+
{
1866+
method = MaterializeJsonStructuralTypeMethodInfo.MakeGenericMethod(structuralType.ClrType);
1867+
}
1868+
18501869
var materializedRootJsonEntity = Call(
1851-
MaterializeJsonEntityMethodInfo.MakeGenericMethod(structuralType.ClrType),
1870+
method,
18521871
QueryCompilationContext.QueryContextParameter,
18531872
keyValuesParameter,
18541873
jsonReaderDataParameter,
@@ -1969,9 +1988,9 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression)
19691988

19701989
var managerVariable = Variable(typeof(Utf8JsonReaderManager), "jsonReaderManager");
19711990
var tokenTypeVariable = Variable(typeof(JsonTokenType), "tokenType");
1972-
var jsonEntityTypeVariable = (ParameterExpression)jsonEntityTypeInitializerBlock.Expressions[^1];
1991+
var jsonStructuralTypeVariable = (ParameterExpression)jsonEntityTypeInitializerBlock.Expressions[^1];
19731992

1974-
Debug.Assert(jsonEntityTypeVariable.Type == structuralType.ClrType);
1993+
Debug.Assert(jsonStructuralTypeVariable.Type == structuralType.ClrType);
19751994

19761995
var finalBlockVariables = new List<ParameterExpression>
19771996
{
@@ -2024,7 +2043,7 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression)
20242043
// - navigation fixups
20252044
// - entity instance variable that is returned as end result
20262045
var propertyAssignmentReplacer = new ValueBufferTryReadValueMethodsReplacer(
2027-
jsonEntityTypeVariable, propertyAssignmentMap);
2046+
jsonStructuralTypeVariable, propertyAssignmentMap);
20282047

20292048
if (body.Expressions[0] is BinaryExpression
20302049
{
@@ -2051,7 +2070,7 @@ protected override Expression VisitSwitch(SwitchExpression switchExpression)
20512070
// or for empty/null collections of a tracking queries.
20522071
ProcessFixup(queryStateManager ? trackingInnerFixupMap : innerFixupMap);
20532072

2054-
finalBlockExpressions.Add(jsonEntityTypeVariable);
2073+
finalBlockExpressions.Add(jsonStructuralTypeVariable);
20552074

20562075
return Block(
20572076
finalBlockVariables,
@@ -2063,18 +2082,35 @@ void ProcessFixup(IDictionary<string, LambdaExpression> fixupMap)
20632082
{
20642083
var navigationEntityParameter = _navigationVariableMap[fixup.Key];
20652084

2066-
// we need to add null checks before we run fixup logic. For regular entities, whose fixup is done as part of the "Materialize*" method
2067-
// the checks are done there (same will be done for the "optimized" scenario, where we populate properties directly rather than store in variables)
2068-
// but in this case fixups are standalone, so the null safety must be added by us directly
2069-
finalBlockExpressions.Add(
2070-
IfThen(
2071-
NotEqual(
2072-
jsonEntityTypeVariable,
2073-
Constant(null, jsonEntityTypeVariable.Type)),
2074-
Invoke(
2075-
fixup.Value,
2076-
jsonEntityTypeVariable,
2077-
_navigationVariableMap[fixup.Key])));
2085+
// Inject the fixup code for each property; we have this as a set of lambdas in the fixup map.
2086+
// In the normal case, simply Invoke the lambda, passing it the structural type to be fixed up as a parameter.
2087+
// This unfortunately doesn't work on value types (where a copy would be mutated), so for them,
2088+
// we unwrap the lambda and integrate its body directly.
2089+
// We should ideally do this for all cases (no need for the extra lambda Invoke), but there are some issues around us writing
2090+
// to readonly fields.
2091+
if (jsonStructuralTypeVariable.Type.IsValueType /*&& Nullable.GetUnderlyingType(jsonStructuralTypeVariable.Type) is null*/)
2092+
{
2093+
var fixupBody = ReplacingExpressionVisitor.Replace(
2094+
originals: [fixup.Value.Parameters[0], fixup.Value.Parameters[1]],
2095+
replacements: [jsonStructuralTypeVariable, _navigationVariableMap[fixup.Key]],
2096+
fixup.Value.Body);
2097+
2098+
finalBlockExpressions.Add(fixupBody);
2099+
}
2100+
else
2101+
{
2102+
// If the structural type being fixed up is nullable, then we need to add null checks before we run fixup logic.
2103+
// For regular entities, whose fixup is done as part of the "Materialize*" method, the checks are done there
2104+
// (the same will be done for the "optimized" scenario, where we populate properties directly rather than store in variables).
2105+
// But in this case fixups are standalone, so the null safety must be added here.
2106+
finalBlockExpressions.Add(
2107+
IfThen(
2108+
NotEqual(jsonStructuralTypeVariable, Constant(null, jsonStructuralTypeVariable.Type)),
2109+
Invoke(
2110+
fixup.Value,
2111+
jsonStructuralTypeVariable,
2112+
_navigationVariableMap[fixup.Key])));
2113+
}
20782114
}
20792115
}
20802116
}
@@ -2756,7 +2792,7 @@ private LambdaExpression GenerateFixup(
27562792
expressions.Add(
27572793
relationship.IsCollection
27582794
? AddToCollectionRelationship(entityParameter, relatedEntityParameter, relationship)
2759-
: AssignReferenceRelationship(entityParameter, relatedEntityParameter, relationship));
2795+
: AssignStructuralProperty(entityParameter, relatedEntityParameter, relationship));
27602796
}
27612797

27622798
if (inverseNavigation != null
@@ -2765,26 +2801,26 @@ private LambdaExpression GenerateFixup(
27652801
expressions.Add(
27662802
inverseNavigation.IsCollection
27672803
? AddToCollectionRelationship(relatedEntityParameter, entityParameter, inverseNavigation)
2768-
: AssignReferenceRelationship(relatedEntityParameter, entityParameter, inverseNavigation));
2804+
: AssignStructuralProperty(relatedEntityParameter, entityParameter, inverseNavigation));
27692805
}
27702806

27712807
return Lambda(Block(typeof(void), expressions), entityParameter, relatedEntityParameter);
27722808
}
27732809

27742810
private static LambdaExpression GenerateReferenceFixupForJson(
2775-
Type entityType,
2776-
Type relatedEntityType,
2811+
Type clrType,
2812+
Type relatedClrType,
27772813
IPropertyBase relationship,
27782814
INavigationBase? inverseNavigation)
27792815
{
2780-
var entityParameter = Parameter(entityType);
2781-
var relatedEntityParameter = Parameter(relatedEntityType);
2816+
var entityParameter = Parameter(clrType);
2817+
var relatedEntityParameter = Parameter(relatedClrType);
27822818
var expressions = new List<Expression>();
27832819

27842820
if (!relationship.IsShadowProperty())
27852821
{
27862822
expressions.Add(
2787-
AssignReferenceRelationship(
2823+
AssignStructuralProperty(
27882824
entityParameter,
27892825
relatedEntityParameter,
27902826
relationship));
@@ -2794,7 +2830,7 @@ private static LambdaExpression GenerateReferenceFixupForJson(
27942830
&& !inverseNavigation.IsShadowProperty())
27952831
{
27962832
expressions.Add(
2797-
AssignReferenceRelationship(
2833+
AssignStructuralProperty(
27982834
relatedEntityParameter,
27992835
entityParameter,
28002836
inverseNavigation));
@@ -2821,11 +2857,21 @@ public static void InverseCollectionFixup<TCollectionElement, TEntity>(
28212857
}
28222858
}
28232859

2824-
private static Expression AssignReferenceRelationship(
2825-
ParameterExpression entity,
2826-
ParameterExpression relatedEntity,
2827-
IPropertyBase relationship)
2828-
=> entity.MakeMemberAccess(relationship.GetMemberInfo(forMaterialization: true, forSet: true)).Assign(relatedEntity);
2860+
private static Expression AssignStructuralProperty(
2861+
ParameterExpression structuralType,
2862+
ParameterExpression relatedStructuralType,
2863+
IPropertyBase structuralProperty)
2864+
{
2865+
var setter = structuralProperty.GetMemberInfo(forMaterialization: true, forSet: true);
2866+
2867+
// If we're assigning a value complex type to a nullable complex property, add an upcast for typing
2868+
var assignee = structuralProperty.ClrType.IsNullableValueType()
2869+
&& structuralProperty.ClrType.UnwrapNullableType() == relatedStructuralType.Type
2870+
? Convert(relatedStructuralType, structuralProperty.ClrType)
2871+
: (Expression)relatedStructuralType;
2872+
2873+
return structuralType.MakeMemberAccess(setter).Assign(assignee);
2874+
}
28292875

28302876
private Expression GetOrCreateCollectionObjectLambda(Type entityType, IPropertyBase relationship)
28312877
{

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2885,14 +2885,14 @@ public static Expression GenerateComplexPropertyShaperExpression(
28852885
complexType.GetJsonPropertyName()
28862886
?? throw new UnreachableException($"No JSON property name for complex property {complexProperty.Name}"),
28872887
tableAlias,
2888-
complexProperty.ClrType,
2888+
complexProperty.ClrType.UnwrapNullableType(),
28892889
typeMapping: containerColumn.StoreTypeMapping,
28902890
isComplexTypeNullable)
28912891
: new ColumnExpression(
28922892
containerColumn.Name,
28932893
tableAlias,
28942894
containerColumn,
2895-
complexProperty.ClrType,
2895+
complexProperty.ClrType.UnwrapNullableType(),
28962896
containerColumn.StoreTypeMapping,
28972897
isComplexTypeNullable);
28982898

0 commit comments

Comments
 (0)