【问题标题】:How to chain methods in .net with async/await如何在 .net 中使用 async/await 链接方法
【发布时间】:2019-03-15 04:43:25
【问题描述】:

我已经开始学习函数式编程,虽然链接方法在正常情况下看起来很棒(在我看来),但在处理 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());

这就是我一直在寻找的东西,即使我对最新的 cmets 感到困惑

【问题讨论】:

  • 你没有。如果一个人需要另一个方法的结果,不要链接这些方法。或者将它们转换为接受 Task&lt;Whatever&gt; 作为其第一个成员的扩展方法
  • 我通常会为此目的使用Facade Design Pattern
  • @Panagiotis Kanavos 这就是我所做的,也许不是我应该做的?我已经编辑了原始帖子以显示链接方法的实际签名
  • @XavSc 首先,它是 await 将实际的异步方法与延续联系起来 - 无论它之后发生什么。其次,扩展方法应该是Historize( this Task&lt;ApplicationProcess&gt; process, ...)` 以允许直接链接。 每个 这样的方法必须使用await process 来获取实际的进程对象。但是,您希望通过链接这样的方法获得什么? fn 是做什么的,它依赖于process 吗?如果要创建处理步骤的管道,请使用 Dataflow 库
  • @XavSc 这样的链接方法不会提高代码的可读性,恰恰相反。它也不会创建管道,它只是一系列异步调用。通过使用 Dataflow 块创建真正的管道,您可以使用输入/输出缓冲、背压、可配置的步骤并行性来真正执行步骤。

标签: c# .net async-await method-chaining


【解决方案1】:

我上传的所有代码都是LinqPad query,所以你可以马上试试。

函数式编程有一个monad 的概念(我强烈建议不熟悉的 C# 程序员从提供的链接开始)。 C# 任务可能被认为是一个 monad,据我了解,它正是您所需要的。

为了回答这个问题,我做了一个简单的例子来说明你所拥有的:

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# 有很好的语法来处理 monad:LINQ 查询理解语法。您只需要定义一个 SelectMany 方法,该方法适用于您想要的类型(在本例中为任务),然后您就可以开始了。

下面我实现了 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 行中唯一可能的表达式。 “硬编码”版本允许您使用任何表达式,该表达式使用在 from 关键字之后定义的任何值。

【讨论】:

【解决方案2】:

我试图以一种方式实现它 - 只有当源任务成功完成(没有错误)时,它才应该继续执行下一个。这似乎工作正常,但在任何情况下都试图思考这可能会失败:-

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

【讨论】:

    猜你喜欢
    • 2017-10-03
    • 1970-01-01
    • 2017-03-28
    • 2013-02-22
    • 2020-10-10
    • 1970-01-01
    • 2012-02-24
    • 2018-06-29
    • 2019-01-17
    相关资源
    最近更新 更多