-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Finish the implementation of the SaveChanges support for JSON-mapped complex collections #36432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR completes the implementation of SaveChanges support for JSON-mapped complex collections in Entity Framework Core. The changes remove previous restrictions that prevented complex collections from being nullable and fix various bugs related to change tracking and update operations for JSON complex collections.
Key changes include:
- Removed restrictions preventing complex collections from being marked as nullable/optional
- Fixed change tracking logic for complex collections to properly detect modifications
- Updated test infrastructure to use proper tracking patterns and baseline expectations
- Enhanced JSON update handling for complex properties in the relational update pipeline
Reviewed Changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/EFCore/Metadata/Internal/ComplexProperty.cs |
Removed restriction preventing complex collections from being nullable |
src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs |
Fixed IsModified logic for complex properties and improved change tracking loops |
src/EFCore/Relational/Update/ModificationCommand.cs |
Enhanced JSON update handling and refactored column modification generation |
test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs |
Removed obsolete validation test for optional complex collections |
| Multiple test files | Updated test implementations from abstract to concrete classes and fixed SQL baselines |
Files not reviewed (1)
- src/EFCore/Properties/CoreStrings.Designer.cs: Language not supported
src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
Show resolved
Hide resolved
src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
Show resolved
Hide resolved
3a09405 to
3c85a3a
Compare
| || _entries is [var singleEntry] | ||
| && (singleEntry.SharedIdentityEntry is not null | ||
| || singleEntry.EntityType.GetComplexProperties().Any() | ||
| || singleEntry.EntityType.GetNavigations().Any(e => e.IsCollection && e.TargetEntityType.IsMappedToJson()))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am not aware of the context enough, but so the code below used to run for all complex properties and all JSON-mapped collection navigations - and now it only runs for table-splitting complex properties only? Just want to make sure we don't need to do this for owned table splitting too.
In any case, maybe consider adding some documentation saying what this does and why it's relevant only to the complex table-splitting case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before JSON this block was used to detect table-splitting, with owned JSON support it was also used to detect JSON, but looking at the logic the two paths were disjointed, so I separated them for clarity. There shouldn't be any behavioral change from this.
| } | ||
|
|
||
| if (jsonEntry) | ||
| if (_entries.Any(e => e.EntityType is IEntityType entityType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a bit of further cleanup, instead of calling HandleJson() (which loops over all entries and deals with JSON ones) and then loop again just below over entries not mapped to JSON, we'd ideally just have one loop over all entries, that dispatches to different entry handlers (JSON, non-JSON, sproc...) as needed?
Also, in a similar vein, HandleJson() has a part where it goes over non-JSON-mapped entity entries to find the JSON mapped navigations and complex properties within, to add them to jsonColumnsUpdateMap. That also seems like it should just be part of the main loop, where we handle other aspects of non-JSON mapped entries.
(basically it seems like JSON handling was originally added here without much attempt to integrate it into the existing flow)
Obviously we don't have to continue cleanup/refactoring now, though, we can always improve later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be somewhat involved, I'll do it as part of #36429 (comment)
src/EFCore/ChangeTracking/Internal/InternalEntryBase.InternalComplexCollectionEntry.cs
Show resolved
Hide resolved
| _originalValues.AcceptChanges(this); | ||
|
|
||
| foreach (var complexCollection in _complexCollectionEntries) | ||
| for (var i = 0; i < _complexCollectionEntries.Length; i++) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this like an attempt to optimize? I think foreach over arrays is just as efficient as for, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this impacts behavior. These entries are mutable structs (I realize this is a smell), so when accessed with foreach the mutations only apply to the copy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see...! No, then it's perfectly understandable (and I also think it's not necessarily a smell to have an array of structs here, to save on the needless allocations/references).
|
|
||
| if (collection) | ||
| { | ||
| _isNullable = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that mean we allow nullable collections, e.g. to allow the null vs. empty distinction on JSON collections? We may need to check various other places (and add test coverage) to make sure this is actually fully supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added test coverage in this PR. The query bugs around this are currently hidden by #36433
| => (ComplexCollectionJsonContext)Fixture.CreateContext(); | ||
|
|
||
| [ConditionalFact] | ||
| [ConditionalFact(Skip = "Issue #36433")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you assigned it to me - is this a bug in query and change tracking? Let's chat so I understand it better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, it means that you need to make sure that all complex properties are populated before StartTrackingFromQuery is called on the containing entity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I thought that's exactly what #36430 did - is this based on top of it (recently merged to main)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this based on top of it (recently merged to main)?
Yes
3c85a3a to
4942acd
Compare
Add a way to configure a complex collection property as optional
Part of #31252
Fixes #36427
Fixes #36428