Skip to content
71 changes: 58 additions & 13 deletions src/Microsoft.OData.Core/UriParser/Binders/SelectExpandBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,27 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn)
PathSegmentToken currentToken = tokenIn.PathToNavigationProp;

IEdmStructuredType currentLevelEntityType = this.EdmType;

// If we have a previous level binding with the last segment as a Type segment,
// The currentLevelEntityType should be the Type of the TypeSegment
// E.g /Orders?$expand=Customer/NS.VipCustomer($expand=Trips)
// If we are Binding the expanded property Trips, currentLevelEntityType should be the type of the VipCustomer
if (this.parsedSegments.Count > 0 && this.parsedSegments.Last() is TypeSegment)
{
IEdmType typeSegmentType = this.parsedSegments.Last().EdmType;

IEdmCollectionType collection = typeSegmentType as IEdmCollectionType;

if (collection != null)
{
currentLevelEntityType = (typeSegmentType as IEdmCollectionType).ElementType.Definition as IEdmStructuredType;
}
else
{
currentLevelEntityType = typeSegmentType as IEdmStructuredType;
}
}

List<ODataPathSegment> pathSoFar = new List<ODataPathSegment>();
PathSegmentToken firstNonTypeToken = currentToken;

Expand Down Expand Up @@ -441,23 +462,25 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn)

ODataExpandPath pathToNavProp = new ODataExpandPath(pathSoFar);

IEdmTypeReference elementTypeReference = hasDerivedTypeSegment ? derivedType.ToTypeReference() : null;
Copy link
Contributor

@habbes habbes Jan 27, 2022

Choose a reason for hiding this comment

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

Out of curiosity, why does the elementTypeReference default to null when there's no derived type segment as opposed to defaulting to the type of the property being expanded? #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's null since we can get it from the expanded property in these lines

BindingState state = new BindingState(config)
{
ImplicitRangeVariable =
NodeFactory.CreateImplicitRangeVariable(elementType != null ? elementType :
targetNavigationSource.EntityType().ToTypeReference(), targetNavigationSource)
};

Copy link
Member

@mikepizzo mikepizzo Feb 4, 2022

Choose a reason for hiding this comment

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

I see that we are getting the type in SelectExpandBinder, but what is the contract with BindFilter, BindOrderby, etc? These private functions take an elementType which gets passed eventually to CreateBindingState where the logic is to fall back to the navigationSource, if it's not otherwise specified. But it's not clear in the various private methods that the elementType is nullable, and future code could make invalid assumptions about whether it may be null. If that private CreateBindingState method is only reached through these methods, would it be cleaner to compute the elementType from the targetNavigationSource here so it is never null in subsequent methods? Alternatively, maybe at least clarify in the description of the elementType parameter to the various methods (i.e., line 529) "The target element type if different from the type of the target navigation source." Logic is sound, I just want us to consider what is the cleaner/more intuitive contract for maintainability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mikepizzo I have updated the description.


// $apply
ApplyClause applyOption = BindApply(tokenIn.ApplyOptions, this.ResourcePathNavigationSource, targetNavigationSource);
ApplyClause applyOption = BindApply(tokenIn.ApplyOptions, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference);

// $compute
ComputeClause computeOption = BindCompute(tokenIn.ComputeOption, this.ResourcePathNavigationSource, targetNavigationSource);
ComputeClause computeOption = BindCompute(tokenIn.ComputeOption, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference);

var generatedProperties = GetGeneratedProperties(computeOption, applyOption);
bool collapsed = applyOption?.Transformations.Any(t => t.Kind == TransformationNodeKind.Aggregate || t.Kind == TransformationNodeKind.GroupBy) ?? false;

// $filter
FilterClause filterOption = BindFilter(tokenIn.FilterOption, this.ResourcePathNavigationSource, targetNavigationSource, null, generatedProperties, collapsed);
FilterClause filterOption = BindFilter(tokenIn.FilterOption, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference, generatedProperties, collapsed);

// $orderby
OrderByClause orderbyOption = BindOrderby(tokenIn.OrderByOptions, this.ResourcePathNavigationSource, targetNavigationSource, null, generatedProperties, collapsed);
OrderByClause orderbyOption = BindOrderby(tokenIn.OrderByOptions, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference, generatedProperties, collapsed);

// $search
SearchClause searchOption = BindSearch(tokenIn.SearchOption, this.ResourcePathNavigationSource, targetNavigationSource, null);
SearchClause searchOption = BindSearch(tokenIn.SearchOption, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference);

if (isRef)
{
Expand All @@ -470,7 +493,7 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn)
}

// $select & $expand
SelectExpandClause subSelectExpand = BindSelectExpand(tokenIn.ExpandOption, tokenIn.SelectOption, parsedPath, this.ResourcePathNavigationSource, targetNavigationSource, null, generatedProperties, collapsed);
SelectExpandClause subSelectExpand = BindSelectExpand(tokenIn.ExpandOption, tokenIn.SelectOption, parsedPath, this.ResourcePathNavigationSource, targetNavigationSource, elementTypeReference, generatedProperties, collapsed);

// $levels
LevelsClause levelsOption = ParseLevels(tokenIn.LevelsOption, currentLevelEntityType, currentNavProp);
Expand All @@ -485,12 +508,13 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn)
/// <param name="applyToken">The apply tokens to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <returns>The null or the built apply clause.</returns>
private ApplyClause BindApply(IEnumerable<QueryToken> applyToken, IEdmNavigationSource resourcePathNavigationSource, IEdmNavigationSource targetNavigationSource)
private ApplyClause BindApply(IEnumerable<QueryToken> applyToken, IEdmNavigationSource resourcePathNavigationSource, IEdmNavigationSource targetNavigationSource, IEdmTypeReference elementType = null)
{
if (applyToken != null && applyToken.Any())
{
MetadataBinder binder = BuildNewMetadataBinder(this.Configuration, resourcePathNavigationSource, targetNavigationSource, null);
MetadataBinder binder = BuildNewMetadataBinder(this.Configuration, resourcePathNavigationSource, targetNavigationSource, elementType);
ApplyBinder applyBinder = new ApplyBinder(binder.Bind, binder.BindingState);
return applyBinder.BindApply(applyToken);
}
Expand All @@ -504,7 +528,7 @@ private ApplyClause BindApply(IEnumerable<QueryToken> applyToken, IEdmNavigation
/// <param name="computeToken">The compute token to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The target element type.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <returns>The null or the built compute clause.</returns>
private ComputeClause BindCompute(ComputeToken computeToken, IEdmNavigationSource resourcePathNavigationSource, IEdmNavigationSource targetNavigationSource, IEdmTypeReference elementType = null)
{
Expand All @@ -524,7 +548,7 @@ private ComputeClause BindCompute(ComputeToken computeToken, IEdmNavigationSourc
/// <param name="filterToken">The filter token to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The Edm element type.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <param name="generatedProperties">The generated properties.</param>
/// <param name="collapsed">The collapsed boolean value.</param>
/// <returns>The null or the built filter clause.</returns>
Expand All @@ -547,7 +571,7 @@ private FilterClause BindFilter(QueryToken filterToken, IEdmNavigationSource res
/// <param name="orderByToken">The orderby token to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The Edm element type.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <param name="generatedProperties">The generated properties.</param>
/// <param name="collapsed">The collapsed boolean value.</param>
/// <returns>The null or the built filter clause.</returns>
Expand All @@ -571,7 +595,7 @@ private OrderByClause BindOrderby(IEnumerable<OrderByToken> orderByToken, IEdmNa
/// <param name="searchToken">The search token to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The Edm element type.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <returns>The null or the built search clause.</returns>
private SearchClause BindSearch(QueryToken searchToken, IEdmNavigationSource resourcePathNavigationSource, IEdmNavigationSource targetNavigationSource, IEdmTypeReference elementType)
{
Expand All @@ -593,7 +617,7 @@ private SearchClause BindSearch(QueryToken searchToken, IEdmNavigationSource res
/// <param name="segments">The parsed segments to visit.</param>
/// <param name="resourcePathNavigationSource">The navigation source at the resource path.</param>
/// <param name="targetNavigationSource">The target navigation source at the current level.</param>
/// <param name="elementType">The Edm element type.</param>
/// <param name="elementType">The target element type if different from the type of the target navigation source.</param>
/// <param name="generatedProperties">The generated properties.</param>
/// <param name="collapsed">The collapsed boolean value.</param>
/// <returns>The null or the built select and expand clause.</returns>
Expand Down Expand Up @@ -659,8 +683,29 @@ private List<ODataPathSegment> ProcessSelectTokenPath(PathSegmentToken tokenIn)
Debug.Assert(tokenIn != null, "tokenIn != null");

List<ODataPathSegment> pathSoFar = new List<ODataPathSegment>();

IEdmStructuredType currentLevelType = this.edmType;

// If we have a previous level binding with the last segment as a Type segment,
// The currentLevelEntityType should be the Type of the TypeSegment
// E.g /Orders?$expand=Customer/NS.VipCustomer($select=VipCustomerName)
// If we are Binding the selected property VipCustomerName, currentLevelEntityType should be the type of the VipCustomer
if (this.parsedSegments.Count > 0 && this.parsedSegments.Last() is TypeSegment)
{
IEdmType typeSegmentType = this.parsedSegments.Last().EdmType;

IEdmCollectionType collection = typeSegmentType as IEdmCollectionType;

if (collection != null)
{
currentLevelType = (typeSegmentType as IEdmCollectionType).ElementType.Definition as IEdmStructuredType;
}
else
{
currentLevelType = typeSegmentType as IEdmStructuredType;
}
}

// first, walk through all type segments in a row, converting them from tokens into segments.
if (tokenIn.IsNamespaceOrContainerQualified() && !UriParserHelper.IsAnnotation(tokenIn.Identifier))
{
Expand Down
Loading