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