Pattern per delegare il comportamento asincrono in C #


9

Sto cercando di progettare una classe che esponga la possibilità di aggiungere problemi di elaborazione asincrona. Nella programmazione sincrona, potrebbe apparire come questo

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

in un mondo asincrono, dove ogni preoccupazione potrebbe dover restituire un'attività, questo non è così semplice. Ho visto questo fatto in molti modi, ma sono curioso di sapere se ci sono buone pratiche che le persone hanno trovato. Una semplice possibilità è

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

C'è qualche "standard" che le persone hanno adottato per questo? Non sembra esserci un approccio coerente che ho osservato nelle API popolari.


Non sono sicuro di cosa stai cercando di fare e perché.
Nkosi,

Sto cercando di delegare le preoccupazioni di implementazione a un osservatore esterno (simile al polimorfismo e al desiderio di composizione sull'eredità). Principalmente per evitare una catena ereditaria problematica (ed effettivamente impossibile perché richiederebbe eredità multipla).
Jeff, il

Le preoccupazioni sono correlate in qualche modo e verranno elaborate in sequenza o in parallelo?
Nkosi,

Sembrano condividere l'accesso al, ProcessingArgsquindi ero confuso al riguardo.
Nkosi,

1
Questo è esattamente il punto della domanda. Gli eventi non possono restituire un'attività. E anche se uso un delegato che restituisce un'attività di T, il risultato andrà perso
Jeff

Risposte:


2

Il seguente delegato verrà utilizzato per gestire problemi di implementazione asincrona

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

Dai commenti è stato indicato

Un esempio specifico è l'aggiunta di più passaggi / attività necessari per completare una "transazione" (funzionalità LOB)

La seguente classe consente alla creazione di un delegato di gestire tali passaggi in modo fluente simile al middleware core .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;
    }
}

La seguente estensione consente una configurazione in linea più semplice utilizzando i wrapper

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

Può essere ulteriormente esteso in base alle necessità per involucri aggiuntivi.

Un esempio di utilizzo del delegato in azione è dimostrato nel seguente test

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

Bellissimo codice.
Jeff,

Non vorresti aspettare il prossimo, quindi attendere il passo? Immagino che dipenda se Aggiungi implica che aggiungi codice da eseguire prima di qualsiasi altro codice che è stato aggiunto. Il modo in cui è è più come un "inserto"
Jeff

1
I passaggi di @Jeff vengono eseguiti per impostazione predefinita nell'ordine in cui sono stati aggiunti alla pipeline. La configurazione in linea predefinita ti consente di cambiarla manualmente se lo desideri nel caso in cui ci siano azioni post da eseguire durante il backup del flusso
Nkosi

Come progettereste / modifichereste questo se volessi usare Task di T come risultato invece di impostare semplicemente il contesto. Vuoi semplicemente aggiornare le firme e aggiungere un metodo Inserisci (anziché solo Aggiungi) in modo che un middleware possa comunicare il suo risultato a un altro middleware?
Jeff, il

1

Se vuoi mantenerlo come delegato, puoi:

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;
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.