Skip to content

Commit 6cf70ef

Browse files
committed
Inject JSON shaper code before the entity gets tracked
Closes #36433
1 parent b53641e commit 6cf70ef

File tree

8 files changed

+344
-326
lines changed

8 files changed

+344
-326
lines changed

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

Lines changed: 190 additions & 155 deletions
Large diffs are not rendered by default.

src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,27 @@ private static Expression CreateReaderColumnsExpression(
484484
return result;
485485
}
486486

487+
/// <summary>
488+
/// Called after a structural type is materialized, but before it's handed off to the change tracker.
489+
/// Here we inject the JSON shapers for any complex JSON properties the type has.
490+
/// </summary>
491+
/// <remarks>
492+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
493+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
494+
/// any release. You should only use it directly in your code with extreme caution and knowing that
495+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
496+
/// </remarks>
497+
public override void AddStructuralTypeInitialization(
498+
StructuralTypeShaperExpression shaper,
499+
ParameterExpression instanceVariable,
500+
List<ParameterExpression> variables,
501+
List<Expression> expressions)
502+
{
503+
Check.DebugAssert(_currentShaperProcessor is not null);
504+
505+
_currentShaperProcessor.ProcessTopLevelComplexJsonProperties(shaper, instanceVariable, expressions);
506+
}
507+
487508
private Expression CreateRelationalCommandResolverExpression(Expression queryExpression)
488509
{
489510
// In the regular case, we generate code that accesses the RelationalCommandCache (which invokes the 2nd part of the

src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ IServiceProperty serviceProperty
140140
=> serviceProperty.ParameterBinding.BindToParameter(bindingInfo),
141141

142142
IComplexProperty { IsCollection: true } complexProperty
143-
=> Expression.Default(complexProperty.ClrType), // Initialize collections to null, they'll be populated separately
143+
=> Default(complexProperty.ClrType), // Initialize collections to null, they'll be populated separately
144144

145145
IComplexProperty complexProperty
146146
=> CreateMaterializeExpression(

src/EFCore/Query/LiftableConstantExpressionHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public static Expression BuildMemberAccessForEntityOrComplexType(
150150
/// any release. You should only use it directly in your code with extreme caution and knowing that
151151
/// doing so can result in application failures when updating to a new Entity Framework Core release.
152152
/// </summary>
153-
public static Expression<Func<MaterializerLiftableConstantContext, object>> BuildMemberAccessLambdaForEntityOrComplexType(
153+
public static Expression<Func<MaterializerLiftableConstantContext, object>> BuildMemberAccessLambdaForStructuralType(
154154
ITypeBase type)
155155
{
156156
var prm = Parameter(typeof(MaterializerLiftableConstantContext));

src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ protected ShapedQueryCompilingExpressionVisitor(
5555

5656
_structuralTypeMaterializerInjector =
5757
new StructuralTypeMaterializerInjector(
58+
this,
5859
dependencies.EntityMaterializerSource,
5960
dependencies.LiftableConstantFactory,
6061
queryCompilationContext.QueryTrackingBehavior,
@@ -233,12 +234,12 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio
233234
{
234235
{ Value: IEntityType entityTypeValue } => liftableConstantFactory.CreateLiftableConstant(
235236
constantExpression.Value,
236-
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForEntityOrComplexType(entityTypeValue),
237+
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForStructuralType(entityTypeValue),
237238
entityTypeValue.ShortName() + "EntityType",
238239
constantExpression.Type),
239240
{ Value: IComplexType complexTypeValue } => liftableConstantFactory.CreateLiftableConstant(
240241
constantExpression.Value,
241-
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForEntityOrComplexType(complexTypeValue),
242+
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForStructuralType(complexTypeValue),
242243
complexTypeValue.ShortName() + "ComplexType",
243244
constantExpression.Type),
244245
{ Value: IProperty propertyValue } => liftableConstantFactory.CreateLiftableConstant(
@@ -361,8 +362,20 @@ protected override Expression VisitExtension(Expression extensionExpression)
361362
}
362363
}
363364

365+
/// <summary>
366+
/// Called after a structural type is materialized, but before it's handed off to the change tracker.
367+
/// </summary>
368+
public virtual void AddStructuralTypeInitialization(
369+
StructuralTypeShaperExpression shaper,
370+
ParameterExpression instanceVariable,
371+
List<ParameterExpression> variables,
372+
List<Expression> expressions)
373+
{
374+
}
375+
364376
private sealed class StructuralTypeMaterializerInjector(
365-
IStructuralTypeMaterializerSource entityMaterializerSource,
377+
ShapedQueryCompilingExpressionVisitor shapedQueryCompiler,
378+
IStructuralTypeMaterializerSource materializerSource,
366379
ILiftableConstantFactory liftableConstantFactory,
367380
QueryTrackingBehavior queryTrackingBehavior,
368381
bool supportsPrecompiledQuery)
@@ -426,10 +439,10 @@ bool ContainsOwner(IEntityType? owner)
426439

427440
protected override Expression VisitExtension(Expression extensionExpression)
428441
=> extensionExpression is StructuralTypeShaperExpression shaper
429-
? ProcessEntityShaper(shaper)
442+
? ProcessStructuralTypeShaper(shaper)
430443
: base.VisitExtension(extensionExpression);
431444

432-
private Expression ProcessEntityShaper(StructuralTypeShaperExpression shaper)
445+
private Expression ProcessStructuralTypeShaper(StructuralTypeShaperExpression shaper)
433446
{
434447
_currentEntityIndex++;
435448

@@ -565,7 +578,7 @@ private Expression ProcessEntityShaper(StructuralTypeShaperExpression shaper)
565578
supportsPrecompiledQuery
566579
? liftableConstantFactory.CreateLiftableConstant(
567580
typeBase,
568-
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForEntityOrComplexType(typeBase),
581+
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForStructuralType(typeBase),
569582
typeBase.Name + "EntityType",
570583
typeof(IEntityType))
571584
: Constant(typeBase),
@@ -606,7 +619,7 @@ private Expression MaterializeEntity(
606619
ParameterExpression instanceVariable,
607620
ParameterExpression? entryVariable)
608621
{
609-
var typeBase = shaper.StructuralType;
622+
var structuralType = shaper.StructuralType;
610623

611624
var expressions = new List<Expression>();
612625
var variables = new List<ParameterExpression>();
@@ -626,7 +639,7 @@ private Expression MaterializeEntity(
626639
typeof(ISnapshot))
627640
: Constant(Snapshot.Empty, typeof(ISnapshot))));
628641

629-
var returnType = typeBase.ClrType;
642+
var returnType = structuralType.ClrType;
630643
var valueBufferExpression = Call(materializationContextVariable, MaterializationContext.GetValueBufferMethod);
631644

632645
var materializationConditionBody = ReplacingExpressionVisitor.Replace(
@@ -637,23 +650,23 @@ private Expression MaterializeEntity(
637650
var expressionContext = (returnType, materializationContextVariable, concreteEntityTypeVariable, shadowValuesVariable);
638651
expressions.Add(Assign(concreteEntityTypeVariable, materializationConditionBody));
639652

640-
var (primaryKey, concreteEntityTypes) = typeBase is IEntityType entityType
653+
var (primaryKey, concreteStructuralTypes) = structuralType is IEntityType entityType
641654
? (entityType.FindPrimaryKey(), entityType.GetConcreteDerivedTypesInclusive().Cast<ITypeBase>().ToArray())
642-
: (null, [typeBase]);
655+
: (null, [structuralType]);
643656

644-
var switchCases = new SwitchCase[concreteEntityTypes.Length];
645-
for (var i = 0; i < concreteEntityTypes.Length; i++)
657+
var switchCases = new SwitchCase[concreteStructuralTypes.Length];
658+
for (var i = 0; i < concreteStructuralTypes.Length; i++)
646659
{
647-
var concreteEntityType = concreteEntityTypes[i];
660+
var concreteStructuralType = concreteStructuralTypes[i];
648661
switchCases[i] = SwitchCase(
649-
CreateFullMaterializeExpression(concreteEntityTypes[i], expressionContext),
662+
CreateFullMaterializeExpression(concreteStructuralTypes[i], expressionContext),
650663
supportsPrecompiledQuery
651664
? liftableConstantFactory.CreateLiftableConstant(
652-
concreteEntityTypes[i],
653-
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForEntityOrComplexType(concreteEntityType),
654-
concreteEntityType.ShortName() + (typeBase is IEntityType ? "EntityType" : "ComplexType"),
655-
typeBase is IEntityType ? typeof(IEntityType) : typeof(IComplexType))
656-
: Constant(concreteEntityTypes[i], typeBase is IEntityType ? typeof(IEntityType) : typeof(IComplexType)));
665+
concreteStructuralTypes[i],
666+
LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForStructuralType(concreteStructuralType),
667+
concreteStructuralType.ShortName() + (structuralType is IEntityType ? "EntityType" : "ComplexType"),
668+
structuralType is IEntityType ? typeof(IEntityType) : typeof(IComplexType))
669+
: Constant(concreteStructuralTypes[i], structuralType is IEntityType ? typeof(IEntityType) : typeof(IComplexType)));
657670
}
658671

659672
var materializationExpression = Switch(
@@ -663,9 +676,11 @@ private Expression MaterializeEntity(
663676

664677
expressions.Add(Assign(instanceVariable, materializationExpression));
665678

679+
shapedQueryCompiler.AddStructuralTypeInitialization(shaper, instanceVariable, variables, expressions);
680+
666681
if (_queryStateManager && primaryKey is not null)
667682
{
668-
if (typeBase is IEntityType entityType2)
683+
if (structuralType is IEntityType entityType2)
669684
{
670685
foreach (var et in entityType2.GetAllBaseTypes().Concat(entityType2.GetDerivedTypesInclusive()))
671686
{
@@ -696,7 +711,7 @@ private Expression MaterializeEntity(
696711
}
697712

698713
private BlockExpression CreateFullMaterializeExpression(
699-
ITypeBase concreteTypeBase,
714+
ITypeBase concreteStructuralType,
700715
(Type ReturnType,
701716
ParameterExpression MaterializationContextVariable,
702717
ParameterExpression ConcreteEntityTypeVariable,
@@ -709,14 +724,14 @@ private BlockExpression CreateFullMaterializeExpression(
709724

710725
var blockExpressions = new List<Expression>(2);
711726

712-
var materializer = entityMaterializerSource
727+
var materializer = materializerSource
713728
.CreateMaterializeExpression(
714729
new StructuralTypeMaterializerSourceParameters(
715-
concreteTypeBase, "instance", queryTrackingBehavior), materializationContextVariable);
730+
concreteStructuralType, "instance", queryTrackingBehavior), materializationContextVariable);
716731

717732
// TODO: Properly support shadow properties for complex types #35613
718733
if (_queryStateManager
719-
&& concreteTypeBase is IRuntimeEntityType { ShadowPropertyCount: > 0 } runtimeEntityType)
734+
&& concreteStructuralType is IRuntimeEntityType { ShadowPropertyCount: > 0 } runtimeEntityType)
720735
{
721736
var valueBufferExpression = Call(
722737
materializationContextVariable, MaterializationContext.GetValueBufferMethod);

test/EFCore.Relational.Specification.Tests/Update/ComplexCollectionJsonUpdateTestBase.cs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public abstract class ComplexCollectionJsonUpdateTestBase<TFixture>(TFixture fix
1111
protected ComplexCollectionJsonContext CreateContext()
1212
=> (ComplexCollectionJsonContext)Fixture.CreateContext();
1313

14-
[ConditionalFact(Skip = "Issue #36433")]
14+
[ConditionalFact]
1515
public virtual Task Add_element_to_complex_collection_mapped_to_json()
1616
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
1717
CreateContext,
@@ -26,17 +26,10 @@ public virtual Task Add_element_to_complex_collection_mapped_to_json()
2626

2727
company.Contacts!.Add(new Contact { Name = "New Contact", PhoneNumbers = ["555-0000"] });
2828

29-
Assert.Equal("""
30-
CompanyWithComplexCollections {Id: 1} Unchanged
31-
Id: 1 PK
32-
Name: 'Test Company'
33-
Contacts (Complex: List<Contact>)
34-
Department (Complex: Department)
35-
Budget: 10000.00
36-
Name: 'Initial Department'
37-
Employees (Complex: List<Employee>)
38-
39-
""", context.ChangeTracker.DebugView.LongView);
29+
Assert.Contains("Contacts (Complex: List<Contact>)", context.ChangeTracker.DebugView.LongView);
30+
Assert.Contains("Department (Complex: Department)", context.ChangeTracker.DebugView.LongView);
31+
Assert.Contains("Name: 'Initial Department'", context.ChangeTracker.DebugView.LongView);
32+
Assert.Contains("Employees (Complex: List<Employee>)", context.ChangeTracker.DebugView.LongView);
4033

4134
ClearLog();
4235
await context.SaveChangesAsync();
@@ -126,7 +119,7 @@ public virtual Task Move_elements_in_complex_collection_mapped_to_json()
126119
}
127120
});
128121

129-
[ConditionalFact(Skip = "Issue #36433")]
122+
[ConditionalFact]
130123
public virtual Task Change_complex_collection_mapped_to_json_to_null_and_to_empty()
131124
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
132125
CreateContext,
@@ -354,7 +347,7 @@ public virtual Task Modify_nested_complex_property_in_complex_collection_mapped_
354347
}
355348
});
356349

357-
[ConditionalFact(Skip = "Issue #36433")]
350+
[ConditionalFact]
358351
public virtual Task Set_complex_collection_to_null_mapped_to_json()
359352
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
360353
CreateContext,
@@ -499,7 +492,7 @@ public virtual Task Complex_collection_with_empty_nested_collections_mapped_to_j
499492
}
500493
});
501494

502-
[ConditionalFact(Skip = "Issue #36433")]
495+
[ConditionalFact]
503496
public virtual Task Set_complex_property_mapped_to_json_to_null()
504497
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
505498
CreateContext,

0 commit comments

Comments
 (0)