Skip to content
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

Add support for $count in navigation properties #2051

Merged
merged 9 commits into from
Apr 22, 2021

Conversation

KenitoInc
Copy link
Contributor

@KenitoInc KenitoInc commented Apr 13, 2021

Issues

This pull request fixes issue #xxx.

Description

Add support for $count in Navigation properties.

expand=$navProp/$count
expand=$navProp/$count($filter=prop gt abc)
expand=$navProp/$count($search=prop)

We are adding a new PathSelectItem named ExpandedCountSelectItem which inherits from ExpandedReferenceSelectItem.
The ExpandedCountSelectItem only supports 2 query options, $filter and $search. See spec http://docs.oasis-open.org/odata/odata/v4.01/cs01/part1-protocol/odata-v4.01-cs01-part1-protocol.html#sec_RequestingtheNumberofItemsinaCollect

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.

@KenitoInc KenitoInc changed the title $count improvements Add support for $count in navigation properties Apr 14, 2021
@KenitoInc KenitoInc marked this pull request as ready for review April 14, 2021 11:11
@KenitoInc KenitoInc added the Ready for review Use this label if a pull request is ready to be reviewed label Apr 14, 2021

if (this.lexer.CurrentToken.Text == UriQueryConstants.CountSegment && isSelect)
{
// $count in only allowed in $expand. e.g $expand=NavProperty/$count
Copy link
Member

Choose a reason for hiding this comment

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

is

/// <summary>
/// This represents one level of expansion for a particular expansion tree.
/// </summary>
public sealed class ExpandedCountSelectItem : ExpandedReferenceSelectItem
Copy link
Member

Choose a reason for hiding this comment

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

Do you think it's better to derive from ExpandedReferenceSelectItem?

Or shall we have a base NavigationSelectItem, and others derived from it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The semantics are the same though.
Also, ExpandedNavigationSelectItem inherits from the ExpandedReferenceSelectItem

/// <param name="filterOption">A filter clause for this expand (can be null)</param>
/// <param name="searchOption">A search clause for this expand (can be null)</param>
/// <exception cref="System.ArgumentNullException">Throws if input pathToNavigationProperty is null.</exception>
public ExpandedCountSelectItem(ODataExpandPath pathToNavigationProperty, IEdmNavigationSource navigationSource, FilterClause filterOption, SearchClause searchOption)
Copy link
Member

Choose a reason for hiding this comment

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

Only $filter and $search are allowed within nav/$count?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes: From the protocol http://docs.oasis-open.org/odata/odata/v4.01/cs01/part1-protocol/odata-v4.01-cs01-part1-protocol.html#sec_RequestingtheNumberofItemsinaCollect

On success, the response body MUST contain the exact count of items matching the request after applying any $filter or $search system query options, formatted as a simple primitive integer value with media type text/plain. Clients SHOULD NOT combine the system query options $top, $skip, $orderby, $expand, and $format with the path suffix /$count. The result of such a request is undefined.

@xuzhg
Copy link
Member

xuzhg commented Apr 14, 2021

Looks good to me.

@gathogojr
Copy link
Contributor

gathogojr commented Apr 19, 2021

@KenitoInc I cloned your branch and performed some tests based on the sample scenarios you provided in the PR description.
Consider this result of expanding Products a navigation property in Category

http://service/Categories?$expand=Products

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "Id": "P1",
                    "CategoryId": "PG1",
                    "Name": "Sugar",
                    "Color": "White",
                    "TaxRate": 0.06
                },
                {
                    "Id": "P2",
                    "CategoryId": "PG1",
                    "Name": "Coffee",
                    "Color": "Brown",
                    "TaxRate": 0.06
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "Id": "P3",
                    "CategoryId": "PG2",
                    "Name": "Paper",
                    "Color": "White",
                    "TaxRate": 0.14
                },
                {
                    "Id": "P4",
                    "CategoryId": "PG2",
                    "Name": "Pencil",
                    "Color": "Black",
                    "TaxRate": 0.14
                }
            ]
        }
    ]
}

Test 1:

http://service/Categories?$expand=Products/$count

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                },
                {
                    "@odata.id": "Products('P2')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                },
                {
                    "@odata.id": "Products('P4')"
                }
            ]
        }
    ]
}

Test 2:

http://service/Categories?$expand=Products/$count($filter=Color eq 'White')

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                }
            ]
        }
    ]
}

Test 3:

http://service/Categories?$expand=Products/$count($search=Black)

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                },
                {
                    "@odata.id": "Products('P2')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                },
                {
                    "@odata.id": "Products('P4')"
                }
            ]
        }
    ]
}

I don't know if I'm doing something wrong. Can you verify whether you get the expected results against a sample service?

@KenitoInc
Copy link
Contributor Author

@KenitoInc I cloned your branch and performed some tests based on the sample scenarios you provided in the PR description.
Consider this result of expanding Products a navigation property in Category

http://service/Categories?$expand=Products

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "Id": "P1",
                    "CategoryId": "PG1",
                    "Name": "Sugar",
                    "Color": "White",
                    "TaxRate": 0.06
                },
                {
                    "Id": "P2",
                    "CategoryId": "PG1",
                    "Name": "Coffee",
                    "Color": "Brown",
                    "TaxRate": 0.06
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "Id": "P3",
                    "CategoryId": "PG2",
                    "Name": "Paper",
                    "Color": "White",
                    "TaxRate": 0.14
                },
                {
                    "Id": "P4",
                    "CategoryId": "PG2",
                    "Name": "Pencil",
                    "Color": "Black",
                    "TaxRate": 0.14
                }
            ]
        }
    ]
}

Test 1:

http://service/Categories?$expand=Products/$count

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                },
                {
                    "@odata.id": "Products('P2')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                },
                {
                    "@odata.id": "Products('P4')"
                }
            ]
        }
    ]
}

Test 2:

http://service/Categories?$expand=Products/$count($filter=Color eq 'White')

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                }
            ]
        }
    ]
}

Test 3:

http://service/Categories?$expand=Products/$count($search=Black)

Result:

{
    "@odata.context": "http://service/$metadata#Categories(Products/$ref())",
    "value": [
        {
            "Id": "PG1",
            "Name": "Food",
            "Products": [
                {
                    "@odata.id": "Products('P1')"
                },
                {
                    "@odata.id": "Products('P2')"
                }
            ]
        },
        {
            "Id": "PG2",
            "Name": "Non-Food",
            "Products": [
                {
                    "@odata.id": "Products('P3')"
                },
                {
                    "@odata.id": "Products('P4')"
                }
            ]
        }
    ]
}

I don't know if I'm doing something wrong. Can you verify whether you get the expected results against a sample service?

@gathogojr I have added this feature to OData Core. We will support it in WebApi later.

@gathogojr
Copy link
Contributor

@gathogojr I have added this feature to OData Core. We will support it in WebApi later.

argh... I jumped the gun without even realizing it!

@pull-request-quantifier-deprecated

This PR has 75 quantified lines of changes. In general, a change size of upto 200 lines is ideal for the best PR experience!


Quantification details

Label      : Small
Size       : +73 -2
Percentile : 30%

Total files changed: 7

Change summary by file extension:
.cs : +71 -2
.txt : +2 -0

Change counts above are quantified counts, based on the PullRequestQuantifier customizations.

Why proper sizing of changes matters

Optimal pull request sizes drive a better predictable PR flow as they strike a
balance between between PR complexity and PR review overhead. PRs within the
optimal size (typical small, or medium sized PRs) mean:

  • Fast and predictable releases to production:
    • Optimal size changes are more likely to be reviewed faster with fewer
      iterations.
    • Similarity in low PR complexity drives similar review times.
  • Review quality is likely higher as complexity is lower:
    • Bugs are more likely to be detected.
    • Code inconsistencies are more likely to be detetcted.
  • Knowledge sharing is improved within the participants:
    • Small portions can be assimilated better.
  • Better engineering practices are exercised:
    • Solving big problems by dividing them in well contained, smaller problems.
    • Exercising separation of concerns within the code changes.

What can I do to optimize my changes

  • Use the PullRequestQuantifier to quantify your PR accurately
    • Create a context profile for your repo using the context generator
    • Exclude files that are not necessary to be reviewed or do not increase the review complexity. Example: Autogenerated code, docs, project IDE setting files, binaries, etc. Check out the Excluded section from your prquantifier.yaml context profile.
    • Understand your typical change complexity, drive towards the desired complexity by adjusting the label mapping in your prquantifier.yaml context profile.
    • Only use the labels that matter to you, see context specification to customize your prquantifier.yaml context profile.
  • Change your engineering behaviors
    • For PRs that fall outside of the desired spectrum, review the details and check if:
      • Your PR could be split in smaller, self-contained PRs instead
      • Your PR only solves one particular issue. (For example, don't refactor and code new features in the same PR).

How to interpret the change counts in git diff output

  • One line was added: +1 -0
  • One line was deleted: +0 -1
  • One line was modified: +1 -1 (git diff doesn't know about modified, it will
    interpret that line like one addition plus one deletion)
  • Change percentiles: Change characteristics (addition, deletion, modification)
    of this PR in relation to all other PRs within the repository.


Was this comment helpful? 👍  :ok_hand:  :thumbsdown: (Email)
Customize PullRequestQuantifier for this repository.

Comment on lines +112 to +114
if (this.lexer.CurrentToken.Text.StartsWith("$", StringComparison.Ordinal)
&& (!allowRef || this.lexer.CurrentToken.Text != UriQueryConstants.RefSegment)
&& this.lexer.CurrentToken.Text != UriQueryConstants.CountSegment)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain the logic here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If Path segment starts with $, it should only have $count or $ref when allowRef is true
e.g
$expand=NavProp/$ref
$expand=NavProp/$count

Comment on lines +17 to +25
/// <summary>
/// Create an Expand item using a nav prop, its entity set and a SelectExpandClause
/// </summary>
/// <param name="pathToNavigationProperty">the path to the navigation property for this expand item, including any type segments</param>
/// <param name="navigationSource">the navigation source for this ExpandItem</param>
/// <param name="filterOption">A filter clause for this expand (can be null)</param>
/// <param name="searchOption">A search clause for this expand (can be null)</param>
/// <exception cref="System.ArgumentNullException">Throws if input pathToNavigationProperty is null.</exception>
public ExpandedCountSelectItem(ODataExpandPath pathToNavigationProperty, IEdmNavigationSource navigationSource, FilterClause filterOption, SearchClause searchOption)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be helpful if you could provide an concrete example of a path that this class represents, and how the parts of the path that the different parameters represent.

Copy link
Contributor

@gathogojr gathogojr left a comment

Choose a reason for hiding this comment

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

:shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ready for review Use this label if a pull request is ready to be reviewed Small
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants