Шаблон для делегирования асинхронного поведения в C #

9

Я пытаюсь создать класс, который предоставляет возможность добавлять проблемы асинхронной обработки. В синхронном программировании это может выглядеть так

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

в асинхронном мире, где каждой проблеме может потребоваться вернуть задачу, это не так просто. Я видел, как это делается многими способами, но мне любопытно, есть ли какие-либо лучшие практики, которые люди нашли. Одна простая возможность

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

Есть ли какой-то «стандарт», который люди приняли для этого? Похоже, что нет единого подхода, который я наблюдал в популярных API.

Джефф
источник
Я не уверен, что вы пытаетесь сделать и почему.
Нкоси
Я пытаюсь передать вопросы реализации внешнему наблюдателю (аналогично полиморфизму и стремлению к композиции, а не наследованию). Главным образом, чтобы избежать проблемной цепочки наследования (и фактически невозможно, потому что это потребовало бы множественного наследования).
Джефф
Связаны ли проблемы каким-либо образом и будут ли они обрабатываться последовательно или параллельно?
Нкоси
Похоже, они делятся доступом, ProcessingArgsпоэтому я запутался в этом.
Нкоси
1
В этом и заключается суть вопроса. События не могут вернуть задачу. И даже если я использую делегата, который возвращает задание T, результат будет потерян
Джефф

Ответы:

2

Следующий делегат будет использоваться для обработки асинхронных проблем реализации

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

Из комментариев было указано

Одним конкретным примером является добавление нескольких шагов / задач, необходимых для завершения «транзакции» (функциональность большого объекта)

Следующий класс позволяет создать делегат для быстрой обработки таких шагов, аналогично промежуточному программному обеспечению ядра .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;
    }
}

Следующее расширение позволяет упростить встроенную настройку с использованием оболочек

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

Это может быть расширено далее по мере необходимости для дополнительных оберток.

Пример использования делегата в действии демонстрируется в следующем тесте

[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
источник
Прекрасный код.
Джефф
Разве вы не хотите ждать следующего, затем ждать шага? Я предполагаю, что это зависит от того, подразумевает ли Add, что вы добавляете код для выполнения перед любым другим кодом, который был добавлен. На самом деле это больше похоже на «вставку»
Джефф
1
Шаги @Jeff по умолчанию выполняются в порядке их добавления в конвейер. Встроенная настройка по умолчанию позволяет вам изменить это вручную, если вы хотите в случае, если есть какие-либо действия после записи, которые необходимо выполнить при обратном потоке
Nkosi
Как бы вы разработали / изменили это, если бы я хотел использовать Task of T в результате, а не просто устанавливать context.Result? Не могли бы вы просто обновить сигнатуры и добавить метод Insert (вместо простого Add), чтобы промежуточное ПО могло сообщать о своем результате другому промежуточному ПО?
Джефф
1

Если вы хотите сохранить его в качестве делегатов, вы можете:

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;
    }
}
Пауло Моргадо
источник