No Discernable Public API (Object) for Applying Generic OData Conformed Strings to IQueryable
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 864
- Forks
- 467
- PR merge metrics
- No merged PRs in 30d
Description
I believe this is a feature request though I cannot say for sure; I have combed through the source as best I could for an answer but come up empty. Also, I believe this is the correct issues board but if it is not please point me to the appropriate repo.
Request
We would like some manner of materializing the underlying Expression<Func<TEntity, bool>> constructed from a FilterClause exposed. This logic currently lives in the internal static Expression<Func<TEntityType, bool>> Bind<TEntityType>(FilterClause filterClause, IEdmModel model, IAssembliesResolver assembliesResolver, ODataQuerySettings querySettings) method of FilterBinder
Background
First, let me say that we do not use OData in the expected manner. We have "bled all over our stack" as the saying goes and have sacrificed plug and play library usage for greater control over the dynamic SQL generated to obtain much more performant, consistent queries for our read-only, dynamic data access API. This ultimately requires us to devote a lot of our business logic to how queries are composed which meant breaking apart OData and EF so that we could manipulate the Expressions themselves. It sounds crazy but it works (ergo not crazy). All of this is my way of saying that we are already using OData as a string to filter expression translator. What I am then requesting is official support for this use case.
We hacked this functionality with the old v3 libraries (Microsoft.Data.OData) by cloning the entire code as a project in our solution and adding a public class in the assembly that would utilize the internal FilterBuilder class's Bind method. Though it requires some mocking of the objects in conjunction with conventional model binder it provides exactly what we need. This is the basic code:
public static Expression<Func<T, bool>> Build(string oDataFilter)
{
const string ModelEntitySetName = "item";
// needed so that special characters (particularly +) are correctly preserved by the uri util odata uses for parsing
oDataFilter = Uri.EscapeDataString(oDataFilter);
var builder = new ODataConventionModelBuilder();
builder.EntitySet<T>(ModelEntitySetName);
var entityEdmModel = builder.GetEdmModel();
var parser = new ODataUriParser(entityEdmModel, new Uri(string.Format("/{0}?$filter={1}", ModelEntitySetName, oDataFilter), UriKind.Relative));
var querySettings = new ODataQuerySettings { HandleNullPropagation = HandleNullPropagationOption.False, EnableConstantParameterization = true };
return FilterBinder.Bind<T>(parser.ParseFilter(), entityEdmModel, new DefaultAssembliesResolver(), querySettings);
}
However, this leaves us with a painful upgrade path (merging the entire source down) and clearly isn't ideal. Instead, we would really like to reference the WebApi OData library and take advantage of public objects to accomplish the same task. While it would require some alteration to our fundamental design, we are able to accommodate a solution that produces a filtered IQueryable. To that end I wrote this proof of concept:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.OData;
using System.Web.OData.Builder;
using System.Web.OData.Query;
using Microsoft.OData.UriParser;
public class TestEntity
{
[Key]
public int Key { get; set; }
public string Name { get; set; }
}
static void Main()
{
// arrange
var list = new List<TestEntity>
{
new TestEntity { Name = "expected", Key = 1 },
new TestEntity { Name = "invalid", Key = 2 }
}.AsQueryable();
var filterString = "Name eq 'expected'";
// act
var builder = new ODataConventionModelBuilder();
builder.EntitySet<TestEntity>("TestEntity");
var model = builder.GetEdmModel();
var context = new ODataQueryContext(model, typeof(TestEntity), null);
var options = new ODataQueryOptionParser(
context.Model,
context.ElementType,
context.NavigationSource,
new Dictionary<string, string> { { "$filter", filterString } });
var filterQueryOption = new FilterQueryOption(filterString, context, options);
/* PROBLEM:
* Below throws an exception because no DI provider exists and thus the
* call to ODataQueryContextExtensions.UpdateQuerySettings fails,
* making this essentially useless as public "string to filter expression"
* translator.
*/
var newList = filterQueryOption.ApplyTo(
list,
new ODataQuerySettings
{
HandleNullPropagation = HandleNullPropagationOption.False,
EnableConstantParameterization = true
}) as IQueryable<TestEntity>;
if (newList == null)
{
throw new Exception("Queryable not convertable");
}
foreach (var item in newList)
{
Console.WriteLine($"TestEntity - Name:{item.Name} Value:{item.Key}");
}
Console.ReadKey();
}
As noted in the code comment, the above doesn't work because FilterQueryOptions.ApplyTo() does not use the provided ODataQuerySettings but calls into the context to update the settings using the DI framework. This is perplexing since I am passing the object directly to the call. I don't understand the full scope of the project but it seems strange to call the DI framework to update the settings when the settings object itself is already present. I understand that, by and large, the pathway I am taking advantage of is designed for unit testing. However, this seems equally problematic for that situation as you should be required to mock the context's dependency resolver to validate the entirely separate ApplyTo() functionality. Especially because what I'm doing is equivalent to an internal constructor used by unit tests.
I am not asking for the above method to necessarily be changed to handle the case in which the dependency injection provider is not present (though that would certainly be an method to promote this usage from a mere attempted hack to workable code). Instead I want to raise the more general issue that I think that this library could easily be more broadly useful if its string-to-expression translation features were publicly available through some modular API. Normally, I wouldn't even bring up such an issue on a massive project like this but it seems a small amount of work to write a shim class or two that makes use of internal classes and methods to take in a single Type, build an EdmModel by convention, and produce expressions from provided strings. This would then be clearly extensible by adding a "configurable" method where the builder is not necessarily the conventional builder or where more than one type (multiple entity sets) could be supported by a single "expression engine". Done right I think it is incredibly worthwile because it makes the underlying implementation applicable to unquantifiably more read-only use cases outside of simply slapping OData querys on top of a REST API. I can't say for certain but I think allowing the expression generation engine to exist outside of the rest of the OData REST API integration could drive an entirely new set of adoptors/contributors. Or maybe I'm totally wrong and we're the only people who would use this. But the idea passes the smell test for me of "simple, useful, and non-breaking."
Assemblies affected
Microsoft.OData.Core v7.0
System.Web.OData v6.0 (including latest nightly)
Expected result
Public object(s) is/are available via the library for retrieving expressions translated from an standard OData string for a generic type.
Actual result
No clear public API for the above exists. The ones that might offer this functionality are marked internal; others seem to (needlessly?) require dependency injection service.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the internal FilterBinder.Bind method and the public FilterQueryOption.ApplyTo path described in the issue, including ODataQueryContextExtensions.UpdateQuerySettings. Review the affected Microsoft.OData.Core v7.0 and System.Web.OData v6.0 APIs. Done means a documented public API can translate a standard OData filter string for a generic type into an expression without requiring the current internal classes or unavailable dependency-injection services.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100