Подтвердить что ты не робот

Динамически добавлять новые лямбда-выражения для создания фильтра

Мне нужно сделать некоторую фильтрацию на ObjectSet, чтобы получить нужные мне объекты:

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;

Позже в коде (и перед запуском отложенного выполнения) я снова фильтрую запрос следующим образом:

query = query.Where(<another lambda here ...>);

Это хорошо работает до сих пор.

Вот моя проблема:

Объекты содержат свойство DateFrom и свойство DateTo, которые являются типами DataTime. Они представляют собой период времени.

Мне нужно отфильтровать объекты, чтобы получить только те, которые являются частью коллекции периодов времени. Периоды в коллекции не обязательно смежные, поэтому логика для восстановления объектов выглядит следующим образом:

entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo)
||
entities.Where(x => x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo)
||

... и снова и снова для всех периодов в коллекции.

Я пробовал это:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;

    query = query.Where(de =>
        de.Date >= period.DateFrom && de.Date <= period.DateTo);
}

Но как только я запускаю отложенное выполнение, он переводит это в SQL так же, как я хочу (один фильтр для каждого из периодов времени для такого количества периодов, которые есть в коллекции), НО это означает вместо И сравнения OR сравнения, которая не возвращает никаких сущностей вообще, поскольку сущность не может быть частью более чем одного периода времени, очевидно.

Мне нужно создать какой-то динамический linq для агрегирования фильтров периодов.


Обновление

На основе ответа на шляпу я добавил следующего участника:

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}

Объявлено новое выражение CombineWithOr:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false;

И использовал его в моей итерации коллекции периода следующим образом:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo;
    resultExpression = this.CombineWithOr(resultExpression, expression);
}

var documentEntries = query.Where(resultExpression.Compile()).ToList();

Я посмотрел на полученный SQL, и это похоже на то, что выражение не имеет никакого эффекта. Полученный SQL возвращает ранее запрограммированные фильтры, но не комбинированные фильтры. Почему?


Обновление 2

Я хотел дать предложение feO2x попробовать, поэтому я переписал свой запрос фильтра следующим образом:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))

Как вы можете видеть, я добавил AsEnumerable(), но компилятор дал мне ошибку, что он не может преобразовать IEnumerable обратно в IQueryable, поэтому я добавил ToQueryable() в конце моего запроса:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();

Все работает отлично. Я могу скомпилировать код и запустить этот запрос. Однако это не соответствует моим потребностям.

При профилировании полученного SQL я вижу, что фильтрация не является частью SQL-запроса, поскольку она фильтрует даты в памяти во время процесса. Я предполагаю, что вы уже знаете об этом, и это то, что вы намеревались предложить.

Ваше предложение работает, НО, так как он извлекает все сущности из базы данных (и их тысячи и тысячи), прежде чем фильтровать их в памяти, он действительно медленно возвращает тот огромный сумму из базы данных.

Я действительно хочу отправить фильтрацию периода как часть результирующего SQL-запроса, поэтому он не вернет огромное количество объектов до окончания процесса фильтрации.

4b9b3361

Ответ 1

Несмотря на хорошие предложения, мне пришлось пойти с LinqKit. Одна из причин заключается в том, что мне придется повторять такую ​​же совокупность предикатов во многих других местах кода. Использование LinqKit является самым простым, не говоря уже о том, что я могу сделать это, написав только несколько строк кода.

Вот как я решил свою проблему с помощью LinqKit:

var predicate = PredicateBuilder.False<Document>();
foreach (var submittedPeriod in submittedPeriods)
{
    var period = period;
    predicate = predicate.Or(d =>
        d.Date >= period.DateFrom && d.Date <= period.DateTo);
}

И я запускаю отложенное выполнение (обратите внимание, что я вызываю AsExpandable() как раз перед):

var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();

Я просмотрел полученный SQL и неплохо выполнил перевод моих предикатов в SQL.

Ответ 2

Вы можете использовать следующий метод:

Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}

И затем:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR.
foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<T, bool>> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo);
    resultExpression = CombineWithOr(resultExpression, expression);
}

// Don't forget to compile the expression in the end.
query = query.Where(resultExpression.Compile());

Для получения дополнительной информации вы можете проверить следующее:

Объединение двух выражений (Expression < Func < T, bool → )

http://www.albahari.com/nutshell/predicatebuilder.aspx

Изменить: Линия Expression<Func<DocumentEntry, bool>> resultExpression = n => false; является просто заполнителем. Для метода CombineWithOr необходимо комбинировать два метода, если вы пишете цикл Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call to CombineWithOr for the first time in your foreach`. Это похоже на следующий код:

int resultOfMultiplications = 1;
for (int i = 0; i < 10; i++)
    resultOfMultiplications = resultOfMultiplications * i;

Если в начале нет resultOfMultiplications, вы не можете использовать его в своем цикле.

Что касается лямбда n => false. Потому что это не имеет никакого эффекта в инструкции OR. Например, false OR someExpression OR someExpression равно someExpression OR someExpression. Этот false не имеет никакого эффекта.

Ответ 3

Как насчет этого кода:

var targets = query.Where(de => 
    ratePeriods.Any(period => 
        de.Date >= period.DateFrom && de.Date <= period.DateTo));

Я использую оператор LINQ Any, чтобы определить, существует ли какой-либо период периода, который соответствует de.Date. Хотя я не совсем уверен, как это преобразуется в эффективные SQL-выражения сущностью. Если бы вы могли опубликовать полученный SQL, это было бы очень интересно для меня.

Надеюсь, что это поможет.

ОБНОВЛЕНИЕ после ответа на шляпу:

Я не думаю, что это решение будет работать, потому что Entity Framework использует выражения LINQ для создания SQL или DML, которые выполняются в базе данных. Следовательно, Entity Framework использует интерфейс IQueryable<T>, а не IEnumerable<T>. Теперь на обоих интерфейсах реализованы операторы LINQ по умолчанию (например, Where, Any, OrderBy, FirstOrDefault и т.д.), Поэтому иногда трудно увидеть разницу. Основное отличие этих интерфейсов заключается в том, что в случае методов расширения IEnumerable<T> возвращаемые перечисляемые объекты постоянно обновляются без побочных эффектов, тогда как в случае IQueryable<T> выполняется повторное вычисление фактического выражения, которое не содержит побочных эффектов ( т.е. вы изменяете дерево выражений, которое, наконец, используется для создания SQL-запроса).

Теперь Entity Framework поддерживает ca. 50 стандартных операторов запросов LINQ, но если вы пишете свои собственные методы, которые управляют IQueryable<T> (например, методом hatenn), это приведет к тому, что дерево выражений, которое Entity Framework не сможет проанализировать, потому что оно просто не знает новый способ расширения. Это может быть причиной, по которой вы не можете видеть объединенные фильтры после их составления (хотя я бы ожидал исключения).

Когда работает решение с оператором Any:

В комментариях вы сказали, что столкнулись с System.NotSupportedException: Не удалось создать постоянное значение типа "RatePeriod". В этом контексте поддерживаются только примитивные типы или типы перечислений. Это тот случай, когда объекты RatePeriod являются объектами в памяти и не отслеживаются Entity Framework ObjectContext или DbContext. Я сделал небольшое тестовое решение, которое можно скачать здесь: https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip

Я использовал Visual Studio 2012 с LocalDB и Entity Framework 5. Чтобы увидеть результаты, откройте класс LinqToEntitiesOrOperatorTest, затем откройте Test Explorer, постройте решение и запустите все тесты. Вы поймете, что ComplexOrOperatorTestWithInMemoryObjects провалится, все остальные должны пройти.

Контекст, который я использовал, выглядит следующим образом:

public class DatabaseContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<RatePeriod> RatePeriods { get; set; }
}
public class Post
{
    public int ID { get; set; }
    public DateTime PostDate { get; set; }
}
public class RatePeriod
{
    public int ID { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

Ну, это так просто, как это получается:-). В тестовом проекте есть два важных метода unit test:

    [TestMethod]
    public void ComplexOrOperatorDBTest()
    {
        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post =>
                DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));

        Assert.AreEqual(3, allAffectedPosts.Count());
    }

    [TestMethod]
    public void ComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };

        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }

Обратите внимание, что первый метод проходит, а второй - с указанным выше исключением, хотя оба метода делают то же самое, за исключением того, что во втором случае я создал объекты периода периода в памяти, DatabaseContext не знает о.

Что вы можете сделать для решения этой проблемы?

  • Соответствуют ли ваши объекты RatePeriod тем же ObjectContext или DbContext? Затем используйте их прямо из него, как это было в первом unit test, упомянутом выше.

  • Если нет, можете ли вы загружать все свои сообщения сразу или это приведет к OutOfMemoryException? Если нет, вы можете использовать следующий код. Обратите внимание на вызов AsEnumerable(), который приводит к тому, что оператор Where используется вместо интерфейса IEnumerable<T> вместо IQueryable<T>. Эффективно это приводит к тому, что все сообщения загружаются в память и затем фильтруются:

    [TestMethod]
    public void CorrectComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };
    
        var allAffectedPosts =
            DatabaseContext.Posts.AsEnumerable()
                           .Where(
                               post =>
                               inMemoryRatePeriods.Any(
                                   period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
    
  • Если второе решение невозможно, я бы рекомендовал написать хранимую процедуру TSQL, в которой вы проходите периоды ставок и формируете правильный оператор SQL. Это решение также является наиболее эффективным.

Ответ 4

В любом случае, я думаю, что динамическое создание запросов LINQ было не таким простым, как я думал. Попробуйте использовать Entity SQL, как показано ниже:

var filters = new List<string>();
foreach (var ratePeriod in ratePeriods)
{
    filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo));
}

var filter = string.Join(" OR ", filters);
var result = query.Where(filter);

Это может быть не совсем правильно (я не пробовал), но это должно быть что-то похожее на это.