【问题标题】:Chaining task-based methods only if each task completes / validates successfully仅当每个任务成功完成/验证时才链接基于任务的方法
【发布时间】:2021-07-21 22:30:58
【问题描述】:

我有一个任务返回方法链,都返回一些Task<SomeResponse<T>>SomeResponse<T> 是一个通用响应类,它公开属性,例如响应是否成功 (IsSuccess),如果成功则包含返回对象的 T Data 属性,如果不成功则附带错误消息。

假设我有 3 个这样的方法(它们都返回 SomeResponse<T>)。我只想继续一个接一个地执行任务,直到其中一个失败或全部成功。流程如下所示:

var first = await firtTask(someParam);
if (!first.IsSuccess) return first;
var second = await secondTask(first.Data);
if (!second.IsSuccess) return second;
var third = await thirdTask(second.Data);
return third; // doesn't matter if it succeeded or not as it's the last one, no need to check.

我这里的问题是,每次调用的SomeResponse<T> 都需要在进行下一个等待之前验证成功,这增加了很多重复的验证代码。检查每个任务是否成功完成是不够的,因为我必须在继续下一个任务之前检查它的 SomeResponse<T>.IsSuccess 属性。

我尝试为此在Task<SomeResponse<T>> 之上创建一个扩展方法:

public static Task<SomeResponse<T>> OnSuccessChainAsync<T>(this Task<SomeResponse<T>> startingTask, Func<T, Task<SomeResponse<T>>> continuationTask)
{
    // omitting null checks etc

    var continuation = startingTask.ContinueWith(
        async previousTask =>
        {
            var response = await previousTask.ConfigureAwait(false);
            if (!response.IsSuccess)
            {
                return response;
            }

            return await continuationTask(response.Data).ConfigureAwait(false);
        }, TaskScheduler.Current);

    return continuation.Unwrap();
}

现在我可以这样写了:

public override Task<SomeResponse<TValue>> AddAsync(TValue someValue)
{
    return firstTask(someValue)
           .OnSuccessChainAsync(secondTask)
           .OnSuccessChainAsync(thirdTask);
}

我不确定我是否走错了方向。我将async-await 与TPL 的ContinueWith 混合在一起,除此之外,我还从我的分析仪中获得了VSTHRD003 Avoid awaiting foreign Tasks

【问题讨论】:

  • 在我看来你重新发明了异常,但更糟。在这里使用异常将完全符合您的要求:在它们发生时停止执行,并且不可能忘记检查是否发生过
  • @canton7 每个返回SomeResponse&lt;T&gt; 的基于任务的方法都在调用一个可能会在那里抛出异常的外部项目。 SomeResponse&lt;T&gt; 通知另一端发生了一些事情(通过IsSuccess)而没有抛出异常(这一端没有出错)。
  • 我知道事情是这样的 -- 我是说如果你改变 以便你抛出一个异常而不是使用Response&lt;T&gt;.IsSuccess ,您将不需要所有这些杂乱、容易出错的开销。例外很有用,有自己的位置,并且很好地集成到了语言中。
  • 嗯,在这里想了解一下:如果我调用了一个GetCustomerByName(canton7) API,而API 本身返回了一个404 等,我为什么要在我身边抛出一个Exception
  • 因为它提供了你想要的行为,即停止执行你当前正在执行的方法,并返回给调用者。它有一个很好的属性,你不能忘记检查 API 是否返回 404。

标签: c# async-await task-parallel-library


【解决方案1】:

不要将旧式 ContinueWithasync/await 混用。 事实上,尽量避免使用ContinueWith:它非常复杂,有很多微妙的行为,其中一半你不想去想,一半用async/await 表达得更清楚。

为什么不把事情简化一下:

public static async Task<SomeResponse<T>> ExecuteInSequence<T>(
    T firstData,
    params Func<T, Task<Response<T>>>[] funcs)
{
    T data = firstData;
    foreach (var func in funcs)
    {
        var response = await func(data);
        if (!response.IsSuccess)
        {
            return response;
        }

        data = response.Data;
    }

    return data;
}

然后你可以写:

ExecuteInSequence(someValue, task1, task2, task3);

没有任何混合,没有链接,只是一个简单的循环。


如果你打算把它写成Task&lt;SomeResponse&lt;T&gt;&gt; 的扩展方法,我还是把一切都保留为awaits:

public static async Task<SomeResponse<T>> OnSuccessChainAsync<T>(
    this Task<SomeResponse<T>> startingTask,
    Func<T, Task<SomeResponse<T>>> continuationTask)
{
    // startingTask will probably have already completed (especially if
    // it's one which we created on a previous invocation), in which case
    // this await will be synchronous.

    var result = await startingTask;
    if (!result.IsSuccess)
    {
        return result;
    }

    return await continuationTask(result.Data);
}

【讨论】:

  • 显然比我的简单得多,非常感谢!从可重用性的角度来看,这适合什么地方?它应该是一些帮助者,还是SomeResponse&lt;T&gt; 的一部分?
  • 我不会把它放在SomeResponse&lt;T&gt; 上,所以在某个地方找一些帮手。也许在一个专门的助手中,也许只是在使用它的(单个)类中。
  • @globetrotter 我还写了一个仅等待版本的延续扩展方法
  • 谢谢!仅等待版本返回的警告与我最初的警告相同:VSTHRD003 避免等待外部任务(不确定它是有效警告还是应该被禁止)
  • @globetrotter 我称之为误报并压制它 - 你知道任务来自哪里,只是分析器没有
猜你喜欢
  • 1970-01-01
  • 2020-04-14
  • 1970-01-01
  • 2012-07-23
  • 1970-01-01
  • 1970-01-01
  • 2018-08-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多