Generic Linq to Entities过滤器方法,它接受要过滤的过滤条件和属性

时间:2014-08-26 14:38:39

标签: c# entity-framework generics linq-to-entities

我已经在SO中查看了许多通用的linq过滤问题及其答案,但它们都没有满足我的需求,所以我认为我应该创建一个问题。

我创建了许多我称之为“过滤器提供程序”的类,一个用于我的模型中的每个实体类,以便为我的应用程序提供简单的搜索。我不想进入像Lucene.Net这样的更高级的解决方案,因为具有匹配分数的基本过滤就足够了。

在这些提供者类的每一个中,有多个方法将接收过滤术语和查询特定属性,并根据属性的相关性返回每个匹配的分数。大多数方法会一次过滤多个属性,但不是全部。

以下是其中两种方法:

private IQueryable<Retailer> MatchHighRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
    var results = retailers.Where(r =>
        (r.CompanyName != null && r.CompanyName.ToUpper().Contains(searchTerm))
        || (r.TradingName != null && r.TradingName.ToUpper().Contains(searchTerm))
    );

    return results;
}

private IQueryable<Retailer> MatchMediumRelevanceFields(string searchTerm, IQueryable<Retailer> retailers)
{
    var results = retailers.Where(r =>
        (r.Address.Street != null && r.Address.Street.ToUpper().Contains(searchTerm))
        || (r.Address.Complement != null && r.Address.Complement.ToUpper().Contains(searchTerm))
    );

    return results;
}

这些方法在每个提供程序类中都会被复制,我希望我可以将它们替换为可以接收要包含在查询中的属性的单个方法。

类似的东西:

public static IQueryable<T> Match<T>(string searchTerm, IQueryable<T> data, Expression<Func<T, string>> filterProperties)
{
    var results = **build the query for each property in filterProperties**

    return results;
}

但我真的无法理解。我尝试使用反射,但它只适用于Linq到Objects,我需要一个Linq to Entities的解决方案。

4 个答案:

答案 0 :(得分:5)

因此,要解决这个问题,我们首先需要一些拼图。第一个拼图是一个方法,它可以采用一个表达式来计算一个值,然后另一个表达式计算一个新值,它采用相同类型的第一个返回值,并创建一个新的表达式来表示传递第一个结果的结果作为第二个参数。这允许我们Compose表达式:

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);
}

这依赖于以下工具将一个表达式的所有实例替换为另一个:

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);
    }
}

我们还需要一个工具来帮助我们OR两个谓词表达式:

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);
    }
}

现在我们已经有了这个,我们可以在每个属性选择器上使用Compose将它从属性结果映射到该属性值是否为非null并包含搜索项。然后,我们可以将所有这些谓词组合在一起,以获得查询的过滤器:

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);
}

答案 1 :(得分:1)

您可以使用表达式树来完成它,但它并不像您想象的那么简单。

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);
}

现在你需要一个表达式访问器来使所有的表达式都使用一个参数

//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);
    }
}

会像

一样使用它
var matches = retailers.Match("7", r => r.Address.Street, x => x.Address.Complement).ToList();

警告 - 我使用AsQueryable使用linq检查了这个对象,但没有针对EF运行它。

答案 2 :(得分:0)

您可以使用Linq.Dynamic构建查询。

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;
}

使用。

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);

限制。

过滤器只能接受基本表达。

  • r => r.Name
  • r => r.PropA.Name
  • r => r.PropA.PropB.Name

答案 3 :(得分:0)