Search code examples
c#entity-frameworkentity-framework-coreexpression-trees

Entity Framework Core- Use Expression Tree With Interface As Parameter


I will really appreciate some help with following scenario. I have the following classes:

public class Product : IHasPrice
{
    public string Title { get; set; }
    public int Price { get; set; }
    public string CustomerId { get; set; }

}

public interface IHasPrice
{
    int Price { get; set; }
}

public class ProductProvider
{
    public ProductProvider()
    {
    }

    public IEnumerable<Product> GetByCustomer(string customerId, Expression<Func<IHasPrice, bool>> predicate = null)
    {
        using (var db = new ApplicationDbContext())
        {
            var queryable = db.Products.Where(p => p.CustomerId == customerId);
            if (predicate != null)
            {
                return queryable.Where(predicate).ToList();
            }
            else
            {
                return queryable.ToList();
            }
        }
    }
}

I want to enable to use the ProductProvider in a way where you can only select by customer, but you can also filter on the price in any way you like (and only on the price). This example doesn't work since queryable.Where expects parameter with typeof Expression(Func(Product, bool)). Is there a way to do it or I must fetch the data to memory before the filtering of the price?


Solution

  • Since IQueryable<out T> interface is covariant, the passed lambda expression can be used directly with Where method:

    var query = queryable.Where(predicate);
    

    The only problem is that now the type of the result query is IQueryable<IHasPrice>. You can turn it back to IQueryable<Product> using Queryable.Cast method:

    var query = db.Products.Where(p => p.CustomerId == customerId);
    if (predicate != null)
        query = query.Where(predicate).Cast<Product>(); // <--
    return query.ToList();
    

    Tested and working with the latest EF Core 2.2 (might fail in some earlier versions).


    An alternative solution is to convert the Expression<Func<IHasPrice, bool>> to the expected Expression<Func<Product, bool>> by "invoking" it:

    var query = db.Products.Where(p => p.CustomerId == customerId);
    if (predicate != null)
    {
        var parameter = Expression.Parameter(typeof(Product), "p");
        var body = Expression.Invoke(predicate, parameter);
        var newPredicate = Expression.Lambda<Func<Product, bool>>(body, parameter);
        query = query.Where(newPredicate);
    }
    return query.ToList();