Je travaille actuellement sur une API Web qui nécessite d'effectuer plusieurs vérifications différentes des droits et d'effectuer des opérations asynchrones pour voir si un utilisateur est autorisé à effectuer un appel API. Si l'utilisateur ne peut réussir qu'une seule des vérifications, il peut continuer, sinon je dois lever une exception et le redémarrer hors de l'API avec une erreur 403.

J'aimerais faire quelque chose de similaire à ceci :

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();
    }
} 
2
Free Radical 22 févr. 2020 à 02:04

1 réponse

Meilleure réponse

Utilisez Task.WhenAny pour suivre les tâches à mesure qu'elles se terminent et exécutez la logique souhaitée.

L'exemple suivant illustre la logique expliquée

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
}

L'exemple ci-dessus produit la sortie suivante

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

Observez dans l'exemple comment un jeton d'annulation a été utilisé pour aider à annuler les tâches restantes qui ne sont pas encore terminées lorsque la première tâche réussie s'est terminée. Cela peut aider à améliorer les performances si les tâches appelées sont conçues correctement.

Si dans votre exemple PerformSomeCheckAsync permet l'annulation, alors il faut en profiter car, une fois qu'une condition réussie est trouvée, les tâches restantes ne sont plus nécessaires, alors les laisser s'exécuter n'est pas très efficace, en fonction de leur charge.

L'exemple fourni ci-dessus peut être agrégé dans une méthode d'extension réutilisable

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;
    }
}

Simplifier l'exemple original pour

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}");
    }
}

Qui produit le résultat suivant

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

Basé sur les fonctions Check*

2
Nkosi 22 févr. 2020 à 06:26