Support value types in complex JSON shaper #36557
Conversation
404f616 to
45cca57
Compare
|
@roji looks like you committed also your overwritten versions (I guess it's from that). Either way, the Versions.props, etc. should not be modified. |
45cca57 to
73650a5
Compare
|
@cincuranet thanks, yeah - I keep have to bring that commit in and remove it because of the unsigned SDK on mac issue. Removing. |
| bool nullable, | ||
| Func<QueryContext, object[]?, JsonReaderData, TEntity> shaper) | ||
| where TEntity : class | ||
| Func<QueryContext, object[]?, JsonReaderData, TStructural> shaper) |
There was a problem hiding this comment.
Shouldn't this have where TStructural : class?
There was a problem hiding this comment.
Not currently - this also gets used with non-nullable structs. The other method that this PR introduces is only for use with nullable value types: it accepts a parameter with shaper returning the non-nullable type, and wraps a null check around it to return null.
I do agree that we should probably review the behavior for non-nullable structs... For scalars, if a null is retrieved from the database and the CLR is non-nullable, we throw rather than return default, which seems to be the right thing (/cc @cincuranet for optional table splitting).
There was a problem hiding this comment.
There was a problem hiding this comment.
Right, I agree we need to consider all this holistically. But note that above I was specifically mentioning the case of a null value - not a missing value - in the JSON document (combined with a non-nullable CLR property). That case seems very similar to a traditional null value in a relational column, where the user modeled it with a non-nullable property on the .NET side - AFAIK we consider this a configuration error and throw (rather than return the default CLR type).
In other words, we have the following questions:
- Null value in JSON, non-nullable .NET value property (the above case; my vote: should throw just like non-JSON relational)
- Missing value in JSON (I think here we agreed to return the CLR default, to make schema evolution easier)
- Missing value in JSON, HasDefaultValue or similar mechanism (for returning some other non-default CLR value)
Does that make sense?
There was a problem hiding this comment.
Discussed offline, we agree that for 10 we should throw if null is present in the database and the property is a non-nullable value type (opened #36587 to track).
| // we unwrap the lambda and integrate its body directly. | ||
| // We should ideally do this for all cases (no need for the extra lambda Invoke), but there are some issues around us writing | ||
| // to readonly fields. | ||
| if (jsonStructuralTypeVariable.Type.IsValueType /*&& Nullable.GetUnderlyingType(jsonStructuralTypeVariable.Type) is null*/) |
| @@ -96,7 +95,7 @@ public virtual object Create() | |||
| { | |||
| throw new InvalidOperationException( | |||
There was a problem hiding this comment.
At least file an issue and add a TODO to fix all the exceptions here if this is going to be used for something other than navigations.
Though, I still don't think that makes practical sense.
| AssertValueRelatedType(e.RequiredRelated, a.RequiredRelated); | ||
| NullSafeAssert<ValueRelatedType>(e.OptionalRelated, a.OptionalRelated, AssertValueRelatedType); | ||
|
|
||
| // TODO: Complete for collection, mind ordering (how is this done elsewhere?) |
| AssertValueNestedType(e.RequiredNested, a.RequiredNested); | ||
| NullSafeAssert<ValueNestedType>(e.OptionalNested, a.OptionalNested, AssertValueNestedType); | ||
|
|
||
| // TODO: Complete for collection, mind ordering (how is this done elsewhere?) |
| ss => ss.Set<ValueRootEntity>(), | ||
| queryTrackingBehavior: queryTrackingBehavior); | ||
|
|
||
| #endregion Value types |
There was a problem hiding this comment.
Add a test for projecting out the outermost value type property
There was a problem hiding this comment.
Pull Request Overview
This PR adds support for value types (structs) in complex JSON shaper functionality, addressing issue #36552. The implementation enables Entity Framework Core to properly handle value type complex properties when working with JSON columns.
- Adds new test entities that mirror existing reference type complex entities but use value types (structs)
- Updates internal infrastructure to handle structural types (both reference and value types) instead of just entities
- Modifies JSON materialization logic to properly handle nullable value types and fixup operations
Reviewed Changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
RelationshipsModelValueTypes.cs |
Defines new value type entities (ValueRootEntity, ValueRelatedType, ValueNestedType) for testing |
RelationshipsData.cs |
Adds creation and querying support for value type test entities |
ComplexPropertiesProjectionTestBase.cs |
Adds test case for selecting root entities with value type complex properties |
ComplexJsonProjectionSqlServerTest.cs |
SQL Server-specific test implementation for value type complex properties |
ComplexPropertiesFixtureBase.cs |
Configures model mapping for value type entities |
ComplexJsonRelationalFixtureBase.cs |
Configures JSON column mapping for value type entities |
ComplexTableSplittingRelationalFixtureBase.cs |
Configures table splitting for value type entities |
ClrCollectionAccessorFactory.cs |
Generalizes from TEntity to TStructural to support both reference and value types |
ClrCollectionAccessor.cs |
Updates generic constraints to support structural types instead of just classes |
SelectExpression.cs |
Fixes type handling for complex properties by unwrapping nullable types |
RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs |
Major updates to handle value type fixups and nullable value type materialization |
RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ClientMethods.cs |
Adds new materialization methods for structural types and nullable value types |
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
| && Int == other.Int | ||
| && String == other.String | ||
| && RequiredNested.Equals(other.RequiredNested) | ||
| && (OptionalNested is null && other.OptionalNested is null || OptionalNested?.Equals(other.RequiredNested) == true) |
There was a problem hiding this comment.
The equality comparison is checking OptionalNested?.Equals(other.RequiredNested) but should be checking OptionalNested?.Equals(other.OptionalNested) to compare the same properties.
| && (OptionalNested is null && other.OptionalNested is null || OptionalNested?.Equals(other.RequiredNested) == true) | |
| && (OptionalNested is null && other.OptionalNested is null || OptionalNested?.Equals(other.OptionalNested) == true) |
There was a problem hiding this comment.
An actual bug caught by Copilot, amazing
| // we unwrap the lambda and integrate its body directly. | ||
| // We should ideally do this for all cases (no need for the extra lambda Invoke), but there are some issues around us writing | ||
| // to readonly fields. | ||
| if (jsonStructuralTypeVariable.Type.IsValueType /*&& Nullable.GetUnderlyingType(jsonStructuralTypeVariable.Type) is null*/) |
There was a problem hiding this comment.
The commented out condition should either be removed if not needed or uncommented with an explanation if it's intentionally disabled for debugging purposes.
| if (jsonStructuralTypeVariable.Type.IsValueType /*&& Nullable.GetUnderlyingType(jsonStructuralTypeVariable.Type) is null*/) | |
| // Only unwrap and integrate for non-nullable value types; for Nullable<T>, handle in the else branch. | |
| if (jsonStructuralTypeVariable.Type.IsValueType && Nullable.GetUnderlyingType(jsonStructuralTypeVariable.Type) is null) |
73650a5 to
d1fd606
Compare
Part of dotnet#31376 and dotnet#36296 Continues dotnet#36557
Closes #36552