Как связать методы в .net с помощью async/await

Я начал изучать функциональное программирование, и хотя цепочка методов выглядит великолепно (на мой взгляд) в обычных случаях, она действительно становится уродливой при работе с async/await.

await (await (await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId))
.Historize(() => _analyseFinanciereService.ProcessAsync(), 
    ProcessStepEnum.Application))
.Notify(p => p.GetLastStep());

Есть ли способ убрать этот шум?

Редактировать :

public static async Task<ApplicationProcess> Historize(
this ApplicationProcess process, 
Func<Task> fn, 
ProcessStepEnum stepEnum)
{
    var dateStart = DateTime.UtcNow;
    var error = string.Empty;
    try
    {
        await fn();
        return process;
    }
    …

public static async Task Notify<TResult>(
    this ApplicationProcess process, 
    Func<ApplicationProcess, TResult> fn)
...

Edit2: с методами расширения, принимающими задачу

await CosmosDbRepository<ApplicationProcess>
    .GetItemAsync(param.ProcessId)
    .HistorizeAsync(() => _analyseFinanciereService.ProcessAsync(), ProcessStepEnum.Application)
    .NotifyAsync(p => p.GetLastStep());

Так что это то, что я искал, даже если меня смущают последние комментарии


person Xav Sc    schedule 10.10.2018    source источник
comment
Вы не знаете. Не связывайте методы, если одному нужны результаты другого. Или преобразовать их в методы расширения, которые принимают Task<Whatever> в качестве своего первого члена.   -  person Panagiotis Kanavos    schedule 10.10.2018
comment
Обычно для этих целей я бы использовал шаблон проектирования фасада!   -  person Salah Akbari    schedule 10.10.2018
comment
@Panagiotis Kanavos это то, что я сделал, может быть, не так, как должен был? Я отредактировал исходный пост, чтобы показать фактическую подпись связанных методов.   -  person Xav Sc    schedule 10.10.2018
comment
@XavSc прежде всего, это await, который связывает фактический асинхронный метод с продолжением - все, что идет после него. Во-вторых, метод расширения должен быть Historize( this Task<ApplicationProcess> process,...)`, чтобы разрешить прямую цепочку. Однако каждый такой метод должен использовать await process для получения фактического объекта процесса. Что вы ожидаете получить, объединяя такие методы? Что делает fn, зависит ли это от process? Если вы хотите создать конвейеры шагов обработки, используйте библиотеку Dataflow.   -  person Panagiotis Kanavos    schedule 10.10.2018
comment
Подобные методы цепочки @XavSc не улучшат читаемость кода, а совсем наоборот. Он также не создаст конвейер, это просто серия асинхронных вызовов. Создавая настоящий конвейер с блоками Dataflow, вы получаете реальные шаги с буферизацией ввода/вывода, обратным давлением, настраиваемым параллелизмом для шагов.   -  person Panagiotis Kanavos    schedule 10.10.2018
comment
это эта либа? : www.nuget.org/packages/System.Threading.Tasks.Dataflow.   -  person Xav Sc    schedule 10.10.2018
comment
почему вы говорите, что это не улучшит читаемость? кроме синтаксического шума, я думаю, что да   -  person Xav Sc    schedule 10.10.2018
comment
Вы можете использовать Rx github.com/dotnet/reactive.   -  person Andrei Tătar    schedule 10.10.2018
comment
Можете ли вы уточнить немного? Что Rx позволит мне сделать в этом контексте?   -  person Xav Sc    schedule 10.10.2018


Ответы (2)


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

В функциональном программировании есть концепция монады (незнакомым с C# программистам я настоятельно рекомендую начать с предоставленной ссылки). Задача C# может считаться монадой, и насколько я понимаю, это именно то, что Вам нужно.

Для целей этого ответа я сделал упрощенный пример того, что у вас есть:

await (await (await A.GetNumber()).DoubleIt()).SquareIt()

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

public static class A
{
   public static Task<int> GetNumber(){return Task.FromResult(3);}
   public static Task<int> DoubleIt(this int input){return Task.FromResult(2 * input);}
   public static Task<int> SquareIt(this int input){return Task.FromResult(input * input);}
}

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

    public static async Task<TOut> AndThen<TIn, TOut>(this Task<TIn> inputTask, Func<TIn, Task<TOut>> mapping)
{
    var input = await inputTask;
    return (await mapping(input));
}

Метод AndThen действует точно так же, как монадическая привязка:

await 
    A.GetNumber()
    .AndThen(A.DoubleIt)
    .AndThen(A.SquareIt)

Что еще более важно, C# имеет удобный синтаксис для работы с монадами: синтаксис обработки запросов LINQ. Вам просто нужно определить метод SelectMany, который работает с желаемым типом (в данном случае Task), и вы готовы к работе.

Ниже я реализовал наиболее «хардкорную» перегрузку SelectMany (с дополнительными resultSelector), которая дает Вам наибольшую гибкость. Простая версия будет почти такой же, как AndThen (думаю, простое переименование сработает).

public static async Task<TOut> SelectMany<TIn, TInterm, TOut>(
   this Task<TIn> inputTask,
   Func<TIn, Task<TInterm>> mapping,
   Func<TIn, TInterm, TOut> resultSelector)
{
    var input = await inputTask;
    return resultSelector(input, await mapping(input));
}

С ним Вы можете использовать синтаксис:

var task = 
    from num in A.GetNumber()
    from doubled in num.DoubleIt()
    from squared in num.SquareIt()
    select $"number: {num} doubled: {doubled}, squared: {squared}";

Console.WriteLine(await task);

И Вы получаете number: 3 doubled: 6, squared: 9.

Простая версия SelectMany позволит вам использовать squared как единственное возможное выражение в последней строке select. Версия "hardcoder" позволяет Вам использовать любое выражение, которое использует любое из значений, определенных после ключевого слова from.

person Grzegorz Sławecki    schedule 10.10.2018
comment
интересно, я не понимаю, где вы вызываете свой собственный SelectMany, хотя - person Xav Sc; 10.10.2018
comment
Синтаксис обработки запросов, представленный здесь, компилируется в серию вызовов SelectMany. Вы можете прочитать больше: stackoverflow.com/a/29639502/1865984 или dzone.com/articles/selectmany-probably-the-most-p - person Grzegorz Sławecki; 10.10.2018

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

return await await sourceTask.ContinueWith(async st => 
            {
               var res = await st; //
               return await contWith(res);
            }, TaskContinuationOptions.NotOnFaulted & TaskContinuationOptions.OnlyOnRanToCompletion;
person Bose_geek    schedule 23.12.2019