Объединение выражений Linq с использованием EFCore 3.0 - PullRequest
0 голосов
/ 11 ноября 2019

У меня есть функция, которая выполняет сложный запрос Where для моего контекста БД, а затем применяет другое переданное ему преобразование:

static IQueryable<T> Query<T>(Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
    try {
        using (var dbc = new MyDbContext() ) {
            var res = dbc.ServicesData
                .Where(sd =>
                    (path1 == null || (path1.Contains("%") || path1.Contains("_") ? EF.Functions.Like(sd.Path1, path1) : sd.Path1 == path1))
                    && (path2 == null || (path2.Contains("%") || path2.Contains("_") ? EF.Functions.Like(sd.Path2, path2) : sd.Path2 == path2))
                    && (path3 == null || (path3.Contains("%") || path3.Contains("_") ? EF.Functions.Like(sd.Path3, path3) : sd.Path3 == path3))
                    && (path4 == null || (path4.Contains("%") || path4.Contains("_") ? EF.Functions.Like(sd.Path4, path4) : sd.Path4 == path4))
                    && (path5 == null || (path5.Contains("%") || path5.Contains("_") ? EF.Functions.Like(sd.Path5, path5) : sd.Path5 == path5)));

            return f(res.ToList().AsQueryable());
            //return f(res).ToList().AsQueryable(); 
        }
    } catch (Exception ex_) {
        return VList<T>.Empty.AsQueryable();
    }
}

Это используется, например, так:

IQueryable<int> Int1InLastHour(IQueryable<ServicesData> input) {
    var lastHour = DateTimeOffset.Now.AddHours(-1).ToUnixTimeMilliseconds();
    return input
     .Where(v => (v.Time <= lastHour) && (v.Int1 is object))
     .Select(v => v.Int1.Value);
}

var lastHourProcessTime = Query(Int1InLastHour, "Publisher", "%", "ItemProcessTime").Sum();

Это работает, однако, так как я вызываю res.ToList() до вызова f, linq в f выполняется в памяти, а не в БД SQL

Если я пытаюсь заменить f(res.ToList().AsQueryable()) с f(res).ToList().AsQueryable() Я получаю исключение:

{"Обработка выражения LINQ '[EntityShaperExpression] [ServicesData]" с помощью' RelationalProjectionBindingExpressionVisitor 'завершилась неудачно. Это может указывать на ошибку или ограничение вEF Core. См. https://go.microsoft.com/fwlink/?linkid=2101433 для получения более подробной информации. "}

Можно ли как-нибудь решить эту проблему? Могу ли я как-то передать запрос (Func<IQueryable<ServicesData>, IQueryable<T>>), а затем объединить его с запросом в Query, прежде чем выполнить его в dbc?

1 Ответ

1 голос
/ 11 ноября 2019

Несколько вопросов. Вы можете разделить запрос, чтобы разбить результаты, но область действия вашего DbContext должна быть в самой внешней точке цепочки, а не внутри самой внутренней:

Это здесь:

static IQueryable<T> Query<T>(Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
    try {
        using (var dbc = new MyDbContext() ) { // DbContext should not be scoped here...
            var res = dbc.ServicesData

Как самый простой рефакторинг:

static IQueryable<T> Query<T>(MyDbContext dbc, Func<IQueryable<ServicesData>, IQueryable<T>> f, string path1 = null, string path2 = null, string path3 = null, string path4 = null, string path5 = null) {
    try 
    {
        var res = dbc.ServicesData.AsQueryable();
        if(path1 != null)
           if(path1.Contains("%") || path1.Contains("_"))
               res = res.Where(EF.Functions.Like(sd.Path1, path1));
           else
               res = res.Where(sd.Path1 == path1);
        // Repeat for Path 2 - 5 ....

        return f(res);
    } 
    catch (Exception ex_) 
    {
        return VList<T>.Empty.AsQueryable();
    }
}

Сначала мы передаем DbContext. Если контекст ограничен здесь, список должен быть материализован перед возвращением. Цель состоит в том, чтобы позволить вызывающим абонентам еще больше уменьшить выражение перед выполнением списка. Это означает, что DbContext должен быть ограничен за пределами этого начального поколения и передан. С контейнерами IoC, управляющими областью действия времени жизни, вы можете обойти это, если DbContext внедрен и ограничен областью запроса или общей области времени жизни.

Следующее предложение по улучшению состоит в том, чтобы переместить условные проверки параметров из Linq в обычные условия, чтобы проверка «Подобно / равно» была добавлена, только если было предоставлено условие. Это приведет к более простому и быстрому запуску SQL на сервере.

Таким образом, конечный результат будет выглядеть примерно так:

using (var dbContext = new MyDbContext())
{
   var lastHourProcessTime = Query(dbContext, Int1InLastHour, "Publisher", "%", "ItemProcessTime").Sum();
}

Я понимаю, что вы пытаетесь перейти сюда, но абстрагирование выражений от EF неизбежно приведет к запутанному коду и все еще подвержено ограничениям и ошибкам. ИМО, упрощая его, как правило, приводит к меньшему количеству проблем, но давайте посмотрим, приблизит ли это вас.

...