Padrão para delegar comportamento assíncrono em C #


9

Estou tentando criar uma classe que expõe a capacidade de adicionar preocupações de processamento assíncrono. Na programação síncrona, isso pode parecer

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

em um mundo assíncrono, onde cada preocupação pode precisar retornar uma tarefa, isso não é tão simples. Eu já vi isso de várias maneiras, mas estou curioso para saber se existem práticas recomendadas que as pessoas encontraram. Uma possibilidade simples é

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

Existe algum "padrão" que as pessoas adotaram para isso? Não parece haver uma abordagem consistente que observei nas APIs populares.


Não tenho certeza do que você está tentando fazer e por quê.
Nkosi

Estou tentando delegar preocupações de implementação a um observador externo (semelhante ao polimorfismo e um desejo de composição sobre a herança). Principalmente para evitar uma cadeia de herança problemática (e realmente impossível porque exigiria herança múltipla).
Jeff

As preocupações estão relacionadas de alguma forma e serão processadas em sequência ou em paralelo?
Nkosi

Eles parecem compartilhar o acesso ao ProcessingArgsque eu estava confuso sobre isso.
Nkosi

11
Esse é precisamente o ponto da questão. Eventos não podem retornar uma tarefa. E mesmo se eu usar um delegado que retorne uma tarefa de T, o resultado será perdido
Jeff

Respostas:


2

O delegado a seguir será usado para lidar com problemas de implementação assíncrona

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

Dos comentários foi indicado

Um exemplo específico é adicionar várias etapas / tarefas necessárias para concluir uma "transação" (funcionalidade LOB)

A classe a seguir permite a criação de um delegado para lidar com essas etapas de maneira fluente, semelhante ao middleware .net core

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

A extensão a seguir permite uma configuração mais simples em linha usando 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);
        });
    }
}

Pode ser estendido ainda mais, conforme necessário, para invólucros adicionais.

Um exemplo de caso de uso do delegado em ação é demonstrado no seguinte teste

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

Código bonito.
30519 Jeff Jeff

Você não gostaria de aguardar o próximo passo e aguardar o passo? Eu acho que depende se Add implica que você adicione código para executar antes de qualquer outro código que foi adicionado. Do jeito que é, é mais como uma "inserção"
Jeff

11
As etapas @Jeff são executadas por padrão na ordem em que foram adicionadas ao pipeline. A configuração embutida padrão permite que você altere isso manualmente, se desejar, caso haja ações pós-executadas no caminho de backup
Nkosi

Como você projetaria / alteraria isso se eu quisesse usar a Tarefa de T como resultado, em vez de apenas definir o contexto. Você apenas atualiza as assinaturas e adiciona um método Insert (em vez de apenas Add) para que um middleware possa comunicar seu resultado a outro middleware?
Jeff

1

Se você deseja mantê-lo como delegados, pode:

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;
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.