Шаблон делегування асинхронної поведінки в 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.


Я не впевнений, що це ти намагаєшся робити і чому.
Nkosi

Я намагаюся делегувати питання впровадження зовнішньому спостерігачеві (подібний до поліморфізму та прагнення до складу над спадщиною). Головним чином, щоб уникнути проблемного ланцюжка успадкування (і насправді неможливо, оскільки це вимагало б багаторазового успадкування).
Джефф

Чи пов’язані проблеми будь-яким чином і чи будуть вони оброблятися послідовно або паралельно?
Nkosi

Вони, схоже, діляться доступом до ProcessingArgsмене, тому я з цим розгубився.
Nkosi

1
Саме в цьому і полягає питання. Події не можуть повернути завдання. І навіть якщо я використовую делегата, який повертає завдання T, результат втрачається
Jeff

Відповіді:


2

Наступний делегат буде використаний для вирішення проблем асинхронної реалізації

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

З коментарів було вказано

Один конкретний приклад - додавання декількох кроків / завдань, необхідних для завершення "транзакції" (функціональність LOB)

Наступний клас дозволяє створити делегата, щоб обробляти такі кроки безперебійним чином, подібним до програмного забезпечення .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;
    }
}

Наступне розширення дозволяє спростити налаштування в режимі рядка за допомогою обгортки

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

Красивий код.
Джефф

Чи не хочете ви чекати наступного, тоді очікуйте кроку? Я думаю, це залежить, якщо "Додати" означає, що ви додаєте код для виконання перед будь-яким іншим кодом, який було додано. До речі, це більше схоже на "вставку"
Джефф

1
Кроки @Jeff за замовчуванням виконуються в тому порядку, коли вони були додані до конвеєра. Налаштування вбудованого рядка за замовчуванням дозволяє вам змінити це вручну, якщо хочете, якщо на шляху резервного потоку будуть виконані публікації
Nkosi

Як би ви створили / змінили це, якби я хотів використати Завдання T як результат, а не просто встановити контекст. Результат? Ви просто оновите підписи і додасте метод 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;
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.