【问题标题】:How to Design Fluent Async Operations?如何设计流畅的异步操作?
【发布时间】:2015-11-13 18:02:20
【问题描述】:

异步操作似乎不适用于我更喜欢​​编写代码的 fluent 接口。如何将异步与 Fluent 结合使用?


示例:我有两个方法以前返回 MyEntity,但在更改为 Async 时效果不佳。在我异步它们之后,我必须 await 任务的结果,但我必须为添加的每个步骤执行此操作:

MyEntity Xx = await(await FirstStepAsync()).SecondStepAsync();

必须有更好的方法。

【问题讨论】:

  • 你能显示这个异步方法的签名吗?

标签: c# .net async-await fluent


【解决方案1】:

更好的方法是延迟执行类似于 LINQ。

您可以有许多实际上不做任何事情的方法,它们只是设置一些选项或存储一些逻辑。最后还有一些方法可以实际执行之前存储的所有其他逻辑。

这样只有少数方法需要async,并且每个链的末尾只使用一个。

类似这样的:

var myEntity = await StartChain().StoreSomeLogic().StoreSomeOtherLogic().ExecuteAsync()

例如,新的异步 MongoDB C# 驱动程序就是这样工作的:

var results = await collection.Find(...).Project(...).Skip(...).Sort(...).ToListAsync();

【讨论】:

  • @i3amon 如果您可以将 som 设置 DTO 一直传递到最终的 Async 方法,那么这个解决方案看起来很棒。但这并不是我方法的本质。
  • @k.c.这是流畅 API 的本质。您有一个包含所有扩展方法的接口(例如IEnumerable),并且您有一些实现该接口的类,该接口必须从所有这些方法(最后一个方法除外)返回以启用下一个流畅的步骤。那个类就是你的 DTO。
  • @i3arnon 好主意,在我的情况下,调用者方法不能标记为async,所以await 不存在(在链的开头),有什么办法可以在这种情况下使用您的想法还是我应该使用替代模式?
  • 如果链中的某些方法需要自己调用异步方法,该解决方案将如何处理? .Wait() 每次这样的调用?
【解决方案2】:

一些处理延续的答案忘记了流利的工作在从每个方法返回的具体实例上。

我已经为您编写了一个示例实现。异步工作将在调用任何DoX 方法时立即开始。

public class AsyncFluent
{
    /// Gets the task representing the fluent work.
    public Task Task { get; private set; }

    public AsyncFluent()
    {
        // The entry point for the async work.
        // Spin up a completed task to start with so that we dont have to do null checks    
        this.Task = Task.FromResult<int>(0);
    }

    /// Does A and returns the `this` current fluent instance.
    public AsyncFluent DoA()
    {
        QueueWork(DoAInternal);
        return this;
    }

    /// Does B and returns the `this` current fluent instance.
    public AsyncFluent DoB(bool flag)
    {
        QueueWork(() => DoBInternal(flag));
        return this;
    }

    /// Synchronously perform the work for method A.
    private void DoAInternal()
    {
        // do the work for method A
    }

    /// Synchronously perform the work for method B.
    private void DoBInternal(bool flag)
    {
        // do the work for method B
    }

    /// Queues up asynchronous work by an `Action`.
    private void QueueWork(Action work)
    {
        // queue up the work
        this.Task = this.Task.ContinueWith<AsyncFluent>(task =>
            {
                work();
                return this;
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
    }
}

【讨论】:

  • 您的答案似乎最接近我想要实现的目标:在某个类实例上调用不同数量的方法。通常每个方法都会返回实例,以便可以对其调用其他操作。这有点像这样。虽然它提供了相当多的开销。谢谢
  • 看起来很复杂。
  • @JerryNixon 绝对。 Fluent 接口旨在消除使用代码的命令式复杂性,并用接口背后的大量结构复杂性取而代之。我很想更新答案以使用本地函数作为简化。
  • 我不明白你的例子,你没有任何异步/异步。
  • @GuilhermeFlores 是 Task 启用异步/等待,因为它实现了 GetAwaiter
【解决方案3】:

您可以添加一个扩展方法重载,该方法采用 TaskTask&lt;T&gt; 到您希望可链接的任何方法。

public static async Task<MyEntity> SecondStepAsync(this Task<MyEntity> entityTask)
{
    return (await entityTask).SecondStepAsync();
}

所以你可以打电话给await FirstStepAsync().SecondStepAsync()

【讨论】:

  • 这是最好的答案恕我直言。基本上对于每个StepAsync(this T),您都需要一个StepAsync(this Task&lt;T&gt;)。然后你可以正常链接它们
【解决方案4】:

其中一个选项是声明和使用以下通用扩展方法:

public static TR Pipe<T, TR>(this T target, Func<T, TR> func) =>
    func(target);

public static async Task<TR> PipeAsync<T, TR>(this Task<T> target, Func<T, TR> func) =>
    func(await target);

public static async Task<TR> PipeAsync<T, TR>(this Task<T> target, Func<T, Task<TR>> func) =>
    await func(await target);

这些实用程序允许以这种方式表示异步调用链:

MyEntity Xx = await FirstStepAsync()
    .PipeAsync(async firstResult => await firstResult.SecondStepAsync())
    .PipeAsync(async secondResult => await secondResult.ThirdStepAsync());

生成的代码可能看起来更冗长,但是扩展链更容易,因为没有嵌套括号。

【讨论】:

    【解决方案5】:

    await 基本上是ContinueWith 的简写,是Task 对象上的一个方法(我在这里进行了简化,但这是基本概念)。如果您正在尝试构建流畅的语法,请考虑使用接收任务并使用 ContinueWith 链接它们的扩展方法:

    public Task FirstStep()
    {
         return Task.Run(/* whatever */);
    }
    
    public static Task SecondStep (this Task previousStep)
    {
        return previousStep.ContinueWith(task => { /* whatver */  };
    }
    

    现在您可以致电await FirstStep().SecondStep(),等待最终结果。每个方法本质上都添加了另一个 ContinueWith 步骤。

    如果您想让它更安全,请从 Task 继承 MyFluentTask,并返回它而不是常规 Task。

    【讨论】:

    • 异常处理在 ContinueWith() 中更为基本 - 在现代异步代码中应该避免。
    【解决方案6】:

    将您的流利方法重写为这种形式 - 即,将它们作为扩展方法几乎没有意义:

            public static async Task<ResultType> Transform(SourceType original)
            {
                // some async work 
                var transformed = await DoSomething(original)
                return transformed;
            }
    

    提供这些通用扩展方法(与@Gennadii Saltyshchak 的 PipeSync 方法几乎相同,但我发现“Then”这个名字更简洁):

            public static async Task<T2> Then<T1, T2>(this T1 first, Func<T1, Task<T2>> second)
            {
                return await second(first).ConfigureAwait(false);
            }
    
            public static async Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, Task<T2>> second)
            {
                return await second(await first.ConfigureAwait(false)).ConfigureAwait(false);
            }
    
            public static async Task<T2> Then<T1, T2>(this Task<T1> first, Func<T1, T2> second)
            {
                return second(await first.ConfigureAwait(false));
            }
    
            public static Task<T2> Then<T1, T2>(this T1 first, Func<T1, T2> second)
            {
                return Task.FromResult(second(first));
            }
    

    然后你可以像这样构建一个流畅的链:

            var processed = await Transform1(source)
                .Then(Transform1)
                .Then(Transform2)
                .Then(Transform3);
    

    由于 Then() 重载,这适用于异步/同步方法的任何排列。

    如果例如Transform2 需要参数,你需要扩展为一个 lambda:

            var processed = await Transform1(source)
                .Then(Transform1)
                .Then(x => Transform2(x, arg1, arg2))
                .Then(Transform3);
    

    我认为这与使用当前语言所能获得的流畅的异步链一样接近!

    应尽可能使用 ValueTask 优化异步/同步的组合。给读者的练习! :-)

    【讨论】:

      【解决方案7】:

      另一个选项是实现基本的 LINQ 运算符以允许 LINQ 语法。然后你可以这样做:

      MyEntity Xx = await
          from f in FirstStepAsync()
          from e in f.SecondStepAsync()
          select e;
      

      你需要的代码是这样的:

      public static class Ex
      {
          public static Task<TResult> SelectMany<TSource, TResult>(this Task<TSource> source, Func<TSource, Task<TResult>> collectionSelector)
              => source.ContinueWith(t => collectionSelector(t.Result)).Unwrap();
      
          public static Task<TResult> SelectMany<TSource, TCollection, TResult>(this Task<TSource> source, Func<TSource, Task<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
              => source.ContinueWith(t =>
              {
                  Task<TCollection> ct = collectionSelector(t.Result);
                  return ct.ContinueWith(_ => resultSelector(t.Result, ct.Result));
              }).Unwrap();
      }
      

      【讨论】:

        猜你喜欢
        • 2021-08-24
        • 2014-09-21
        • 2011-02-17
        • 1970-01-01
        • 1970-01-01
        • 2015-04-20
        • 2023-03-16
        • 2020-01-18
        • 2019-01-14
        相关资源
        最近更新 更多