Skip to content

Commit 4b9bc54

Browse files
committed
Inject JSON shaper code before the entity gets tracked
Closes #36433
1 parent cdd0ea8 commit 4b9bc54

File tree

7 files changed

+319
-301
lines changed

7 files changed

+319
-301
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 StructuralTypePostMaterializationHook(
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/ShapedQueryCompilingExpressionVisitor.cs

Lines changed: 15 additions & 0 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,
@@ -361,7 +362,19 @@ 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 StructuralTypePostMaterializationHook(
369+
StructuralTypeShaperExpression shaper,
370+
ParameterExpression instanceVariable,
371+
List<ParameterExpression> variables,
372+
List<Expression> expressions)
373+
{
374+
}
375+
364376
private sealed class StructuralTypeMaterializerInjector(
377+
ShapedQueryCompilingExpressionVisitor shapedQueryCompiler,
365378
IStructuralTypeMaterializerSource materializerSource,
366379
ILiftableConstantFactory liftableConstantFactory,
367380
QueryTrackingBehavior queryTrackingBehavior,
@@ -663,6 +676,8 @@ private Expression MaterializeEntity(
663676

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

679+
shapedQueryCompiler.StructuralTypePostMaterializationHook(shaper, instanceVariable, variables, expressions);
680+
666681
if (_queryStateManager && primaryKey is not null)
667682
{
668683
if (structuralType is IEntityType entityType2)

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,

test/EFCore.SqlServer.FunctionalTests/Update/ComplexCollectionJsonUpdateSqlServerTest.cs

Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,13 @@ public override async Task Remove_element_from_complex_collection_mapped_to_json
3333
AssertSql(
3434
"""
3535
@p0='[{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 66)
36-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
37-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
38-
@p3='1'
36+
@p1='1'
3937
4038
SET IMPLICIT_TRANSACTIONS OFF;
4139
SET NOCOUNT ON;
42-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
40+
UPDATE [Companies] SET [Contacts] = @p0
4341
OUTPUT 1
44-
WHERE [Id] = @p3;
42+
WHERE [Id] = @p1;
4543
""");
4644
}
4745

@@ -52,15 +50,13 @@ public override async Task Modify_element_in_complex_collection_mapped_to_json()
5250
AssertSql(
5351
"""
5452
@p0='[{"Name":"First Contact - Modified","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 141)
55-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
56-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
57-
@p3='1'
53+
@p1='1'
5854
5955
SET IMPLICIT_TRANSACTIONS OFF;
6056
SET NOCOUNT ON;
61-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
57+
UPDATE [Companies] SET [Contacts] = @p0
6258
OUTPUT 1
63-
WHERE [Id] = @p3;
59+
WHERE [Id] = @p1;
6460
""");
6561
}
6662

@@ -71,15 +67,13 @@ public override async Task Move_elements_in_complex_collection_mapped_to_json()
7167
AssertSql(
7268
"""
7369
@p0='[{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]},{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]}]' (Nullable = false) (Size = 130)
74-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
75-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
76-
@p3='1'
70+
@p1='1'
7771
7872
SET IMPLICIT_TRANSACTIONS OFF;
7973
SET NOCOUNT ON;
80-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
74+
UPDATE [Companies] SET [Contacts] = @p0
8175
OUTPUT 1
82-
WHERE [Id] = @p3;
76+
WHERE [Id] = @p1;
8377
""");
8478
}
8579

@@ -117,16 +111,14 @@ public override async Task Complex_collection_with_nested_complex_type_mapped_to
117111

118112
AssertSql(
119113
"""
120-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
121-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
122-
@p2='[{"Name":"John Doe","PhoneNumbers":["555-1234","555-5678"],"Address":{"City":"Seattle","Country":"USA","PostalCode":"98101","Street":"123 Main St"}},{"Name":"Jane Smith","PhoneNumbers":["555-9876"],"Address":{"City":"Portland","Country":"USA","PostalCode":"97201","Street":"456 Oak Ave"}}]' (Nullable = false) (Size = 289)
123-
@p3='1'
114+
@p0='[{"Name":"John Doe","PhoneNumbers":["555-1234","555-5678"],"Address":{"City":"Seattle","Country":"USA","PostalCode":"98101","Street":"123 Main St"}},{"Name":"Jane Smith","PhoneNumbers":["555-9876"],"Address":{"City":"Portland","Country":"USA","PostalCode":"97201","Street":"456 Oak Ave"}}]' (Nullable = false) (Size = 289)
115+
@p1='1'
124116
125117
SET IMPLICIT_TRANSACTIONS OFF;
126118
SET NOCOUNT ON;
127-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
119+
UPDATE [Companies] SET [Employees] = @p0
128120
OUTPUT 1
129-
WHERE [Id] = @p3;
121+
WHERE [Id] = @p1;
130122
""");
131123
}
132124

@@ -138,14 +130,13 @@ public override async Task Modify_multiple_complex_properties_mapped_to_json()
138130
"""
139131
@p0='[{"Name":"Contact 1","PhoneNumbers":["555-1111"]}]' (Nullable = false) (Size = 50)
140132
@p1='{"Budget":50000.00,"Name":"Department A"}' (Nullable = false) (Size = 41)
141-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
142-
@p3='1'
133+
@p2='1'
143134
144135
SET IMPLICIT_TRANSACTIONS OFF;
145136
SET NOCOUNT ON;
146-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
137+
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1
147138
OUTPUT 1
148-
WHERE [Id] = @p3;
139+
WHERE [Id] = @p2;
149140
""");
150141
}
151142

@@ -156,15 +147,13 @@ public override async Task Clear_complex_collection_mapped_to_json()
156147
AssertSql(
157148
"""
158149
@p0='[]' (Nullable = false) (Size = 2)
159-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
160-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
161-
@p3='1'
150+
@p1='1'
162151
163152
SET IMPLICIT_TRANSACTIONS OFF;
164153
SET NOCOUNT ON;
165-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
154+
UPDATE [Companies] SET [Contacts] = @p0
166155
OUTPUT 1
167-
WHERE [Id] = @p3;
156+
WHERE [Id] = @p1;
168157
""");
169158
}
170159

@@ -175,15 +164,13 @@ public override async Task Replace_entire_complex_collection_mapped_to_json()
175164
AssertSql(
176165
"""
177166
@p0='[{"Name":"Replacement Contact 1","PhoneNumbers":["999-1111"]},{"Name":"Replacement Contact 2","PhoneNumbers":["999-2222","999-3333"]}]' (Nullable = false) (Size = 134)
178-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
179-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
180-
@p3='1'
167+
@p1='1'
181168
182169
SET IMPLICIT_TRANSACTIONS OFF;
183170
SET NOCOUNT ON;
184-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
171+
UPDATE [Companies] SET [Contacts] = @p0
185172
OUTPUT 1
186-
WHERE [Id] = @p3;
173+
WHERE [Id] = @p1;
187174
""");
188175
}
189176

@@ -193,16 +180,14 @@ public override async Task Add_element_to_nested_complex_collection_mapped_to_js
193180

194181
AssertSql(
195182
"""
196-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
197-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
198-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001","555-9999"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 163)
199-
@p3='1'
183+
@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001","555-9999"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 163)
184+
@p1='1'
200185
201186
SET IMPLICIT_TRANSACTIONS OFF;
202187
SET NOCOUNT ON;
203-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
188+
UPDATE [Companies] SET [Employees] = @p0
204189
OUTPUT 1
205-
WHERE [Id] = @p3;
190+
WHERE [Id] = @p1;
206191
""");
207192
}
208193

@@ -212,16 +197,14 @@ public override async Task Modify_nested_complex_property_in_complex_collection_
212197

213198
AssertSql(
214199
"""
215-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
216-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
217-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Modified City","Country":"USA","PostalCode":"99999","Street":"100 First St"}}]' (Nullable = false) (Size = 153)
218-
@p3='1'
200+
@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Modified City","Country":"USA","PostalCode":"99999","Street":"100 First St"}}]' (Nullable = false) (Size = 153)
201+
@p1='1'
219202
220203
SET IMPLICIT_TRANSACTIONS OFF;
221204
SET NOCOUNT ON;
222-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
205+
UPDATE [Companies] SET [Employees] = @p0
223206
OUTPUT 1
224-
WHERE [Id] = @p3;
207+
WHERE [Id] = @p1;
225208
""");
226209
}
227210

@@ -265,16 +248,14 @@ public override async Task Replace_complex_collection_element_mapped_to_json()
265248

266249
AssertSql(
267250
"""
268-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
269-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
270-
@p2='[{"Name":"Replacement Employee","PhoneNumbers":["555-7777","555-8888"],"Address":{"City":"Replace City","Country":"Canada","PostalCode":"54321","Street":"789 Replace St"}}]' (Nullable = false) (Size = 172)
271-
@p3='1'
251+
@p0='[{"Name":"Replacement Employee","PhoneNumbers":["555-7777","555-8888"],"Address":{"City":"Replace City","Country":"Canada","PostalCode":"54321","Street":"789 Replace St"}}]' (Nullable = false) (Size = 172)
252+
@p1='1'
272253
273254
SET IMPLICIT_TRANSACTIONS OFF;
274255
SET NOCOUNT ON;
275-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
256+
UPDATE [Companies] SET [Employees] = @p0
276257
OUTPUT 1
277-
WHERE [Id] = @p3;
258+
WHERE [Id] = @p1;
278259
""");
279260
}
280261

@@ -284,16 +265,14 @@ public override async Task Complex_collection_with_empty_nested_collections_mapp
284265

285266
AssertSql(
286267
"""
287-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
288-
@p1='{"Budget":10000.00,"Name":"Initial Department"}' (Nullable = false) (Size = 47)
289-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}},{"Name":"Employee No Phone","PhoneNumbers":[],"Address":{"City":"Quiet City","Country":"USA","PostalCode":"00000","Street":"456 No Phone St"}}]' (Nullable = false) (Size = 295)
290-
@p3='1'
268+
@p0='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}},{"Name":"Employee No Phone","PhoneNumbers":[],"Address":{"City":"Quiet City","Country":"USA","PostalCode":"00000","Street":"456 No Phone St"}}]' (Nullable = false) (Size = 295)
269+
@p1='1'
291270
292271
SET IMPLICIT_TRANSACTIONS OFF;
293272
SET NOCOUNT ON;
294-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
273+
UPDATE [Companies] SET [Employees] = @p0
295274
OUTPUT 1
296-
WHERE [Id] = @p3;
275+
WHERE [Id] = @p1;
297276
""");
298277
}
299278

@@ -337,16 +316,14 @@ public override async Task Replace_complex_property_mapped_to_json()
337316

338317
AssertSql(
339318
"""
340-
@p0='[{"Name":"First Contact","PhoneNumbers":["555-1234","555-5678"]},{"Name":"Second Contact","PhoneNumbers":["555-9876","555-5432"]}]' (Nullable = false) (Size = 130)
341-
@p1='{"Budget":99999.99,"Name":"Replacement Department"}' (Nullable = false) (Size = 51)
342-
@p2='[{"Name":"Initial Employee","PhoneNumbers":["555-0001"],"Address":{"City":"Initial City","Country":"USA","PostalCode":"00001","Street":"100 First St"}}]' (Nullable = false) (Size = 152)
343-
@p3='1'
319+
@p0='{"Budget":99999.99,"Name":"Replacement Department"}' (Nullable = false) (Size = 51)
320+
@p1='1'
344321
345322
SET IMPLICIT_TRANSACTIONS OFF;
346323
SET NOCOUNT ON;
347-
UPDATE [Companies] SET [Contacts] = @p0, [Department] = @p1, [Employees] = @p2
324+
UPDATE [Companies] SET [Department] = @p0
348325
OUTPUT 1
349-
WHERE [Id] = @p3;
326+
WHERE [Id] = @p1;
350327
""");
351328
}
352329

0 commit comments

Comments
 (0)