Динамический фильтр EF Core - PullRequest
0 голосов
/ 14 декабря 2018

Я работал в классе динамических фильтров для запросов EF Core, используя деревья выражений, все выглядит хорошо, фильтр работает, я могу передать коллекцию фильтров, и она работает, но когда я смотрю на предложение SQL, этоЗапрашивая всю таблицу и применяя фильтр к полученной коллекции, вот мой класс ...

   public static class QueryExpressionBuilder
{
    private static readonly MethodInfo ContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    private static readonly MethodInfo StartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
    private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });

    #region DynamicWhere

    /// <summary>Where expression generator.</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="filters">The filters.</param>
    /// <returns></returns>
    public static Expression<Func<T, bool>> GetExpression<T>(IList<Filter> filters)
    {
        if (filters.Count == 0)
            return null;

        ParameterExpression param = Expression.Parameter(typeof(T), "t");
        Expression exp = null;

        if (filters.Count == 1)
            exp = GetExpression(param, filters[0]);
        else if (filters.Count == 2)
            exp = GetExpression<T>(param, filters[0], filters[1]);
        else
        {
            while (filters.Count > 0)
            {
                var f1 = filters[0];
                var f2 = filters[1];

                if (exp == null)
                    exp = GetExpression<T>(param, filters[0], filters[1]);
                else
                    exp = Expression.AndAlso(exp, GetExpression<T>(param, filters[0], filters[1]));

                filters.Remove(f1);
                filters.Remove(f2);

                if (filters.Count == 1)
                {
                    exp = Expression.AndAlso(exp, GetExpression(param, filters[0]));
                    filters.RemoveAt(0);
                }
            }
        }

        return Expression.Lambda<Func<T, bool>>(exp, param);
    }

    /// <summary>Comparision operator expression generator.</summary>
    /// <param name="param">The parameter.</param>
    /// <param name="filter">The filter.</param>
    /// <returns></returns>
    private static Expression GetExpression(ParameterExpression param, Filter filter)
    {
        MemberExpression member = Expression.Property(param, filter.PropertyName);
        var type = member.Type;
        ConstantExpression constant;
        switch (type.Name)
        {
            case "Int32":
                constant = Expression.Constant(Convert.ToInt32(filter.Value));
                break;
            case "String":
            default:
                constant = Expression.Constant(filter.Value);
                break;
        }

        // ConstantExpression constant = Expression.Constant(filter.Value);

        switch (filter.Operation)
        {
            case Op.Equals:
                return Expression.Equal(member, constant);

            case Op.GreaterThan:
                return Expression.GreaterThan(member, constant);

            case Op.GreaterThanOrEqual:
                return Expression.GreaterThanOrEqual(member, constant);

            case Op.LessThan:
                return Expression.LessThan(member, constant);

            case Op.LessThanOrEqual:
                return Expression.LessThanOrEqual(member, constant);

            case Op.Contains:
                return Expression.Call(member, ContainsMethod, constant);

            case Op.StartsWith:
                return Expression.Call(member, StartsWithMethod, constant);

            case Op.EndsWith:
                return Expression.Call(member, EndsWithMethod, constant);
        }

        return null;
    }

    /// <summary>And logic connector expression generator.</summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="param">The parameter.</param>
    /// <param name="filter1">The filter1.</param>
    /// <param name="filter2">The filter2.</param>
    /// <returns></returns>
    private static BinaryExpression GetExpression<T>(ParameterExpression param, Filter filter1, Filter filter2)
    {
        var bin1 = GetExpression(param, filter1);
        var bin2 = GetExpression(param, filter2);

        return Expression.AndAlso(bin1, bin2);
    }

    #endregion

}

}

Чтобы вызвать этот класс, я делаю что-то вроде этого:

var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();

Параметр filters, который я передаю, является коллекцией этого класса:

public class Filter
{
    public string PropertyName { get; set; }
    public Op Operation { get; set; }
    public object Value { get; set; }
}

Буду признателен за любую помощь.

1 Ответ

0 голосов
/ 14 декабря 2018

Основная проблема не в классе, а в том, как вы его используете:

var whereDeleg = QueryExpressionBuilder.GetExpression<Tax>(filters).Compile();
var myList = _dbContext.MyEntity.Where(whereDeleg).ToList();

Вы берете Expression<Func<T, bool>> из вашего метода, но затем вызов Complie() преобразует его в Func<T, bool>,Таким образом, хотя _dbContext.MyEntity равно IQueryable<T>, не существует IQueryable<T> метода расширения Where, принимающего Func<T, bool> (все они принимают Expression<Func<T, bool>>).Но поскольку IQueryable<T> наследует (следовательно, является) IEnumerable<T>, компилятор находит и использует метод расширения Where для IEnumerable<T> (определенный в классе Enumerable).

Это заставляет Where (и все последующие методы, если таковые имеются) выполнять на стороне клиента после выполнения и материализации запроса до Where (в вашем случае - всей таблицы).

Разница между IQueryable<T> и IEnumerable<T> покрывается Возвращая IEnumerable против IQueryable .Все, что вам нужно, это убедиться, что вы всегда вызываете IQueryable<T> методы расширения вместо IEnumerable<T> методов с тем же именем и аналогично выглядящим аргументами, используя Expression<Func<...>> вместо Func<...>.

Учитывая все вышесказанное, вы должны использовать результат метода напрямую, не вызывая Compile:

var predicate = QueryExpressionBuilder.GetExpression<Tax>(filters);
var myList = _dbContext.MyEntity.Where(predicate).ToList();

или просто

var myList = _dbContext.MyEntity.Where(QueryExpressionBuilder.GetExpression<Tax>(filters)).ToList();

Или, что еще лучше, добавьте следующий пользовательскийметод расширения до QueryExpressionBuilder класса:

public static IQueryable<T> Where<T>(this IQueryable<T> source, IList<Filter> filters)
{
    var predicate = GetExpression<T>(filters);
    return predicate != null ? source.Where(predicate) : source;
}

, чтобы можно было просто использовать (и минимизировать вероятность ошибок):

var myList = _dbContext.MyEntity.Where(filters).ToList();

Примечание:Реализация метода основного построителя выражений слишком сложна и также уничтожает переданный список ввода filters.Это можно упростить следующим образом (который не имеет вышеупомянутого дефекта):

public static Expression<Func<T, bool>> GetExpression<T>(IEnumerable<Filter> filters)
{
    var param = Expression.Parameter(typeof(T), "t");
    var body = filters
        .Select(filter => GetExpression(param, filter))
        .DefaultIfEmpty()
        .Aggregate(Expression.AndAlso);
    return body != null ? Expression.Lambda<Func<T, bool>>(body, param) : null;
}
...