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.