Add ValueComparer for JSON DOM types using DeepEquals#3887
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves EFCore.PG change tracking for JSON DOM-mapped properties (JsonDocument, JsonElement) by introducing provider-specific ValueComparers that use JsonElement.DeepEquals, reducing unnecessary UPDATEs when JSON content is unchanged but object instances differ.
Changes:
- Add
ValueComparerimplementations forJsonDocumentandJsonElementtoNpgsqlJsonTypeMapping, using deep JSON equality semantics. - Extend unit tests to validate deep equality behavior for the JSON DOM comparers.
- Add functional tests asserting that
SaveChangesemits UPDATEs only when JSON content changes (and does not throw for undefinedJsonElementcases).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/EFCore.PG/Storage/Internal/Mapping/NpgsqlJsonTypeMapping.cs |
Adds JSON DOM ValueComparers and wires them into the JSON type mapping so change tracking compares by JSON content. |
test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs |
Expands comparer unit tests to verify deep equality across new instances, ordering/number formatting cases, and null/undefined scenarios. |
test/EFCore.PG.FunctionalTests/Query/JsonDomChangeTrackingTest.cs |
New functional coverage validating UPDATE/no-UPDATE behavior for json/jsonb DOM properties and ensuring undefined JsonElement doesn’t throw during DetectChanges. |
| private sealed class JsonDocumentComparer() : ValueComparer<JsonDocument>( | ||
| (a, b) => a == null ? b == null : b != null && JsonElement.DeepEquals(a.RootElement, b.RootElement), | ||
| o => o.GetHashCode(), | ||
| // Disposing the original JsonDocument will throw when it's later compared against the snapshot | ||
| o => o); |
There was a problem hiding this comment.
This isn't actually an issue. ValueComparer has the following constructor:
public ValueComparer(
Expression<Func<T?, T?, bool>> equalsExpression,
Expression<Func<T, int>> hashCodeExpression,
Expression<Func<T, T>> snapshotExpression)
: base(equalsExpression, hashCodeExpression, snapshotExpression)
{
}
Note that unlike equalsExpression, hashCodeExpression is typed as Func<T, int> rather than Func<T?, int> — T isn't nullable here, so this is expected.
The change tracker currently uses shallow comparison when detecting changes to JSON DOM properties (JsonDocument and JsonElement). If the JSON content itself hasn't actually changed, but a new object instance was created, this results in unnecessary saves to the database.
This PR adds ValueComparers for JsonDocument and JsonElement based on DeepEquals.
Trade-offs:
As a very rough estimate of the potential performance gain, I benchmarked saving 1 million JsonDocuments to the database, recreating the objects each time without actually changing their content:
Relates to #3314