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

Support type cast in group by #1182

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

clemvnt
Copy link
Contributor

@clemvnt clemvnt commented Mar 2, 2024

Hello,

Cast type support in group by.

This PR needs OData/odata.net#2879.

Issues

#1180

Description

For the following group by : groupby((MyAddress/Fully.Qualified.Namespace.HomeAddress/HomeNO))

The response was an error :

System.NotSupportedException: The query specified in the URI is not valid. Binding OData QueryNode of kind 'SingleResourceCast' is not supported by 'AggregationBinder'.

Now, the response is :

[
  {
    "myAddress": {
        "homeNO": "Value1"
    }
  },
  {
    "myAddress": {
        "homeNO": "Value2"
    }
  }
]

clemvnt and others added 2 commits March 1, 2024 00:13
@clemvnt clemvnt marked this pull request as draft March 2, 2024 00:46
@clemvnt
Copy link
Contributor Author

clemvnt commented Mar 2, 2024

@microsoft-github-policy-service agree

IEdmStructuredTypeReference structured = node.StructuredTypeReference;
Contract.Assert(structured != null, "NS casts can contain only structured types");

Type clrType = Model.GetClrType(structured);

Choose a reason for hiding this comment

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

Could we stick with using var for consistency ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method is based on the following method :

public virtual Expression BindSingleResourceCastNode(SingleResourceCastNode node, QueryBinderContext context)
{
CheckArgumentNull(node, context);
IEdmStructuredTypeReference structured = node.StructuredTypeReference;
Contract.Assert(structured != null, "NS casts can contain only structured types");
Type clrType = context.Model.GetClrType(structured);
Expression source = BindCastSourceNode(node.Source, context);
return Expression.TypeAs(source, clrType);
}

There doesn't seem to be any consistency between explicit types and var in the project.

But if someone of the organization feels the same way, I'll make the changes.


Type clrType = Model.GetClrType(structured);

Expression source = Bind(node.Source);

Choose a reason for hiding this comment

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

Could we stick with using var for consistency ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See previous comment.

{
List<Product> productList = new List<Product>();

Product p1 = new DerivedProduct

Choose a reason for hiding this comment

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

Could we stick with using var for consistency ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See previous comment.

@@ -1584,6 +1720,22 @@ public IQueryable<Customer> Get()
return _customers.AsQueryable();
}
}

public class ProductsForTypeCastController : ODataController

Choose a reason for hiding this comment

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

Could we document these public classes and methods ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here, we're in a test file, so the other classes in this file have no comments.

But if someone of the organization feels the same way, I'll make the changes.

@ElizabethOkerio
Copy link
Contributor

@clemvnt Will you continue working on this PR?

@clemvnt
Copy link
Contributor Author

clemvnt commented Jun 25, 2024

Yes. I'm waiting for a new version of Microsoft.OData.Core with the OData/odata.net#2879 fix before publishing the PR.

@clemvnt clemvnt changed the base branch from release-8.x to main September 14, 2024 20:53
@clemvnt
Copy link
Contributor Author

clemvnt commented Sep 14, 2024

I've changed the base branch to main. This PR requires the next version of Microsoft.OData.Core (8.0.2). Other than that, it's ready.

@clemvnt clemvnt marked this pull request as ready for review October 8, 2024 20:28
@clemvnt
Copy link
Contributor Author

clemvnt commented Oct 8, 2024

This PR is ready.

@WanjohiSammy
Copy link
Contributor

@clemvnt Are you still working on this PR?

@clemvnt
Copy link
Contributor Author

clemvnt commented Jan 11, 2025

I fixed the merge conflict. The PR is ready.

cc. @WanjohiSammy

@gathogojr
Copy link
Contributor

@clemvnt Please check out the failing tests

Also test for derived complex type (in particular) as per scenario:

class A
{
    public string Prop { get; set; }
}

class DerivedFromA : A
{
    public string PropInDerived { get; set; }
}

class Product
{
    // ...
    public A A { get; set; }
    // ...
}

// ...
modelBuilder.ComplexType<A>();
modelBuilder.ComplexType<DerivedFromA>();

Request URL

Products?groupby((A/Fully.Qualified.Namespace.DerivedFromA/PropInDerived))

@clemvnt
Copy link
Contributor Author

clemvnt commented Jan 20, 2025

  • Merged with main.
  • Fixed tests.
  • Added a new test case :
Products?groupby((Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))`)

@gathogojr
Copy link
Contributor

gathogojr commented Jan 20, 2025

  • Merged with main.
  • Fixed tests.
  • Added a new test case :
Products?groupby((Category/Microsoft.AspNetCore.OData.Tests.Models.DerivedCategory/DerivedCategoryName))`)

@clemvnt The specific scenario I asked you to verify is one where the derived type is a complex type, not entity type. Here's an illustration of a scenario that seem to proceed unexpected results (sharing only the bits you'd need to repro):

// Data models
namespace Microsoft.AspNetCore.OData.E2E.Tests.DollarApply
{   
    public class MyCustomer
    {
        public int Id { get; set; }
        public MyAddress Address { get; set; }
    }

    public class MyAddress
    {
        public string Street { get; set; }
    }

    public class MyHomeAddress : MyAddress
    {
        public string HomeNo { get; set; }
        public MyCity City { get; set; }
    }

    public class MyCity
    {
        public string Name { get; set; }
    }
}


// Edm model
var builder = new ODataConventionModelBuilder();
            
builder.EntitySet<MyCustomer>("MyCustomers");
builder.ComplexType<MyAddress>();
builder.ComplexType<MyHomeAddress>();
builder.ComplexType<MyCity>();

var model = builder.GetEdmModel();


// Controller
public class MyCustomersController : ODataController
{
    private static readonly List<MyCustomer> myCustomers = new List<MyCustomer>
    {
        new MyCustomer
        {
            Id = 1,
            Address = new MyHomeAddress { Street = "High Street", HomeNo = "#1", City = new MyCity { Name = "Belfast" } }
        },
        new MyCustomer
        {
            Id = 2,
            Address = new MyHomeAddress { Street = "Moore Street", HomeNo = "#2", City = new MyCity { Name = "Dublin" } }
        }
    };

    [EnableQuery]
    public ActionResult<IEnumerable<MyCustomer>> Get()
    {
        return myCustomers;
    }
}


// Tests
[Fact]
public async Task TestPrimitivePropertyInDerivedTypeAsync()
{
    var queryUrl = "default/MyCustomers?$apply=groupby((Address/Microsoft.AspNetCore.OData.E2E.Tests.DollarApply.MyHomeAddress/HomeNo))";
    var request = new HttpRequestMessage(HttpMethod.Get, queryUrl);
    request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none"));
    var client = CreateClient(); // Initialize client

    var response = await client.SendAsync(request);

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
            
    var content = await response.Content.ReadAsStringAsync();
    // Response as expected: {"value":[{"Address":{"HomeNo":"#1"}},{"Address":{"HomeNo":"#2"}}]}
}

[Fact]
public async Task TestComplexPropertyInDerivedTypeAsync()
{
    var queryUrl = "default/MyCustomers?$apply=groupby((Address/Microsoft.AspNetCore.OData.E2E.Tests.DollarApply.MyHomeAddress/City/Name))";
    var request = new HttpRequestMessage(HttpMethod.Get, queryUrl);
    request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=none"));
    var client = CreateClient(); // Initialize client

    var response = await client.SendAsync(request);

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    var content = await response.Content.ReadAsStringAsync();
    // Response NOT as expected: {"value":[{"Address":{}},{"Address":{}}]}
}

@clemvnt
Copy link
Contributor Author

clemvnt commented Jan 21, 2025

Thank you for the clarification.

It seems the issue arises from the ODataResourceSerializer, which is unable to locate the City property on the structured type Microsoft.AspNetCore.OData.E2E.Tests.DollarApply.MyAddress :

IEdmProperty edmProperty = structuredType?.Properties()
.FirstOrDefault(p => p.Name.Equals(prop.Key, StringComparison.Ordinal));

Looking at the GroupByWrapper instance, there doesn't appear to be any information indicating the EDM type of the value. I’m unsure how to infer that the type should be MyHomeAddress instead of MyAddress within the ODataResourceSerializer logic. Do you have any insights on how to address this?

For context, in my specific use case, I don’t encounter this issue because I bypass the OData formatter. Instead, I manually apply OData clauses to IQueryable objects.

@gathogojr
Copy link
Contributor

Thank you for the clarification.

It seems the issue arises from the ODataResourceSerializer, which is unable to locate the City property on the structured type Microsoft.AspNetCore.OData.E2E.Tests.DollarApply.MyAddress :

IEdmProperty edmProperty = structuredType?.Properties()
.FirstOrDefault(p => p.Name.Equals(prop.Key, StringComparison.Ordinal));

Looking at the GroupByWrapper instance, there doesn't appear to be any information indicating the EDM type of the value. I’m unsure how to infer that the type should be MyHomeAddress instead of MyAddress within the ODataResourceSerializer logic. Do you have any insights on how to address this?

For context, in my specific use case, I don’t encounter this issue because I bypass the OData formatter. Instead, I manually apply OData clauses to IQueryable objects.

@clemvnt I'll need to do some digging to figure out how we can remedy it. At least we now know about it. If it's okay with you, we could merge your fix since it addresses a gap, then open a new ticket to track this scenario.

@gathogojr
Copy link
Contributor

Thinking about it some more...

It seems the issue arises from the ODataResourceSerializer, which is unable to locate the City property on the structured type Microsoft.AspNetCore.OData.E2E.Tests.DollarApply.MyAddress
I’m unsure how to infer that the type should be MyHomeAddress instead of MyAddress within the ODataResourceSerializer logic

Somehow it works for the primitive HomeNo property declared on MyHomeAddress... We'll need to figure out why that works and why it doesn't work if the property is complex.

@clemvnt
Copy link
Contributor Author

clemvnt commented Jan 22, 2025

If it's okay with you, we could merge your fix since it addresses a gap, then open a new ticket to track this scenario.

Sure, I’d be happy to create a new issue for this case and include all the information I have gathered about it.

Somehow it works for the primitive HomeNo property declared on MyHomeAddress... We'll need to figure out why that works and why it doesn't work if the property is complex.

The EDM property isn’t found even for the primitive HomeNo property. However, it works because the following condition is satisfied:

property = new ODataProperty
{
Name = prop.Key,
Value = prop.Value
};

For the City property, we don’t meet the same condition since it’s a navigation property.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants