OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
687 stars 349 forks source link

Allow using unmapped properties getters in queries #2886

Open joecarl opened 8 months ago

joecarl commented 8 months ago

Imagine the following scenario:

[EntitySet("TestEntities")]
public class TestModel
{
    public decimal PlasticKg { get; set; }
    public decimal OthersKg { get; set; }

    [NotMapped] // This attribute doesn't exist in Microsoft.OData.Client but I think it would be necessary for this case and useful for other cases
    public decimal TotalKg { get => PlasticKg + OthersKg; } 
}

public class MyContext : DataServiceContext
{
    public DataServiceQuery<TestModel> TestEntities;

    public MyContext(Uri serviceRoot) : base(serviceRoot)
    {      
        TestEntities = CreateQuery<TestModel>("TestEntities");
    }
}

// Main program:

var ctx = new MyContext(uri);

// The following lines should behave the same (but they don't):
ctx.TestEntities.Where(c => c.PlasticKg + c.OthersKg > 10).ToList(); // Works fine
ctx.TestEntities.Where(c => c.TotalKg > 10).ToList(); // Throws because the server responds with "Bad Request: Could not find a property named 'TotalKg' in model ..."  

So my proposal is:

I could implement this myself and submit a pull request, but I would like to receive some feedback before proceding. Thanks

xuzhg commented 8 months ago

@joecarl Thanks for sharing your thoughts. Any contributions are welcome. Feel free to share the PR.

By the way, it looks like to design/implement the $compute feature. For example:

[Computed] 
public decimal TotalKg { get => PlasticKg + OthersKg; } 

Here's our proposal: If an unmapped property (Not Found from Edm Model at the client side) is defined and used in the LINQ Expression, we can do:

1) If this property has '[Computed]' decorated, and it only contains the getter, we can generate this property into $compute clause. So

ctx.TestEntities.Where(c => c.TotalKg > 10).ToList(); can be translated to:

~/testEntities?$filter=TotalKey gt 10&$compute=PlasticKg add OthersKg as TotalKey

2) If this property has nothing decorated, we can translate this property as nested expression as:

~/testEntities?$filter=(PlasticKg add OthersKg) gt 10

Any thoughts?

joecarl commented 8 months ago

@xuzhg Thankyou very much for your answer. Your proposal seems good to me, I will think about it and will likely submit a pull request. I have a question though: does the $compute feature bring any advantage over the non computed query?

Also, about the NotMapped attribute. It might not be necessary for this case. But imagine the scenario where my model has a property public decimal Cost { get; set; } and the OData endpoint actually has a field named Cost but for whatever reason I don't want to use it (e.g.: the api has obsolete prices and i want to calculate it myself later or maybe the matching name are just an unfortunate coincidence). How could I prevent the materializer from setting that prop? Also how could I make sure that an exception will be thrown if I use that prop in a query? (An exception should be thrown since otherwise I would be doing an unintended filtering and the api would silently return wrong results)

joecarl commented 8 months ago

I've been attempting to implement this, but I've encountered a significant obstacle. The getter method's body cannot be translated at runtime because it is a compiled method, unlike Expression objects whose tree is stored at compile time, allowing access at runtime.

Does anybody know how to face this problem? Any help is appreciated