Skip to content

Conversation

@JoasE
Copy link
Contributor

@JoasE JoasE commented Dec 18, 2025

Adds a CosmosStructuralTypeMaterializerSource and overrides AddStructuralTypeInitialization in CosmosShapedQueryCompilingExpressionVisitor to generate materialization expressions for complex properties
Leverages Nullable<>.HasValue for nullable value types in to support value types without equals operator
Part of: #31253

@JoasE

This comment was marked as resolved.

@JoasE JoasE marked this pull request as ready for review December 19, 2025 11:53
@JoasE JoasE requested a review from a team as a code owner December 19, 2025 11:53
Copilot AI review requested due to automatic review settings December 19, 2025 11:53
Copy link

Copilot AI left a 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 pull request implements binding logic for complex properties in the Cosmos DB provider, enabling proper materialization of complex types from JSON documents. The implementation introduces a custom CosmosStructuralTypeMaterializerSource that defers complex property handling to allow nested materialization expressions to be generated during query compilation.

Key changes:

  • Introduces CosmosStructuralTypeMaterializerSource to control when complex properties are materialized during shaping
  • Implements nested complex property and collection binding in CosmosShapedQueryCompilingExpressionVisitor
  • Enhances null-handling logic in CosmosProjectionBindingExpressionVisitor for nullable value types

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/EFCore.Cosmos/Query/Internal/CosmosStructuralTypeMaterializerSource.cs New class that overrides complex type materialization behavior to enable deferred binding
src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs Adds logic to generate materialization expressions for complex properties and collections from JObject/JArray
src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs Exposes methods and fields needed for complex property binding; adds handling for ComplexPropertyBindingExpression
src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs Improves null-safety checks for nullable value types and refactors member expression updates
src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs Registers the new CosmosStructuralTypeMaterializerSource service
test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs Uncomments assertions that now pass with complex property binding implemented
test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs New test class with Cosmos-specific overrides for complex property projection tests
test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs Test fixture configuration for complex properties tests in Cosmos

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

@JoasE JoasE marked this pull request as draft December 19, 2025 12:33
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@JoasE JoasE marked this pull request as ready for review December 19, 2025 16:48
Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @JoasE, thanks for submitting this and sorry I haven't gotten around to reviewing it earlier.

I admit that this part of the Cosmos query pipeline is generally unfamiliar to me, and also represents an older/legacy part of the EF codebase in general. I'll do my best to fully understand everything and provide meaningful feedback. As we add more test coverage I'm sure issues will be flushed out.

For now, here's a first round of review comments - most are nits but see some more important things as well.

{
Expression updatedMemberExpression = memberExpression.Update(
expression != null ? MatchTypes(expression, memberExpression.Expression!.Type) : expression);
if (expression is null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please rename expression to innerExpression here, to make it clear what it's referring to? Otherwise the code below is somewhat confusing (I initially thought it's referring to the member expression)

var expressionValue = Expression.Parameter(expression.Type);
var assignment = Expression.Assign(expressionValue, expression);

if (expression.Type.IsNullableType() == true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit (also below):

Suggested change
if (expression.Type.IsNullableType() == true
if (expression.Type.IsNullableType()

var expressionValue = Expression.Parameter(expression.Type);
var assignment = Expression.Assign(expressionValue, expression);

if (expression.Type.IsNullableType() == true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, when the inner expression is a Nullable<> and the outer isn't nullable, this adds "safe null" compensation logic, checking inner.HasValue() and then calling inner.Value.Foo only if true. Also, you're using a block with a variable and an assignment to make sure that inner only gets evaluated once (nice idea).

What I'm not clear on, is why exactly we need both this block and the one below (where we again check and compensate)...

Also, can you please add a short comment before the block explaining this (basically my explanation above, assuming it's correct), as it's really non-obvious?


AssertSql(
"""
SELECT VALUE c
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be projecting out only c["RequiredAssociate"] rather than all of c, right? I'm assuming this is because query support isn't yet fully done (i.e. full translation capabilities), but will be done later?

Note that the same seems true for various other tests on this class.

}
}

private int _currentComplexIndex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please move this to the top of the class, where we have fields.

Comment on lines +240 to +243
Constant(complexProperty.Name)
)
)
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we generally don't put closing parentheses on their own lines, but simply stack them on the last line of the expression. Can you please do a pass and ensure it's this way everywhere?

Suggested change
Constant(complexProperty.Name)
)
)
);
Constant(complexProperty.Name))));

Comment on lines +187 to +194
if (complexProperty.IsCollection)
{
expressions.Add(CreateComplexCollectionAssignmentBlock(member, complexProperty));
}
else
{
expressions.Add(CreateComplexPropertyAssignmentBlock(member, complexProperty));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (complexProperty.IsCollection)
{
expressions.Add(CreateComplexCollectionAssignmentBlock(member, complexProperty));
}
else
{
expressions.Add(CreateComplexPropertyAssignmentBlock(member, complexProperty));
}
expressions.Add(complexProperty.IsCollection
? CreateComplexCollectionAssignmentBlock(member, complexProperty)
: CreateComplexPropertyAssignmentBlock(member, complexProperty));


private sealed class ComplexPropertyBindingExpression : Expression
{
public ComplexPropertyBindingExpression(IComplexProperty complexProperty, ParameterExpression jObjectParameter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use primary constructor

private static readonly MethodInfo GetItemMethodInfo
= typeof(JObject).GetRuntimeProperties()
.Single(pi => pi.Name == "Item" && pi.GetIndexParameters()[0].ParameterType == typeof(string))
public static readonly MethodInfo GetItemMethodInfo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually used anywhere not on a JObject, passing a string property name as an argument?

Comment on lines +146 to +147
case MethodCallExpression jObjectMethodCallExpression
when jObjectMethodCallExpression.Method.IsGenericMethod && jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObjectWithSerializerMethodInfo:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case MethodCallExpression jObjectMethodCallExpression
when jObjectMethodCallExpression.Method.IsGenericMethod && jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObjectWithSerializerMethodInfo:
case MethodCallExpression { Method.IsGenericMethod: true } jObjectMethodCallExpression
when jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObjectWithSerializerMethodInfo:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants