use Expression<Func<T,X>> in Linq contains extension
Asked Answered
A

3

7

Using the following example i would like to use my Expression inside my Contains method, having it pass the query onto sql server using the EF.

How can i build this up to work correctly?

void Main()
{

    IQueryable<Person> qry = GetQueryableItemsFromDB();
    var filtered = qry.Filter(p=>p.CompanyId);

}

public static class Ext
{
    public static IQueryable<T> Filter<T>(this IQueryable<T> items, Expression<Func<T, int>> resolveCompanyIdExpression)
    {      
        IEnumerable<int> validComps = GetCompanyIdsFromDataBase();        
        var exp = Expression.Lambda<Func<T, bool>>(          
            Expression.Call(typeof(Queryable),"Contains", new[] { typeof(Company) },
            Expression.Constant(validComps),
            resolveCompanyIdExpression.Body),
            resolveCompanyIdExpression.Parameters[0]);
        return items.Where(exp);  
    }  

    public static IQueryable<T> Filter<T>(this IQueryable<T> items, Expression<Func<T, IEnumerable<int>>> resolveCompanyIdExpression)
    {      
        IEnumerable<int> validComps = GetCompanyIdsFromDataBase();        
        //No Idea what to do here ?
    }  
}

public class Person
{
    public int CompanyId {get;set;}
}

I know i could pass in the entire predicate but i only want the user to supply how to resolve the Company from the entity in question.

UPDATE

I have decided to resolve the companyId rather than the entire company entity, i can get the list of ids in memory and im not fussed if that is IQueryable or just a plain array/IEnumerable

However i get some strange errors :

An exception occured during the execution of ' Extent.Select(o => o).Where(p => (p.Hide = False)).Where(p => (p.Archived = False)).Where(item => System.Int32[].Contains(item.Development.CompanyId))'. See InnerException for more details.

Inner exception is

Argument expression is not valid

UPDATE 2

I have edited the code to reflect what i would really like like to do, not having much luck on finding a solution to this.

Analogue answered 10/4, 2012 at 14:1 Comment(2)
Even if you got this to work the way you have it, you'd be performing a separate round-trip for each item in the list because you're basing your query on the list of objects. You'll need to change your query to start with validComps, or get an IQueryable<T> from the database and start from there. You'll also need to base your "contains" on the unique IDs of the objects rather than the entire objects.Lawley
Ok it was my typo, that should be IQueryable<T>Analogue
I
8

If I'm understanding correctly, what you want is expression composition:

public static IQueryable<T> Filter<T>(IQueryable<T> query, Expression<Func<T, int>> getCompanyId) {
    IEnumerable<int> validCompanyIds = GetCompanyIdsFromDatabase();
    Expression<Func<int, bool>> filterByCompanyId = id => validCompanyIds.Contains(id);

    // these generics will actually be inferred, I've just written them to be explicit
    Expression<Func<T, bool>> composed = filterByCompanyId.Compose<T, int, bool>(getCompanyId);
    return query.Where(composed);
}

Below is the implementation of the Compose() extension method on expression:

    /// <summary>
    /// Composes two lambda expressions f(y) and g(x), returning a new expression representing f(g(x)).
    /// This is useful for constructing expressions to pass to functions like Where(). If given x => x.Id and id => ids.Contains(id),
    /// for example, you can create the expression x => ids.Contains(x.Id), which could be passed to Where() for an IQueryable of x's type
    /// </summary>
    /// <typeparam name="TIn">The input of g</typeparam>
    /// <typeparam name="TIntermediate">The output of g and the input of f</typeparam>
    /// <typeparam name="TOut">The output of f</typeparam>
    /// <param name="f">The outer function</param>
    /// <param name="g">The inner function</param>
    /// <returns>A new lambda expression</returns>
    public static Expression<Func<TIn, TOut>> Compose<TIn, TIntermediate, TOut>(this Expression<Func<TIntermediate, TOut>> f, Expression<Func<TIn, TIntermediate>> g)
    {
        // The implementation used here gets around EF's inability to process Invoke expressions. Rather than invoking f with the output of g, we
        // effectively "inline" g by replacing all instances of f's parameter with g's body and creating a new lambda with the rebound body of f and
        // the parameters of g
        var map = f.Parameters.ToDictionary(p => p, p => g.Body);            
        var reboundBody = ParameterRebinder.ReplaceParameters(map, f.Body);
        var lambda = Expression.Lambda<Func<TIn, TOut>>(reboundBody, g.Parameters);
        return lambda;
    }

public class ParameterRebinder : ExpressionVisitor
        { 
            private readonly Dictionary<ParameterExpression, Expression> Map;

            public ParameterRebinder(Dictionary<ParameterExpression, Expression> map)
            {
                this.Map = map ?? new Dictionary<ParameterExpression, Expression>();
            }

            public static Expression ReplaceParameters(Dictionary<ParameterExpression, Expression> map, Expression exp)
            {
                return new ParameterRebinder(map).Visit(exp);
            } 

            protected override Expression VisitParameter(ParameterExpression node)
            {
                Expression replacement;
                if (this.Map.TryGetValue(node, out replacement))
                {
                    return this.Visit(replacement);
                }
                return base.VisitParameter(node);
            }
        }
Irritate answered 31/8, 2012 at 0:23 Comment(0)
P
0

try Expression.Compile() method:

return items.Where(item => validComps.Contains(resolveCompanyExpression.Compile()(item))).AsQueryable();
Powerful answered 10/4, 2012 at 14:14 Comment(2)
I dont think that .Compile will work as an Expression, it returns Func<T,Company> for me and does not work when passing query to database.Analogue
perhaps this link might help: rapidapplicationdevelopment.blogspot.com/2008/03/…Powerful
B
0

Isn't the items parameter an IQueryable? If so, try this:

public static IQueryable<T> FilterByCompany<T>(this IQueryable<T> items, Expression<Func<T, Company>> resolveCompanyExpression)
    where T : EntityBase
{
    IQueryable<Company> validComps = GetCompaniesFromDataBase();

    var exp = Expression.Lambda<Func<T, bool>>(
        Expression.Call(
            typeof(Queryable),
            "Contains",
            new[] { typeof(Company) },
            Expression.Constant(validComps),
            resolveCompanyExpression.Body
        ),
        resolveCompanyExpression.Parameters[0]
    );
    return items.Where(exp);
}
Belden answered 10/4, 2012 at 14:28 Comment(1)
In the OP's example, this is a List<>.Reive

© 2022 - 2024 — McMap. All rights reserved.