首页 > 解决方案 > 创建一个动态 EF 过滤器,为任何字符串实体属性构建一个 LINQ Where equals/contains 语句

问题描述

简而言之,我正在寻找这个人所做的事情,但是使用 Entity Framework 6。

实施建议的解决方案会导致错误“LINQ to Entities 不支持 LINQ 表达式节点类型 'Invoke'”。由于建议的解决方案使用Invoke,这显然是一个问题。

我知道有一种方法可以利用自定义 Compose 方法来重写表达式树而不使用Invoke,但我似乎无法理解它。

这就是我想要写的。

IQueryable<TEntity>使用一个对象动态构建了一个对象,该QueryParameters对象只是用于 WHERE 子句的一组属性。TEntity是一个标准的代码优先 EF 实体,到处都有数据注释。查询构造如下所示:

IQueryable<TEntity> query = Context.Set<TEntity>();

if (queryParams == null)
    return query;

if (!string.IsNullOrWhiteSpace(queryParams.FirstName))
{
    if (queryParams.ExactSearch)
    {
        query = query.Where(x => x.FirstName == queryParams.FirstName);
    }
    else
    {
        if (queryParams.PreferStartsWith)
        {
            query = query.Where(
                x => x.FirstName.ToLower()
                    .StartsWith(
                        queryParams.FirstName
                            .ToLower()));
        }
        else
        {            
            query = query.Where(
                x => x.FirstName.ToLower()
                    .Contains(
                        queryParams.FirstName
                            .ToLower()));
        }
    }
}

// ... repeat for all of queryParams' string props.
// DateTime, int, bool, etc have their own filters.

对于要查询的字符串字段的每个查询参数,都会重复此操作。显然,这会导致大量重复代码。我希望能够编写一个带有这样签名的过滤器:

public static IQueryable<TEntity> Search<TEntity>(
    this IQueryable<TEntity> query,
    Expression<Func<TEntity, string>> fieldExpression,
    string searchValue,
    bool exactSearch = true,
    bool useStartsWithOverContains = false) {...}

然后我可以像这样消费:

if (!string.IsNullOrWhiteSpace(queryParams.FirstName))
{
    query = query.Search(
                x => x.FirstName,
                queryParams.FirstName,
                queryParams.ExactSearch,
                queryParams.PreferStartsWith);
}

最接近我对该扩展方法的定义如下,但如前所述,它会产生“LINQ to Entities 不支持'Invoke'”错误:

public static IQueryable<TEntity> Search<TEntity>(
    this IQueryable<TEntity> query,
    Expression<Func<TEntity, string>> fieldExpression,
    string searchValue,
    bool exactSearch = true,
    bool useStartsWithOverContains = false)
{
    if (string.IsNullOrWhiteSpace(searchValue))
        return query;

    searchValue = searchValue.Trim();

    Expression<Func<TEntity, bool>> expression;

    if (exactSearch)
    {
        var x = Expression.Parameter(typeof(TEntity), "x");

        var left = Expression.Invoke(fieldExpression, x);
        var right = Expression.Constant(searchValue);
        var equalityExpression = Expression.Equal(left, right);

        expression = Expression.Lambda<Func<TEntity, bool>>(
            equalityExpression,
            x);
    }
    else
    {
        searchValue = searchValue.ToLower();
        var x = Expression.Parameter(typeof(TEntity), "x");

        var fieldToLower = Expression.Call(
            Expression.Invoke(fieldExpression, x),
            typeof(string).GetMethod(
                "ToLower",
                Type.EmptyTypes));
        var searchValueExpression =
            Expression.Constant(searchValue);

        var body = Expression.Call(
            fieldToLower,
            typeof(string).GetMethod(
                useStartsWithOverContains ? "StartsWith" : "Contains",
                new[] { typeof(string) }),
            searchValueExpression);

        expression = Expression.Lambda<Func<TEntity, bool>>(
            body,
            x);
    }

    return query.Where(expression);
}

我开始包含我提到的Compose 方法,但我很快就迷路了,因此将其删除。

接受任何指导!谢谢!

标签: c#entity-frameworklinqexpressioniqueryable

解决方案


编写表达式比每次都尝试手动构造表达式要容易得多它的编写速度更快,不容易出错,并且实际上最终得到了您可以在其末尾实际阅读的代码。您需要做的就是编写代码,说明如何使用组合表达式中的值,您已经从原始代码中获得了这些值。

public static IQueryable<TEntity> Search<TEntity>(
    this IQueryable<TEntity> query,
    Expression<Func<TEntity, string>> fieldExpression,
    string searchValue,
    bool exactSearch = true,
    bool useStartsWithOverContains = false)
{
    if (string.IsNullOrWhiteSpace(searchValue))
        return query;

    searchValue = searchValue.Trim();

    if (exactSearch)
    {
        return query.Where(fieldExpression.Compose(field => field == searchValue));
    }
    else if (useStartsWithOverContains)
    {
        return query.Where(fieldExpression.Compose(field => field.StartsWith(searchValue.ToLower())));
    }
    else
    {
        return query.Where(fieldExpression.Compose(field => field.Contains(searchValue.ToLower())));
    }
}

请注意,您可能应该使用“比较”或类似的枚举,而不是使用两个布尔值。例如,现在有人可以说他们不想完全确定,但他们确实想使用开头。三个选项中只有一个参数。


推荐阅读