如何设计流畅的异步操作?

How to Design Fluent Async Operations?

异步操作似乎不能很好地与我更喜欢编写代码的流畅界面配合使用。异步如何与流畅结合?


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

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

必须有更好的方法。

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

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

这样只需要几个方法 async 并且在每个链的末尾只使用一个方法。

像这样:

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

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

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

await 基本上是 shorthand for ContinueWithTask 对象上的一个方法(我在这里进行了简化,但这是基本概念)。如果您正在尝试构建流畅的语法,请考虑使用接收任务的扩展方法并使用 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,然后 return 而不是常规 Task。

您可以添加一个扩展方法重载,它采用 TaskTask<T> 到您希望可链接的任何方法。

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

所以你可以调用 await FirstStepAsync().SecondStepAsync()

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

我已经为您编写了示例实现。异步工作将在调用任何 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);
    }
}

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

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());

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

将您的流畅方法重写为这种形式 - 即,将它们作为扩展方法毫无意义:

        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() 重载,这适用于 async/sync 方法的任何排列。

如果例如Transform2 接受参数,你需要扩展为 lambda:

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

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

async/sync 的组合应尽可能使用 ValueTask 进行优化。 reader的一个练习! :-)

另一种选择是实现基本的 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();
}