Как отложить выполнение запроса EF до тех пор, пока он действительно не понадобится

У меня есть функция, которая определяет разрешения пользователя на взаимодействие с определенными объектами на основе набора правил, которые требуют разных способов запрашивать несколько частей базы данных. Один из этих запросов является особенно дорогим (он запрашивает массивное представление, состоящее из объединений и CTE), и этот запрос возвращает около 50 результатов для текущего пользователя. Этот конкретный запрос в настоящее время выполняется в направлении начала этой функции и заполняет список с именем userRoles. Затем следует серия из 10 блоков if...else if...else if..., что означает, что как только один из этих блоков соответствует условию, он переходит в конец и возвращает результат без обработки остальной части. их. Список userRoles используется в 4 из этих блоков, но есть 3 блока, предшествующих первому блоку, в котором он используется. Таким образом, если условия в любом из этих первых трех блоков совпадают, то в первую очередь нет причин запрашивать дорогостоящее представление.

В псевдокоде вот как это выглядит сейчас:

var userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if (userRoles.Any(x => x.role == "red")) { logicForRedRole(); }
else if (userRoles.Any(x => x.role == "blue")) { logicForBlueRole(); }
// more else if blocks...

Я хочу, чтобы запрос, который заполняет список, выполнялся только в том случае, если первые три блока не совпали, и теперь ему нужен список для проверки 4-го блока. Мне нужно, чтобы запрос был определен вверху, потому что на самом деле более эффективно просто получить все результаты, а затем отфильтровать их по случаям, которые необходимы в каждом из блоков, чем выполнять запрос для каждого блока. Я пытался создать объект Task<T>, передав асинхронный запрос (var userRolesTask = new Task<List<UserRole>>(async () => await DAL.GetUserRolesAsync(userId));), но это не работает, потому что на самом деле он создает объект Task<Task<List<UserRole>>> и его нужно развернуть, чтобы получить фактический результат, и я не уверен, что await userRolesTask фактически начнет выполнение запроса. Я мог бы изменить его на var userRolesTask = new Task<List<UserRole>>(() => DAL.GetUserRolesAsync(userId).Result);, но тогда он просто блокирует вызывающий поток, и я теряю преимущества асинхронности.

Итак, мне нужно что-то, что функционирует как задача, но на самом деле просто обертывает асинхронный метод и не выполняется до тех пор, пока не ожидается. Я определенно могу сделать что-то неуклюжее, которое бы принимало Func<Task<T>> и имело бы свойство T Result и метод async Task<T> GetResult(), который можно было бы ожидать, и который выполнял бы запрос только при первом вызове. Я просто надеялся найти что-то, что либо встроено, либо используется беспрепятственно без использования какой-либо дополнительной конструкции, такой как await userRolesTask.GetResult().

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

Думали ли вы о простом рефакторинге стека if elseif, чтобы можно было переместить запрос к БД вниз?

Fildor 13.04.2023 22:40

Не могли бы вы заменить var в var userRoles фактическим типом переменной?

Theodor Zoulias 13.04.2023 23:40

@TheodorZoulias Какая разница, какой на самом деле тип? Это может быть List<UserRole> или IEnumerable<Role>, это просто фиктивный псевдокод для иллюстративных целей. :)

CptRobby 14.04.2023 00:10

На самом деле разница между List<Role> и IEnumerable<Role> огромна. Если вы не хотите раскрывать тип, надеясь получить более общие ответы, это нормально, но вы можете пропустить более целенаправленные предложения.

Theodor Zoulias 14.04.2023 00:23
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
94
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

var userRoles;

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else {
    userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF
    if (userRoles.Any(x => x.role == "red")) { logicForRedRole(); }
    else if (userRoles.Any(x => x.role == "blue")) { logicForBlueRole(); }
    else if { ... }
}

Это не сработает, потому что после этого есть куча других блоков if. Я просто выложил упрощенную версию.

CptRobby 13.04.2023 22:41

Может быть, опубликуйте минимально воспроизводимый пример, чтобы нам не пришлось гадать, в чем проблема.

Fildor 13.04.2023 22:43

Я сделал. Думаю, я просто предположил, что «и т. д. И т. Д. И т. Д.» Будут пониматься как другие блоки else if. Спасибо, хотя бы за попытку. :)

CptRobby 13.04.2023 22:45

Я действительно не понимаю, почему код ответа не должен работать даже с несвязанными условными случаями впоследствии... по крайней мере, вы могли бы заставить его работать. Возможно, извлекая случаи, касающиеся БД, в функцию...

Fildor 13.04.2023 22:48

У вас не может быть else, если вы следуете за блоком else. Я имею в виду, что вы можете переместить все последующие блоки else if внутрь блока else, но вдобавок к тому, что он уродлив для чтения, фактический код намного сложнее, чем этот небольшой фрагмент псевдокода, и он просто не будет работать правильно. Псевдокод предназначен только в качестве иллюстративного примера, помогающего визуализировать проблему, а не саму проблему. Я ищу что-то, что можно использовать в нескольких местах. :)

CptRobby 13.04.2023 22:55

Вы можете реализовать и объявить его как IQueryable. Когда вам это понадобится, вы можете вызвать метод ToList() в этой функции. Таким образом, он будет выполнен после вызова метода ToList().

Я сделал попытку (фактический код не использует DAL). Но это на самом деле запускает запрос к базе данных несколько раз, что убивает производительность даже больше, чем однократный запуск, даже если в этом нет необходимости. Спасибо за предложение. :)

CptRobby 13.04.2023 22:59

Как насчет извлечения метода:

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if ( await IsUserRole() )
else if ...

//...

private async Task<bool> IsUserRole()
{
    var userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF

    if (userRoles.Any(x => x.role == "red")) 
    {
        logicForRedRole(); 
        return true;
    }
    if (userRoles.Any(x => x.role == "blue")) 
    {
        logicForBlueRole(); 
        return true;
    }
    return false;
}

И еще одна возможность — использовать Lazy<T>:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
                    
public class Program
{
    public static async Task Main()
    {
        // Just for example ...
        bool condition(int conditionNr) {
            Console.WriteLine("Testing condition #{0}", conditionNr);
            return false;
        }
        
        // The expensive Query
        var userId = 5;
        var expensiveDbAccess = new Lazy<Task<IEnumerable<int>>>(async () => {
            Console.WriteLine("Executing expensive query for User '{0}'", userId);
            await Task.Delay(1000); // "Doing some work"
            return new List<int>{1,2,3,4,5};
// This body would actually just be your
// return await DAL.GetUserRolesAsync(userId);
        });
        
        Console.WriteLine("Going down the rabbit hole ...");
        
        if (condition(1)){ Console.WriteLine("1"); }
        else if (condition(2)){ Console.WriteLine("2"); }
        else if (condition(3)){ Console.WriteLine("3"); }
        else if ( (await expensiveDbAccess.Value).Contains(6) ){ Console.WriteLine("Contained 6"); }
        else if ( (await expensiveDbAccess.Value).Contains(3) ){ Console.WriteLine("Contained 3"); }
        else if (condition(4)){ Console.WriteLine("4"); }
        else if (condition(5)){ Console.WriteLine("5"); }
    }
}

Я проверил это в Fiddle.

Выход:

Going down the rabbit hole ...
Testing condition #1
Testing condition #2
Testing condition #3
Executing expensive query for User '5'
Contained 3

Если вам нужен более сложный пример, не стесняйтесь комментировать или дополнять свой вопрос.

Хотя это и решает проблему блоков else if, следующих за блоком else в примере, фактическая функция намного сложнее, что делает ее очень трудной для фактической реализации, потому что она проверяет свойства других объектов и тому подобное. Слишком много всего происходит с этой функцией, и вообще не стоит думать о перестановке блоков. ржу не могу

CptRobby 13.04.2023 23:13

Мне любопытно, как бы вы использовали Lazy<T> в контексте асинхронного запроса EF.

CptRobby 13.04.2023 23:15

@CptRobby Добавлен пример для Lazy<T>. Имейте в виду, что я добавил несколько строк Console.WriteLines только для демонстрации потока и фактического выполнения запроса.

Fildor 14.04.2023 08:30

ХОРОШО. Я вижу как это. Lazy<T> в основном получает метод для заполнения значения (в данном случае это Task<T>), и этот метод выполняется только один раз.

CptRobby 14.04.2023 15:44

Точно. Он убирает шаблонный код для этого распространенного варианта использования.

Fildor 14.04.2023 18:15

Да, это не идеально, но я думаю, что это, по крайней мере, стоит того, чтобы проголосовать. На самом деле сегодня я сделал что-то совершенно другое и смог полностью исключить дорогостоящий запрос к БД, проанализировав, что делает представление и как каждое условие блока if использует другую часть массивного представления. Я смог заставить три места, которые его использовали, просто выполнять простые запросы к фактическим таблицам, содержащим соответствующие данные, и, похоже, это работает довольно хорошо. :)

CptRobby 14.04.2023 23:30

Мне все еще кажется, что должен быть LazyTask<T>, который будет принимать асинхронную функцию и выполнять ее только тогда, когда вам нужно получить результаты. Что-то вроде var userRolesTask = new LazyTask<IEnumerable<UserRole>>(async () => await DAL.GetUserRolesAsync(userId));, а потом else if ((await userRolesTask).Any(x => x.Role == "Red")) {.... Необходимость обернуть его в другую конструкцию просто делает его более громоздким для работы. Попытка Ярослава заключалась, по крайней мере, в том, что он ждал вызова функции, но он страдал от необходимости определять две вещи и связывать их вместе, чтобы заставить его работать.

CptRobby 14.04.2023 23:43

Я только что опубликовал ответ, который в значительной степени охватывает мой список пожеланий, и принял его, так как я думаю, что он будет наиболее полезен для других, которые ищут то, что я пытался найти. Но я хочу поблагодарить вас за вашу помощь! Я бы хотел, чтобы был способ дать частичный кредит больше, чем просто голосование. :)

CptRobby 17.04.2023 19:22

Если вы хотите получить какую-то «ленивую» загрузку, вы можете сделать следующее:

    public async Task YourMethod(Guid userId)
    {
        if (unrelatedConditions == 1) { doSomething1(); }
        else if (unrelatedConditions == 2) { doSomething2(); }
        else if (unrelatedConditions == 3) { doSomething3(); }
        else if ((await GetRolesAsync(userId)).Any(x => x.role == "red")) { logicForRedRole(); }
        else if ((await GetRolesAsync(userId)).Any(x => x.role == "blue")) { logicForBlueRole(); }
    }

    private List<Role> _roles;
    private async ValueTask<List<Role>> GetRolesAsync(Guild userId)
    {
        return _roles ??= await DAL.GetUserRolesAsync(userId)
    }

Обратите внимание: если ваша версия фреймворка не поддерживает ValueTask, вы можете заменить его на Task

Это интересное предложение. Я никогда раньше не видел ??=, и мне пришлось поискать, чтобы найти, что такое ValueTask. Это не будет работать так, как вы написали, потому что эта функция находится в общем провайдере, поэтому она не может хранить это в частном поле. Но это наводит меня на мысль, что я собираюсь попробовать, где асинхронная функция определена внутри самого метода, чтобы несколько вызовов функции для разных пользователей имели свою собственную копию. :)

CptRobby 13.04.2023 23:36

Это в основном ручная работа Lazy<T> ... но все же абсолютно действительная.

Fildor 14.04.2023 08:26

@YaroslavBres Я только что опубликовал ответ и принял его, так как думаю, что он будет наиболее полезен для других, которые ищут то, что я пытался найти. Но я хочу поблагодарить вас за вашу помощь! Это заставило меня пойти по правильному пути, и я хотел бы, чтобы был способ дать частичное признание, а не просто голосование. :)

CptRobby 17.04.2023 19:25
Ответ принят как подходящий

Как указано в моем комментарии к ответу Филдора, в итоге я выбрал совершенно другой маршрут и смог полностью реорганизовать дорогостоящий запрос на более мелкие части, каждая из которых выполняется только тогда, когда они действительно необходимы.

Но я все еще был заинтересован в решении проблемы на будущее, и, вдохновившись ответом Ярослава, я смог за выходные придумать что-то, что почти идеально удовлетворяет тому, что я искал. Это действительно простая универсальная функция, которую вы можете просто добавить в служебный класс для удобства использования. Вот код:

public static Func<Task<T>> CreateLazyTask<T>(Func<Task<T>> asyncCall)
{
    Task<T> executedTask = null;
    return () => executedTask ??= asyncCall();
}

Используя мой первоначальный пример кода, это можно использовать так:

var userRolesTask = CreateLazyTask(() => DAL.GetUserRolesAsync(userId)); // Expensive query using EF

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if ((await userRolesTask()).Any(x => x.role == "red")) { logicForRedRole(); }
else if ((await userRolesTask()).Any(x => x.role == "blue")) { logicForBlueRole(); }
// more else if blocks...

Есть несколько вещей, на которые стоит обратить внимание в этой реализации. Прежде всего, это то, что не используется async/await, и он просто возвращает Task<T> из asyncCall, поэтому он не пытается создать конечный автомат или что-то еще для этого, и ему не придется перепаковывать результаты в завершенный Task<T> в последующем. вызовов (это то, что делает код Ярослава), есть только один Task<T>, созданный asyncCall, который возвращается для каждого вызова функции, возвращаемой CreateLazyTask (что похоже на код Fildor Lazy<T>).

Кроме того, я почти уверен, что это должно быть потокобезопасным (в традиционном смысле). Таким образом, должна быть возможность создать функцию с этим и передать ее в набор рабочих потоков, и она будет выполнена в первый раз, когда она потребуется, и если другие потоки ожидают ее до завершения, она просто обработает ее правильно и возобновит все потоки, когда задача завершится, и последующие вызовы просто получат результат напрямую и продолжат работу. Я не думаю, что метод Ярослава будет потокобезопасным (я думаю, что одновременные вызовы вызовут многократное выполнение запроса, пока не будет возвращен результат), и я не совсем уверен, как пример Lazy<T> поведет себя в этом сценарии.

-- Если подумать об этом немного подробнее, на самом деле может быть очень небольшой временной отрезок, когда несколько вызовов могут инициировать выполнение несколько раз (между моментом выполнения asyncCall и срабатыванием первого ожидания, чтобы он мог назначить Task<T> обратно на executedTask), но обычно это будет гораздо меньшее окно, чем ожидание его фактического завершения. Существует также множество других сложностей, связанных с тем, какой тип данных возвращается и как они будут использоваться, которые необходимо учитывать, прежде чем фактически использовать что-то подобное в сценарии, допускающем параллельное выполнение. ---

Далее, нет необходимости указывать тип возвращаемого значения, так как компилятор может определить это непосредственно из переданного в asyncCall. Кроме того, нет необходимости использовать await для чего-то другого, кроме того, что создается (например, свойство Value в примере Lazy<T>), он просто ожидает результатов созданной функции. Так это намного легче читается.

Так что это в основном лучшие части ответов Филдора и Ярослава, все они заключены в очень аккуратно содержащуюся общую функцию, которую можно использовать где угодно для любого типа асинхронного вызова. Я надеюсь, что это поможет всем, у кого были такие же трудности, как и у меня! 😁

--- Изменить в ответ на комментарий о безопасности потоков ---

Итак, Стивен Клири, по-видимому, написал статью о создании асинхронной версии Lazy<T> под названием AsyncLazy<T> . Там он заявил, что «он не будет выполняться более одного раза, даже если несколько потоков попытаются запустить его одновременно (это гарантируется ленивым типом)». Так как он сказал это, я считаю, что метод Lazy<T> будет потокобезопасным. (Хотя глядя на справочный источник для Lazy<T>, я вижу комментарии, которые заставляют меня думать, что это все еще не 100% ...) В этой статье есть некоторые другие странности, которые я не понимаю (например, его использование Task. Run, который он специально не рекомендует во многих других статьях), но в целом это довольно хорошо, и похоже, что с тех пор он немного изменил его в своем проекте AsyncEx (включая удаление Task.Run).

Но для тех, кому нужно использовать то, что я написал, там, где требуется потокобезопасность, вот версия, которая использует Lazy<T> для предотвращения условий гонки:

public static Func<Task<T>> CreateLazyTask<T>(Func<Task<T>> asyncCall)
{
    var lazy = new Lazy<Task<T>>(asyncCall);
    return () => lazy.Value;
}

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

Theodor Zoulias 17.04.2023 19:40

@TheodorZoulias Но, черт возьми, в предоставленной вами ссылке есть почти то, что я искал для начала: AsyncLazy<T> от самого Стивена Клири! Если бы вы только что опубликовали это на прошлой неделе в качестве ответа вместо того, чтобы просить меня уточнить, какой тип возвращаемого значения у вымышленной функции DAL.GetUserRolesAsync, я бы, вероятно, принял это. РЖУ НЕ МОГУ

CptRobby 17.04.2023 20:48

Другие вопросы по теме

Похожие вопросы

Клиентская проекция содержит ссылку на постоянное выражение сквозного метода экземпляра
Получение данных из последовательного порта на более высоких скоростях передачи с использованием С#
Поле Fluent Validation Boolean должно быть обязательным
Как вы обновляете свойство LastUpdate родительских сущностей при добавлении или изменении дочернего элемента на любом уровне иерархии под ним в Entity Framework?
Невозможно заменить выбранный номер регулярного выражения типом следующего номера
С# перезаписывает текущую строку в консоли, не работая в conhost.exe
Является ли моя устойчивая функция Azure детерминированной?
Приложение Maui работает нормально на компьютере с Windows, но получает ошибки, когда я пытаюсь использовать физическое устройство Android
ASP.NET Core Web API и Autofac: разрешение из контейнера, но внутри метода GET контроллера
Ошибка службы Windows: 1053, не могу понять, почему