How to continue on first successful task, throw exception if all tasks fail

风格不统一 提交于 2021-02-08 05:22:45

问题


I am currently working on a web API that requires that I perform several different checks for rights and perform async operations to see if a user is allowed to make an API call. If the user can pass just one of the checks, they may continue, otherwise I need to throw an exception and boot them back out of the API with a 403 error.

I'd like to do something similar to this:

public async Task<object> APIMethod()
{
    var tasks = new[] { CheckOne(), CheckTwo(), CheckThree() };

    // On first success, ignore the result of the rest of the tasks and continue
    // If none of them succeed, throw exception; 

    CoreBusinessLogic();
}

// Checks two and three are the same
public async Task CheckOne()
{
    var result = await PerformSomeCheckAsync();
    if (result == CustomStatusCode.Fail)
    {
        throw new Exception();
    }
} 

回答1:


Use Task.WhenAny to keep track of tasks as they complete and perform the desired logic.

The following example demonstrates the explained logic

public class Program {
    public static async Task Main() {
        Console.WriteLine("Hello World");
        await new Program().APIMethod();
    }

    public async Task APIMethod() {
        var cts = new CancellationTokenSource();
        var tasks = new[] { CheckOne(cts.Token), CheckTwo(cts.Token), CheckThree(cts.Token) };
        var failCount = 0;
        var runningTasks = tasks.ToList();            
        while (runningTasks.Count > 0) {
            //As tasks complete
            var finishedTask = await Task.WhenAny(runningTasks);
            //remove completed task
            runningTasks.Remove(finishedTask);
            Console.WriteLine($"ID={finishedTask.Id}, Result={finishedTask.Result}");
            //process task (in this case to check result)
            var result = await finishedTask;
            //perform desired logic
            if (result == CustomStatusCode.Success) { //On first Success                    
                cts.Cancel(); //ignore the result of the rest of the tasks 
                break; //and continue
            }
            failCount++;
        }

        // If none of them succeed, throw exception; 
        if (failCount == tasks.Length)
            throw new InvalidOperationException();

        //Core Business logic....
        foreach (var t in runningTasks) {
            Console.WriteLine($"ID={t.Id}, Result={t.Result}");
        }
    }

    public async Task<CustomStatusCode> CheckOne(CancellationToken cancellationToken) {
        await Task.Delay(1000); // mimic doing work
        if (cancellationToken.IsCancellationRequested)
            return CustomStatusCode.Canceled;
        return CustomStatusCode.Success;
    }

    public async Task<CustomStatusCode> CheckTwo(CancellationToken cancellationToken) {
        await Task.Delay(500); // mimic doing work
        if (cancellationToken.IsCancellationRequested)
            return CustomStatusCode.Canceled;
        return CustomStatusCode.Fail;
    }

    public async Task<CustomStatusCode> CheckThree(CancellationToken cancellationToken) {
        await Task.Delay(1500); // mimic doing work
        if (cancellationToken.IsCancellationRequested)
            return CustomStatusCode.Canceled;
        return CustomStatusCode.Fail;
    }
}

public enum CustomStatusCode {
    Fail,
    Success,
    Canceled
}

The above example produces the following output

Hello World
ID=1, Result=Fail
ID=2, Result=Success
ID=3, Result=Canceled

observe in the example how a cancellation token was used to help cancel the remaining tasks that have not completed as yet when the first successful task completed. This can help improve performance if the called tasks are designed correctly.

If in your example PerformSomeCheckAsync allows for cancellation, then it should be taken advantage of since, once a successful condition is found the remaining task are no longer needed, then leaving them running is not very efficient, depending on their load.

The provided example above can be aggregated into a reusable extension method

public static class WhenAnyExtension {
    /// <summary>
    /// Continues on first successful task, throw exception if all tasks fail
    /// </summary>
    /// <typeparam name="TResult">The type of task result</typeparam>
    /// <param name="tasks">An IEnumerable<T> to return an element from.</param>
    /// <param name="predicate">A function to test each element for a condition.</param>
    /// <param name="cancellationToken"></param>
    /// <returns>The first result in the sequence that passes the test in the specified predicate function.</returns>
    public static async Task<TResult> WhenFirst<TResult>(this IEnumerable<Task<TResult>> tasks, Func<TResult, bool> predicate, CancellationToken cancellationToken = default(CancellationToken)) {
        var running = tasks.ToList();
        var taskCount = running.Count;
        var failCount = 0;
        var result = default(TResult);
        while (running.Count > 0) {
            if (cancellationToken.IsCancellationRequested) {
                result = await Task.FromCanceled<TResult>(cancellationToken);
                break;
            }
            var finished = await Task.WhenAny(running);
            running.Remove(finished);
            result = await finished;
            if (predicate(result)) {
                break;
            }
            failCount++;
        }
        // If none of them succeed, throw exception; 
        if (failCount == taskCount)
            throw new InvalidOperationException("No task result satisfies the condition in predicate");

        return result;
    }
}

Simplifying the original example to

public static async Task Main()
{
    Console.WriteLine("Hello World");
    await new Program().APIMethod();
}

public async Task APIMethod()
{
    var cts = new CancellationTokenSource();
    var tasks = new[]{CheckThree(cts.Token), CheckTwo(cts.Token), CheckOne(cts.Token), CheckTwo(cts.Token), CheckThree(cts.Token)};
    //continue on first successful task, throw exception if all tasks fail
    await tasks.WhenFirst(result => result == CustomStatusCode.Success);
    cts.Cancel(); //cancel remaining tasks if any
    foreach (var t in tasks)
    {
        Console.WriteLine($"Id = {t.Id}, Result = {t.Result}");
    }
}

which produces the following result

Hello World
Id = 1, Result = Canceled
Id = 2, Result = Fail
Id = 3, Result = Success
Id = 4, Result = Fail
Id = 5, Result = Canceled

based on the Check* functions



来源:https://stackoverflow.com/questions/60347327/how-to-continue-on-first-successful-task-throw-exception-if-all-tasks-fail

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!