Fix run value converters that convert nulls for JSON columns#37984
Fix run value converters that convert nulls for JSON columns#37984JoasE wants to merge 11 commits intodotnet:mainfrom
Conversation
During (de)serialization, check if converter converts nulls and run for null values if so Closes: dotnet#37983
| resultExpression = Convert(resultExpression, property.ClrType); | ||
| } | ||
|
|
||
| var converter = property.GetTypeMapping().Converter; |
There was a problem hiding this comment.
This is copied over from above for CreateGetValueExpression
This comment was marked as resolved.
This comment was marked as resolved.
|
@AndriySvyryd assigning to you for reviewing but let me know if you want me to do it. |
There was a problem hiding this comment.
Pull request overview
Fixes JSON column (de)serialization so value converters that opt into null conversion (ConvertsNulls == true) are applied even when the JSON token/value is null, addressing incorrect null persistence/materialization behavior.
Changes:
- Apply JSON value converter logic for
nullvalues during JSON update serialization (ModificationCommand,RelationalJsonUtilities). - Apply value converter logic for JSON
nulltokens during query materialization (RelationalShapedQueryCompilingExpressionVisitor). - Add/adjust relational and provider-specific tests to validate null-converting converters in JSON scenarios.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore.Relational/Update/ModificationCommand.cs | Serialize JSON properties via JsonValueReaderWriter even when the value is null, if the converter converts nulls. |
| src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs | Same as above for JSON parameter/constant serialization utilities. |
| src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs | When JSON token is null, invoke converter (when ConvertsNulls) instead of returning default CLR value. |
| test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs | Adds a regression test exercising JSON equality with a converter that converts nulls. |
| test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs | Adds SQL assertion for the new regression scenario. |
| test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs | Refactors base test to allow provider-specific JSON embedding (new overridable hooks). |
| test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerJsonTest.cs | New SQL Server functional test running the end-to-end converter suite with the entity stored as JSON. |
| test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs | Updates expected JSON payload/parameter sizing for null-converting converter output. |
| test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs | Same as above for json column type. |
| test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs | Updates expected JSON payload/parameter sizing for null-converting converter output. |
test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs
Outdated
Show resolved
Hide resolved
test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs
Show resolved
Hide resolved
| var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter; | ||
| Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property"); | ||
| jsonValueReaderWriter.ToJson(writer, propertyValue); | ||
| jsonValueReaderWriter.ToJson(writer, propertyValue!); |
There was a problem hiding this comment.
propertyValue can be null here
There was a problem hiding this comment.
That’s actually intentional here, since this change allows propertyValue to be null if the converter has indicated it can handle null values.
There was a problem hiding this comment.
Then you should change the signature of ToJson to allow nulls
There was a problem hiding this comment.
@AndriySvyryd That's a good point! I had some reservations about that but didn't really give it much thought. I've looked at it more now and FWIG JsonValueReaderWriter currently doesn't allow null input by design, and I'm not sure changing that would make sense and/or would require a different setup all together.
If I change JsonValueReaderWriter to allow null argument for object value, I can't just pass it to ToJsonTyped. I would have to make ToJsonTyped value argument nullable aswell, but I don't think that would be desired. AFAIK The typed JsonValueReaderWriter<T> should generally not have to worry about null values. I've considered having JsonConvertedValueReaderWriter inherit from non-typed JsonValueReaderWriter to solve this. Then I could add a null check and write in the JsonValueReaderWriter<T>.ToJson implementation and also the JsonConvertedValueReaderWriter.ToJson implementation. However, materialization works with the typed JsonValueReaderWriter<T>.FromJsonTyped, so the JsonConvertedValueReaderWriter wouldn't be usable in materialization. I think that if we want to make ToJson accept null and do the null write for the value, the only option that makes sense would be unsealing JsonValueReaderWriter<T>.ToJson so that I can override it in JsonConvertedValueReaderWriter. However, I'm not sure that is desirable either.
Some workarounds could be:
- Adding a method to
IJsonConvertedValueReaderWriterthat allows null input, then doing the ConvertsNulls check and null write there - Leveraging
IJsonConvertedValueReaderWriter.ValueConverterandICompositeJsonValueReaderWriter.InnerReaderWriterto circumventJsonConvertedValueReaderWriter.ToJson. Invoking theValueConvertermanually and checking if it converted to null (seems cumbersome) - Accepting the null suppression for this specific instance
There was a problem hiding this comment.
Side note that I ran into this when working on:
https://github.com/dotnet/efcore/pull/38024/changes#diff-62e5c8b19c66714cf0310ceff8fb90312c039fb64dc9e7af31241e8956bb412dR103-R116
There was a problem hiding this comment.
I think this exposed the reason I find this suppression smelly: it's relying on implementation details. You could add JsonValueReaderWriter.HandlesNulls, make the value nullable and throw if it's null and HandlesNulls is false
There was a problem hiding this comment.
@AndriySvyryd That makes a lot of sense! I've added JsonValueReaderWriter.HandlesNulls
There was a problem hiding this comment.
I'm wondering now how this should (not) affect FromJson
There was a problem hiding this comment.
I think we can have FromJson stay unaffected, but would recommend renaming to HandlesNullWrites
Otherwhise a similar signature change for FromJson would be needed, but materialization uses FromJsonTyped
During (de)serialization, check if converter converts nulls and run for null values if so
Closes: #37983