У меня есть функция, которая определяет разрешения пользователя на взаимодействие с определенными объектами на основе набора правил, которые требуют разных способов запрашивать несколько частей базы данных. Один из этих запросов является особенно дорогим (он запрашивает массивное представление, состоящее из объединений и 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 для выполнения чего-то подобного, но это не вариант для этого проекта.
Не могли бы вы заменить var в var userRoles фактическим типом переменной?
@TheodorZoulias Какая разница, какой на самом деле тип? Это может быть List<UserRole> или IEnumerable<Role>, это просто фиктивный псевдокод для иллюстративных целей. :)
На самом деле разница между List<Role> и IEnumerable<Role> огромна. Если вы не хотите раскрывать тип, надеясь получить более общие ответы, это нормально, но вы можете пропустить более целенаправленные предложения.





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. Я просто выложил упрощенную версию.
Может быть, опубликуйте минимально воспроизводимый пример, чтобы нам не пришлось гадать, в чем проблема.
Я сделал. Думаю, я просто предположил, что «и т. д. И т. Д. И т. Д.» Будут пониматься как другие блоки else if. Спасибо, хотя бы за попытку. :)
Я действительно не понимаю, почему код ответа не должен работать даже с несвязанными условными случаями впоследствии... по крайней мере, вы могли бы заставить его работать. Возможно, извлекая случаи, касающиеся БД, в функцию...
У вас не может быть else, если вы следуете за блоком else. Я имею в виду, что вы можете переместить все последующие блоки else if внутрь блока else, но вдобавок к тому, что он уродлив для чтения, фактический код намного сложнее, чем этот небольшой фрагмент псевдокода, и он просто не будет работать правильно. Псевдокод предназначен только в качестве иллюстративного примера, помогающего визуализировать проблему, а не саму проблему. Я ищу что-то, что можно использовать в нескольких местах. :)
Вы можете реализовать и объявить его как IQueryable. Когда вам это понадобится, вы можете вызвать метод ToList() в этой функции. Таким образом, он будет выполнен после вызова метода ToList().
Я сделал попытку (фактический код не использует DAL). Но это на самом деле запускает запрос к базе данных несколько раз, что убивает производительность даже больше, чем однократный запуск, даже если в этом нет необходимости. Спасибо за предложение. :)
Как насчет извлечения метода:
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 в примере, фактическая функция намного сложнее, что делает ее очень трудной для фактической реализации, потому что она проверяет свойства других объектов и тому подобное. Слишком много всего происходит с этой функцией, и вообще не стоит думать о перестановке блоков. ржу не могу
Мне любопытно, как бы вы использовали Lazy<T> в контексте асинхронного запроса EF.
@CptRobby Добавлен пример для Lazy<T>. Имейте в виду, что я добавил несколько строк Console.WriteLines только для демонстрации потока и фактического выполнения запроса.
ХОРОШО. Я вижу как это. Lazy<T> в основном получает метод для заполнения значения (в данном случае это Task<T>), и этот метод выполняется только один раз.
Точно. Он убирает шаблонный код для этого распространенного варианта использования.
Да, это не идеально, но я думаю, что это, по крайней мере, стоит того, чтобы проголосовать. На самом деле сегодня я сделал что-то совершенно другое и смог полностью исключить дорогостоящий запрос к БД, проанализировав, что делает представление и как каждое условие блока if использует другую часть массивного представления. Я смог заставить три места, которые его использовали, просто выполнять простые запросы к фактическим таблицам, содержащим соответствующие данные, и, похоже, это работает довольно хорошо. :)
Мне все еще кажется, что должен быть LazyTask<T>, который будет принимать асинхронную функцию и выполнять ее только тогда, когда вам нужно получить результаты. Что-то вроде var userRolesTask = new LazyTask<IEnumerable<UserRole>>(async () => await DAL.GetUserRolesAsync(userId));, а потом else if ((await userRolesTask).Any(x => x.Role == "Red")) {.... Необходимость обернуть его в другую конструкцию просто делает его более громоздким для работы. Попытка Ярослава заключалась, по крайней мере, в том, что он ждал вызова функции, но он страдал от необходимости определять две вещи и связывать их вместе, чтобы заставить его работать.
Я только что опубликовал ответ, который в значительной степени охватывает мой список пожеланий, и принял его, так как я думаю, что он будет наиболее полезен для других, которые ищут то, что я пытался найти. Но я хочу поблагодарить вас за вашу помощь! Я бы хотел, чтобы был способ дать частичный кредит больше, чем просто голосование. :)
Если вы хотите получить какую-то «ленивую» загрузку, вы можете сделать следующее:
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. Это не будет работать так, как вы написали, потому что эта функция находится в общем провайдере, поэтому она не может хранить это в частном поле. Но это наводит меня на мысль, что я собираюсь попробовать, где асинхронная функция определена внутри самого метода, чтобы несколько вызовов функции для разных пользователей имели свою собственную копию. :)
Это в основном ручная работа Lazy<T> ... но все же абсолютно действительная.
@YaroslavBres Я только что опубликовал ответ и принял его, так как думаю, что он будет наиболее полезен для других, которые ищут то, что я пытался найти. Но я хочу поблагодарить вас за вашу помощь! Это заставило меня пойти по правильному пути, и я хотел бы, чтобы был способ дать частичное признание, а не просто голосование. :)
Как указано в моем комментарии к ответу Филдора, в итоге я выбрал совершенно другой маршрут и смог полностью реорганизовать дорогостоящий запрос на более мелкие части, каждая из которых выполняется только тогда, когда они действительно необходимы.
Но я все еще был заинтересован в решении проблемы на будущее, и, вдохновившись ответом Ярослава, я смог за выходные придумать что-то, что почти идеально удовлетворяет тому, что я искал. Это действительно простая универсальная функция, которую вы можете просто добавить в служебный класс для удобства использования. Вот код:
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;
}
В вопросе не упоминается параллелизм, поэтому ИМХО этот ответ не по теме. Это может быть ответ на этот вопрос по теме, но это будет ошибочный ответ, потому что предлагаемое решение не является потокобезопасным.
@TheodorZoulias Но, черт возьми, в предоставленной вами ссылке есть почти то, что я искал для начала: AsyncLazy<T> от самого Стивена Клири! Если бы вы только что опубликовали это на прошлой неделе в качестве ответа вместо того, чтобы просить меня уточнить, какой тип возвращаемого значения у вымышленной функции DAL.GetUserRolesAsync, я бы, вероятно, принял это. РЖУ НЕ МОГУ
Думали ли вы о простом рефакторинге стека if elseif, чтобы можно было переместить запрос к БД вниз?