simple-odata-client / Simple.OData.Client

MIT License
329 stars 196 forks source link

Filter column against list of values #576

Open phatcher opened 5 years ago

phatcher commented 5 years ago

Say you are trying to restrict a query against a list of values that is supplied at runtime e.g.

var publishers = new List<int> { 1, 3, 5 };

var books = client.For<Book>
                  .Filter(x => publishers.Contains(x.PublisherId))
                  .FindEntriesAsync()

You will get a syntax error saying that it can't turn System.Collection into a URI

It is possible to put this together like this..

var filterParams = publishers.Select(id => $"PublisherId eq {id}");
var filter = string.Join(" or ", filterParams);
var pubFilter = Uri.EscapeDataString($"({filter})");

var books = client.For<Book>
                  .Filter(pubFilter)
                  .FindEntriesAsync()

But you have to take care with the type that's being encoded and its fairly hacky overall.

Suggest we look at a way of providing a helper that would do this sort of encoding, may be an overload of filter that takes a delegate and a collection?

sixlettervariables commented 5 years ago

I use the following extension method:

public static IBoundClient<TOwner> WhereInFilter<TOwner, TProperty>(
    this IBoundClient<TOwner> client,
    Expression<Func<TOwner, TProperty>> getter,
    params TProperty[] propertyValues)
    where TOwner : class
{
    return client.Filter(CreateWhereInFilter(getter, propertyValues));
}

public static IBoundClient<TOwner> WhereInFilter<TOwner, TProperty>(
    this IBoundClient<TOwner> client,
    Expression<Func<TOwner, TProperty>> getter,
    IEnumerable<TProperty> propertyValues)
    where TOwner : class
{
    return client.Filter(CreateWhereInFilter(getter, propertyValues));
}

private static Expression<Func<TOwner, bool>> CreateWhereInFilter<TOwner, TProperty>(Expression<Func<TOwner, TProperty>> getter, IEnumerable<TProperty> propertyValues)
{
    // 1. Find the property accessor
    var arg = getter.Parameters.First();
    var property = (MemberExpression)getter.Body;

    // 2. Create comparison chain
    Expression comparisons = null;
    foreach (var value in propertyValues)
    {
        var comparison = Expression.Equal(property, Expression.Constant(value));
        if (comparisons == null)
        {
            comparisons = comparison;
        }
        else
        {
            comparisons = Expression.Or(comparisons, comparison);
        }
    }

    // 3. Return the chained comparisons as a new predicate
    return Expression.Lambda<Func<TOwner, bool>>(comparisons, arg);
}

Used like:

var foos = await this.ODataApi.For<Foo>()
                              .WhereInFilter(x => x.FooID, fooIds)
                              .FindEntriesAsync()
                              .ConfigureAwait(false);
phatcher commented 5 years ago

@sixlettervariables Thanks, that's the sort of thing I was thinking of.

Think this should be in the library as it's a common problem and under the class of problem that it was intended to help with - mind adding it as a pull request once @object expresses an opinion?

sixlettervariables commented 5 years ago

Certainly, I'd enjoy some feedback on the naming of the API (and its shape) and where in the project it would best fit (i.e. new API on IBoundClient? Extension method?).

scsloan commented 11 months ago

Any update here?

@sixlettervariables did you have a shared library for this?

sixlettervariables commented 11 months ago

Apologies @scsloan but in my current role I no longer actively use OData.