-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Support value types in complex JSON shaper #36557
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
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. |
QueryContext queryContext, | ||
object[]? keyPropertyValues, | ||
JsonReaderData? jsonReaderData, | ||
bool nullable, | ||
Func<QueryContext, object[]?, JsonReaderData, TEntity> shaper) | ||
where TEntity : class | ||
Func<QueryContext, object[]?, JsonReaderData, TStructural> shaper) |
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.
Shouldn't this have where TStructural : class
?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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*/) |
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.
Remove commented out code
@@ -96,7 +95,7 @@ public virtual object Create() | |||
{ | |||
throw new InvalidOperationException( |
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.
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?) |
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.
Add Issue #
AssertValueNestedType(e.RequiredNested, a.RequiredNested); | ||
NullSafeAssert<ValueNestedType>(e.OptionalNested, a.OptionalNested, AssertValueNestedType); | ||
|
||
// TODO: Complete for collection, mind ordering (how is this done elsewhere?) |
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.
Add Issue #
ss => ss.Set<ValueRootEntity>(), | ||
queryTrackingBehavior: queryTrackingBehavior); | ||
|
||
#endregion Value types |
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.
Add a test for projecting out the outermost value type property
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
Copilot uses AI. Check for mistakes.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
Copilot uses AI. Check for mistakes.
73650a5
to
d1fd606
Compare
Part of dotnet#31376 and dotnet#36296 Continues dotnet#36557
Closes #36552