Modèle de délégation du comportement asynchrone en C #

9

J'essaie de concevoir une classe qui expose la possibilité d'ajouter des problèmes de traitement asynchrone. En programmation synchrone, cela pourrait ressembler à

   public class ProcessingArgs : EventArgs
   {
      public int Result { get; set; }
   } 

   public class Processor 
   {
        public event EventHandler<ProcessingArgs> Processing { get; }

        public int Process()
        {
            var args = new ProcessingArgs();
            Processing?.Invoke(args);
            return args.Result;
        }
   }


   var processor = new Processor();
   processor.Processing += args => args.Result = 10;
   processor.Processing += args => args.Result+=1;
   var result = processor.Process();

dans un monde asynchrone, où chaque préoccupation peut avoir besoin de retourner une tâche, ce n'est pas si simple. J'ai vu cela de nombreuses façons, mais je suis curieux de savoir s'il existe des meilleures pratiques que les gens ont trouvées. Une possibilité simple est

 public class Processor 
   {
        public IList<Func<ProcessingArgs, Task>> Processing { get; } =new List<Func<ProcessingArgs, Task>>();

        public async Task<int> ProcessAsync()
        {
            var args = new ProcessingArgs();
            foreach(var func in Processing) 
            {
                await func(args);
            }
            return args.Result
        }
   }

Y a-t-il une "norme" que les gens ont adoptée pour cela? Il ne semble pas y avoir d'approche cohérente que j'ai observée dans les API populaires.

Jeff
la source
Je ne sais pas trop ce que vous essayez de faire et pourquoi.
Nkosi
J'essaie de déléguer les préoccupations d'implémentation à un observateur externe (similaire au polymorphisme et à un désir de composition plutôt qu'à l'héritage). Principalement pour éviter une chaîne d'héritage problématique (et en fait impossible car cela nécessiterait un héritage multiple).
Jeff
Les préoccupations sont-elles liées de quelque façon et seront-elles traitées en séquence ou en parallèle?
Nkosi
Ils semblent partager l'accès à la ProcessingArgsdonc j'étais confus à ce sujet.
Nkosi
1
C'est précisément le point de la question. Les événements ne peuvent pas renvoyer une tâche. Et même si j'utilise un délégué qui renvoie une tâche de T, le résultat sera perdu
Jeff

Réponses:

2

Le délégué suivant sera utilisé pour gérer les problèmes d'implémentation asynchrone

public delegate Task PipelineStep<TContext>(TContext context);

D'après les commentaires, il a été indiqué

Un exemple spécifique est l'ajout de plusieurs étapes / tâches requises pour effectuer une "transaction" (fonctionnalité LOB)

La classe suivante permet la création d'un délégué pour gérer ces étapes d'une manière fluide similaire au middleware de base .net

public class PipelineBuilder<TContext> {
    private readonly Stack<Func<PipelineStep<TContext>, PipelineStep<TContext>>> steps =
        new Stack<Func<PipelineStep<TContext>, PipelineStep<TContext>>>();

    public PipelineBuilder<TContext> AddStep(Func<PipelineStep<TContext>, PipelineStep<TContext>> step) {
        steps.Push(step);
        return this;
    }

    public PipelineStep<TContext> Build() {
        var next = new PipelineStep<TContext>(context => Task.CompletedTask);
        while (steps.Any()) {
            var step = steps.Pop();
            next = step(next);
        }
        return next;
    }
}

L'extension suivante permet une configuration en ligne plus simple à l'aide de wrappers

public static class PipelineBuilderAddStepExtensions {

    public static PipelineBuilder<TContext> AddStep<TContext>
        (this PipelineBuilder<TContext> builder,
        Func<TContext, PipelineStep<TContext>, Task> middleware) {
        return builder.AddStep(next => {
            return context => {
                return middleware(context, next);
            };
        });
    }

    public static PipelineBuilder<TContext> AddStep<TContext>
        (this PipelineBuilder<TContext> builder, Func<TContext, Task> step) {
        return builder.AddStep(async (context, next) => {
            await step(context);
            await next(context);
        });
    }

    public static PipelineBuilder<TContext> AddStep<TContext>
        (this PipelineBuilder<TContext> builder, Action<TContext> step) {
        return builder.AddStep((context, next) => {
            step(context);
            return next(context);
        });
    }
}

Il peut être étendu au besoin pour des enveloppes supplémentaires.

Un exemple de cas d'utilisation du délégué en action est illustré dans le test suivant

[TestClass]
public class ProcessBuilderTests {
    [TestMethod]
    public async Task Should_Process_Steps_In_Sequence() {
        //Arrange
        var expected = 11;
        var builder = new ProcessBuilder()
            .AddStep(context => context.Result = 10)
            .AddStep(async (context, next) => {
                //do something before

                //pass context down stream
                await next(context);

                //do something after;
            })
            .AddStep(context => { context.Result += 1; return Task.CompletedTask; });

        var process = builder.Build();

        var args = new ProcessingArgs();

        //Act
        await process.Invoke(args);

        //Assert
        args.Result.Should().Be(expected);
    }

    public class ProcessBuilder : PipelineBuilder<ProcessingArgs> {

    }

    public class ProcessingArgs : EventArgs {
        public int Result { get; set; }
    }
}
Nkosi
la source
Beau code.
Jeff
Souhaitez-vous pas attendre ensuite puis attendre l'étape? Je suppose que cela dépend si Add implique que vous ajoutez du code à exécuter avant tout autre code qui a été ajouté. La façon dont c'est est plus comme un "insert"
Jeff
1
Les étapes @Jeff sont par défaut exécutées dans l'ordre où elles ont été ajoutées au pipeline. La configuration en ligne par défaut vous permet de changer cela manuellement si vous le souhaitez au cas où il y aurait des actions de publication à effectuer sur le chemin de la remontée
Nkosi
Comment pourriez-vous concevoir / modifier cela si je voulais utiliser Task of T en conséquence au lieu de simplement définir le contexte. Souhaitez-vous simplement mettre à jour les signatures et ajouter une méthode d'insertion (au lieu de simplement ajouter) afin qu'un middleware puisse communiquer son résultat à un autre middleware?
Jeff
1

Si vous souhaitez le conserver en tant que délégué, vous pouvez:

public class Processor
{
    public event Func<ProcessingArgs, Task> Processing;

    public async Task<int?> ProcessAsync()
    {
        if (Processing?.GetInvocationList() is Delegate[] processors)
        {
            var args = new ProcessingArgs();
            foreach (Func<ProcessingArgs, Task> processor in processors)
            {
                await processor(args);
            }
            return args.Result;
        }
        else return null;
    }
}
Paulo Morgado
la source