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

구현 관찰을 외부 관찰자에게 위임하려고합니다 (다형성과 유사하고 상속에 대한 구성에 대한 요구). 주로 문제의 상속 체인을 피하기 위해 (그리고 실제로는 다중 상속이 필요하기 때문에 불가능합니다).
— Jeff

우려 사항이 어떤 식 으로든 관련되어 있으며 순차적으로 또는 동시에 처리됩니까?
— Nkosi

그들은 ProcessingArgs내가 그것에 대해 혼란 스러웠습니다.
— Nkosi

1
그것이 바로 질문의 요점입니다. 이벤트는 작업을 반환 할 수 없습니다. T의 Task를 반환하는 델리게이트를 사용하더라도 결과는 손실됩니다.
— Jeff

답변:


2

다음 위임은 비동기 구현 문제를 처리하는 데 사용됩니다.

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

의견에서 그것은 표시되었다

하나의 특정 예는 "트랜잭션"(LOB 기능)을 완료하는 데 필요한 여러 단계 / 태스크를 추가하는 것입니다.

다음 클래스는 .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; }
    }
}

아름다운 코드.
— Jeff

다음에 기다리고 다음 단계를 기다리고 싶습니까? 추가가 추가 된 다른 코드보다 먼저 실행할 코드를 추가하는지 여부에 따라 달라집니다. 그것이“삽입”과 비슷합니다
— Jeff

1
@Jeff 단계는 기본적으로 파이프 라인에 추가 된 순서대로 실행됩니다. 기본 인라인 설정을 사용하면 스트림을 백업하는 도중에 사후 조치가 필요한 경우 수동으로 변경할 수 있습니다
— Nkosi

context.Result를 설정하는 대신 T의 Task를 결과로 사용하려면 어떻게 디자인 / 변경합니까? 미들웨어가 결과를 다른 미들웨어와 통신 할 수 있도록 서명을 업데이트하고 Add 대신 Insert 메소드를 추가 하시겠습니까?
— Jeff

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;
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.