Generic Linq to Entities filter method that accepts filter criteria and properties to be filtered

前端 未结 4 708
死守一世寂寞
死守一世寂寞 2021-01-01 07:35

I\'ve looked into many generic linq filtering questions and their answers here in SO but none of them satisfy my needs so I thought I should create a question.

I\'ve

相关标签:
4条回答
  • 2021-01-01 07:53

    Try to use Expressions like those all

    http://www.codeproject.com/Articles/493917/Dynamic-Querying-with-LINQ-to-Entities-and-Express

    0 讨论(0)
  • 2021-01-01 08:02

    You can use Linq.Dynamic to build the query.

    public static IQueryable<T> Match<T>(
        string searchTerm, 
        IQueryable<T> data, 
        params Expression<Func<T, string>>[] filterProperties) where T : class
    {
        var predicates = new List<string>();
        foreach (var prop in filterProperties)
        {
            var lambda = prop.ToString();
            var columnName = lambda.Substring(lambda.IndexOf('.') + 1);
            var predicate = string.Format(
                "({0} != null && {0}.ToUpper().Contains(@0))", columnName);
            predicates.Add(predicate);
        }
    
        var filter = string.Join("||", predicates);
        var results = data.Where(filter, searchTerm);
        return results;
    }
    

    Usage.

    var retailers = Match(
        "asd", db.Retailers, r => r.CompanyName, r => r.TradingName);
    
    var retailers = Match(
        "asd", db.Retailers, r => r.Address.Street, r => r.Address.Complement);
    

    Limitation.

    The filter can only accept basic expression.

    • r => r.Name
    • r => r.PropA.Name
    • r => r.PropA.PropB.Name
    0 讨论(0)
  • 2021-01-01 08:12

    So to solve this problem we need a few puzzle pieces first. The first puzzle piece is a method that can take an expression that computes a value, and then another expression that computes a new value taking the same type the first returns, and creates a new expression that represents the result of passing the result of the first function as the parameter to the second. This allows us to Compose expressions:

    public static Expression<Func<TFirstParam, TResult>>
        Compose<TFirstParam, TIntermediate, TResult>(
        this Expression<Func<TFirstParam, TIntermediate>> first,
        Expression<Func<TIntermediate, TResult>> second)
    {
        var param = Expression.Parameter(typeof(TFirstParam), "param");
    
        var newFirst = first.Body.Replace(first.Parameters[0], param);
        var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
    
        return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
    }
    

    This relies on the following tool to replace all instances of one expression with another:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    internal class ReplaceVisitor : ExpressionVisitor
    {
        private readonly Expression from, to;
        public ReplaceVisitor(Expression from, Expression to)
        {
            this.from = from;
            this.to = to;
        }
        public override Expression Visit(Expression node)
        {
            return node == from ? to : base.Visit(node);
        }
    }
    

    We'll also need a tool to help us OR two predicate expressions together:

    public static class PredicateBuilder
    {
        public static Expression<Func<T, bool>> True<T>() { return f => true; }
        public static Expression<Func<T, bool>> False<T>() { return f => false; }
    
        public static Expression<Func<T, bool>> Or<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
        }
    
        public static Expression<Func<T, bool>> And<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
        }
    }
    

    Now that we have this we can use Compose on each property selector to map it from the property results to whether or not that property value is non-null and contains the search term. We can then OR all of those predicates together to get a filter for your query:

    public static IQueryable<T> Match<T>(
        IQueryable<T> data,
        string searchTerm,
        IEnumerable<Expression<Func<T, string>>> filterProperties)
    {
        var predicates = filterProperties.Select(selector =>
                selector.Compose(value => 
                    value != null && value.Contains(searchTerm)));
        var filter = predicates.Aggregate(
            PredicateBuilder.False<T>(),
            (aggregate, next) => aggregate.Or(next));
        return data.Where(filter);
    }
    
    0 讨论(0)
  • 2021-01-01 08:12

    You can do it with expression trees but it's not as simple as you might think.

    public static IQueryable<T> Match<T>(this IQueryable<T> data, string searchTerm,
                                             params Expression<Func<T, string>>[] filterProperties)
    {
        var parameter = Expression.Parameter(typeof (T), "source");
    
        Expression body = null;
    
        foreach (var prop in filterProperties)
        {
            // need to replace all the expressions with the one parameter (gist taken from Colin Meek blog see link on top of class)
    
            //prop.body should be the member expression
            var propValue =
                prop.Body.ReplaceParameters(new Dictionary<ParameterExpression, ParameterExpression>()
                    {
                        {prop.Parameters[0], parameter}
                    });
    
    
            // is null check
            var isNull = Expression.NotEqual(propValue, Expression.Constant(null, typeof(string)));
    
            // create a tuple so EF will parameterize the sql call
            var searchTuple = Tuple.Create(searchTerm);
            var matchTerm = Expression.Property(Expression.Constant(searchTuple), "Item1");
            // call ToUpper
            var toUpper = Expression.Call(propValue, "ToUpper", null);
            // Call contains on the ToUpper
            var contains = Expression.Call(toUpper, "Contains", null, matchTerm);
            // And not null and contains
            var and = Expression.AndAlso(isNull, contains);
            // or in any additional properties
            body = body == null ? and : Expression.OrElse(body, and);
        }
    
        if (body != null)
        {
            var where = Expression.Call(typeof (Queryable), "Where", new[] {typeof (T)}, data.Expression,
                                        Expression.Lambda<Func<T, bool>>(body, parameter));
            return data.Provider.CreateQuery<T>(where);
        }
        return data;
    }
    
     public static Expression ReplaceParameters(this Expression exp, IDictionary<ParameterExpression, ParameterExpression> map)
    {
        return new ParameterRebinder(map).Visit(exp);
    }
    

    Now you need to have a expressionvisitor to make all the expressions use one parameter

    //http://blogs.msdn.com/b/meek/archive/2008/05/02/linq-to-entities-combining-predicates.aspx
    public class ParameterRebinder : ExpressionVisitor
    {
        private readonly IDictionary<ParameterExpression, ParameterExpression> _map;
    
        public ParameterRebinder(IDictionary<ParameterExpression, ParameterExpression> map)
        {
            _map = map;
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (_map.ContainsKey(node))
            {
                return _map[node];
            }
            return base.VisitParameter(node);
        }
    }
    

    Would use it like

    var matches = retailers.Match("7", r => r.Address.Street, x => x.Address.Complement).ToList();
    

    Warning - I checked this with linq to objects using the AsQueryable but didn't run it against EF.

    0 讨论(0)
提交回复
热议问题