0

Related Articles:

How do I build Expression Call for Any Method with generic parameter

LINQ Expression Tree: Call Any method against a DbSet

I'd like to be able to dynamically construct a where statement that checks the values of the property again a list of values.

Statically the call might look like:

IEnumerable<Guid> ids = //.... list of guids
context.DbSet<Person>()
  .Where(p => ids.Any(i => i == p.id))
  .ToList();

Table

[Table("Person")]
class Person 
{
    public Guid Id { get; set; }
    public string GivenName { get; set; }
    public string FamilyName { get; set; }
    public int ChildrenCount { get; set; }
}

I receive data via JSON so it's not strongly typed:

class Query
{
    public string PropertyName { get; set; } // 'Id'          or 'FamilyName' or 'ChildrenCount'
    public string AnyValues { get; set; }    // 'guid1,guid2' or 'Musk,Gates' or '1,2,3' respectively
}

Mostly Working example relevant to core question:

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.Generic;

                    
public class Program
{
    public static void Main()
    {
      var query = new Query
      {
        PropertyName = "id",
        AnyValues = new List<Guid> { Guid.Empty }
      };

      var exp = ToExpression<Person>(query);

      var people = context.DbSet<Person>().Where(exp).ToListAsync();
    }
    
    public static Expression ToExpression<TModel>(Query query)
    {
        Expression result = null;

        var propInfo = typeof(TModel).GetProperty(query.PropertyName, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance);

        if (propInfo != null)
        {
          var propType = propInfo.PropertyType;

          var parameterExpression = Expression.Parameter(typeof(TModel), nameof(TModel));

          var memberExpression = Expression.Property(parameterExpression, query.PropertyName);

          var constantExpression = GetConstantExpressionForType(propType, query.AnyValues);

          if (memberExpression != null && constantExpression != null)
          {
            // https://stackoverflow.com/questions/34730432/how-do-i-build-expression-call-for-any-method-with-generic-parameter
            // var org = Expression.Parameter(typeof(Organization), "org");
            // Expression<Func<OrganizationField, bool>> predicate = a => a.CustomField.Name == filter.Name && values.Contains(a.Value);
            // var body = Expression.Call(typeof(Enumerable), "Any", new[] { typeof(OrganizationField) },
            //   Expression.PropertyOrField(org, "OrganizationFields"), predicate);
            // var lambda = Expression.Lambda<Func<Organization, bool>>(body, org);   

            // STUCK HERE    
            result = Expression.Call(
              typeof(Enumerable),
              "Any",
              ?? );   
          }
        }

        return result;
    }
    
    // Just a Hack for the example, not relevant to the question itself
    public static ConstantExpression GetConstantExpressionForType(Type type, object values)
    {
        ConstantExpression result = null;
        
        if (type == typeof(Guid)) 
        {
          result = Expression.Constant(values, typeof(IEnumerable<Guid>));
        } 
        else if (type == typeof(string)) 
        {
          result = Expression.Constant(values, typeof(IEnumerable<string>));
        }
        else if (type == typeof(int)) 
        {
          result = Expression.Constant(values, typeof(IEnumerable<int>));
        }
        
        return result;
    }
}

public class Person 
{
    public Guid Id { get; set; }
    public string GivenName { get; set; }
    public string FamilyName { get; set; }
    public int ChildrenCount { get; set; }
}

public class Query
{
    public string PropertyName { get; set; } // 'Id'          or 'FamilyName' or 'ChildrenCount'
    //public string AnyValues { get; set; }    // 'guid1,guid2' or 'Musk,Gates' or '1,2,3' respectively
    public object AnyValues { get; set; }    // to simply the core of the question
}
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • Before dwelling into the Expression tree issue, I think there is an error in the first code snippet: IEnumerable ids = //.... list of guids context.DbSet().Where(p => ids.Any(p.id)).ToList(); Should be context.DbSet().Where(p => ids.Any(i => i == p.id)).ToList(); or context.DbSet().Where(p => ids.Contains(p.id)).ToList(); If I'm not mistaken, I don't think the query variation using Any can be used, because you are using a local collection (ids). Contains should work. – Laurent Gabiot Nov 07 '21 at 22:48
  • Join on hybrid collections might also be used. See https://stackoverflow.com/questions/8164460/local-sequence-cannot-be-used-in-linq-to-sql-implementation-of-query-operators-e – Laurent Gabiot Nov 07 '21 at 22:49
  • EF Can use Any(), depending on the situation (index etc) it can perform better than contains (https://dotnetfiddle.net/LBoZ1Y). – Erik Philips Nov 07 '21 at 23:19
  • 1
    Ok, my mistake, I had a doubt. Thanks for the question edit. – Laurent Gabiot Nov 07 '21 at 23:40
  • Use [this extension](https://stackoverflow.com/a/67666993/10646316). `FilterByItems` is exactly what you need. – Svyatoslav Danyliv Nov 08 '21 at 18:27
  • @SvyatoslavDanyliv if the goal was to not actually build the expression. That passes in the predicate, I want to build the predicate using any, so it solves the output problem, but doesn't do anything to help me understand building a call using Any, – Erik Philips Nov 08 '21 at 19:45
  • @ErikPhilips, EF Core do not support `Any` with local collections. That's why I have created this extension. – Svyatoslav Danyliv Nov 08 '21 at 20:28
  • @SvyatoslavDanyliv DbSet's aren't local collections. – Erik Philips Nov 08 '21 at 22:48
  • `ids ` is local collection. – Svyatoslav Danyliv Nov 09 '21 at 04:47
  • @SvyatoslavDanyliv Code seems to work fine with Any: https://dotnetfiddle.net/EUHTvJ – Erik Philips Nov 09 '21 at 12:37
  • @ErikPhilips, replace with Relational provider, SQLite for example. In-memory provider is far away from real usage. – Svyatoslav Danyliv Nov 09 '21 at 14:26
  • @SvyatoslavDanyliv I've been using ANY before Core existed, and I'm using it now. It not really relevant to the quesiton. – Erik Philips Nov 09 '21 at 16:10
  • My fault. Looks like EF translates such simple `Any`. Anyway prepared answer, sorry completely rewritten. – Svyatoslav Danyliv Nov 09 '21 at 18:47

1 Answers1

1

Use the following extension method:

public static class QueryableExtensions
{
    public static IQueryable<T> AnyFromItems<T>(this IQueryable<T> source, string items, string propName)
    {
        var strItems = items.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

        var entityParam = Expression.Parameter(typeof(T), "e");
        var propExpression = Expression.PropertyOrField(entityParam, propName);
        var itemType = propExpression.Type;
        var itemParam = Expression.Parameter(itemType, "i");

        var anyPredicate =
            Expression.Lambda(
                Expression.Equal(itemParam, propExpression),
                itemParam);

        // apply conversion
        var itemsExpression = Expression.Call(typeof(QueryableExtensions), nameof(QueryableExtensions.ParseItems),
            new[] { itemType }, Expression.Constant(strItems));

        var filterLambda =
            Expression.Lambda<Func<T, bool>>(
                Expression.Call(typeof(Enumerable), nameof(Enumerable.Any), new[] { itemType },
                    itemsExpression, anyPredicate),
                entityParam);

        return source.Where(filterLambda);
    }

    private static IEnumerable<TItem> ParseItems<TItem>(IEnumerable<string> items)
    {
        return items.Select(i => (TItem)Convert.ChangeType(i, typeof(TItem)));
    }
}

And usage:

var query = new Query
{
    PropertyName = "id",
    AnyValues = string.Join(", ", new List<Guid> { Guid.Empty });
};

var people = await context.DbSet<Person>()
    .AnyFromItems(query.AnyValues, query.PropertyName)
    .ToListAsync();
Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • Very nice. I found that `Any` is translated into a bunch of `Or` in the [Query Engine](https://blog.sqlauthority.com/2018/06/13/sql-server-performance-comparison-in-vs-or/) so my temporary solution was to loop through all the values and create or, within or, ^N. While it works, it's hard to read an maintain. Thank you very much. – Erik Philips Nov 10 '21 at 16:41
  • This kind of `Amy` translated into `IN`, which is ok. – Svyatoslav Danyliv Nov 10 '21 at 17:20