2

The following design emerged from using Azure Durable Functions with lots of fan out, which turned out to not work very well. Our starting point was an Orchestrator with a companion Durable Entity for state keeping, and Activity functions for the actual work.

We ran into several problems, like timeouts and too many ongoing instances.

Finally, we discovered that these three artifacts could be merged into one Durable Entity which calls itself:

  • The entity keeps the state, the Context
  • Operations are modeled as states as in the State Design Pattern
  • Each state takes a Context, do something useful, then return the next version of the Context.

This turned out to work very well, but wasn't maintainable because every state need to know its successor. Which means for every new state added, we had to identify its predecessor and change it to point to the new state as its successor.

In the end, we don't really care about predecessors and successors, only that functions can be called one by one in correct order.

I don't know what to call the elements, so Context and Pipeline will do for now. First version:

// Convert 212 Fahrenheit to Celsius

var initial = new Context(212, 0); 
var pipeline = new Pipeline(
  x => x with { x.Value - 32},
  x => x with { x.Value * 5},
  x => x with { x.Value / 9}
);

var a = pipeline.Run(initial); // returns Context(180, 1)
var b = pipeline.Run(a); // returns Context(900, 2)
var c = pipeline.Run(b); // returns Context(100, 3)

These are the basic requirements, but we need more:

  • Each step may run more than once, like when we read data from a paged api
  • All steps may run more than once, like all dates for a given period
  • Functions must be async for IO

Two new functions are prepended: One to decide if the whole pipeline should be run again, the other to decide if the current function should be run again:

var pipeline = new Pipeline(
  x => x.Position < 3,
  x => x.ContinuationToken != null,
  x => x with { x.Value - 32},
  x => x with { x.Value * 5},
  x => x with { x.Value / 9}
);

All functions must be async, so we can:

var next = await pipeline.Run(previous);

Finally, the pipeline should be generic and accept yet another function to advance the context to point to the next function.

var pipeline = new Pipeline<Context>(
  x => x.Position < 3,
  x => x.ContinuationToken != null,
  x => x with { Position = x.Position + 1},
  ...
);
6
  • Is this an "interpreter"?
    – Alexander
    Commented Nov 17, 2022 at 0:18
  • An interpreter would take some kind of a language and translate to executable operations, right? There is no language here, no need for it either, except for C# in this case. Commented Nov 17, 2022 at 11:00
  • To the downvoter: Can you inform of the issue, please, so I can improve the question? Commented Nov 17, 2022 at 11:01
  • Looks like "Chain-of-responsability" to me. Commented Nov 17, 2022 at 13:33
  • You could do some research into Actor Models. Durable Entities is loosely based on that I believe, but there are other implementations/frameworks that might better fit your needs.
    – Rik D
    Commented Nov 17, 2022 at 16:22

3 Answers 3

6

Not every problem has a name or design pattern. Rather than hunting for a solution, clearly define the problem and requirements that a solution must implement. Judging by your question, I believe you have done this. The next thing is simply choosing a good name so you can get on with writing code to solve the more interesting problems.

Choosing a good name is subjective. Based on the problem and requirements, select a word or phrase that describes what this object needs to do.

To be honest, "pipeline" works just fine. Things go in one end and come out the other. Name this thing so that future programmers or Future Thomas Eyde understand the concept, and don't worry so much about following some pre-described pattern.

1
  • Thank you, I guess Pipeline it is. Important to me is to have descriptive a name so not only future me knows what I talk about, but also the team. Commented Nov 17, 2022 at 10:58
0

Consider the command pattern. It crams everything needed to call into a class and hides it behind one uniform interface.

With the details of how to call abstracted away you have a clean place to manage when to call and how to document it.

3
  • Yes, the functions could be modelled as commands, but the command pattern doesn't say anything about order, which is important. Commented Nov 17, 2022 at 10:57
  • @ThomasEyde Looks like "Chain-of-responsability" to me, where each handler has a reference to its successor and the "context" if you will is passed in a message called request();. The difference is that in your example the order is managed through a queue (pipeline) instead of each handler calling its successor. Commented Nov 17, 2022 at 13:53
  • Yes, I thought about chain of responsibility too, but isn’t that pattern about passing something through a list of handlers until one accepts? Commented Nov 17, 2022 at 15:00
0

I believe the Template Method Pattern could be useful in your case.

1
  • This looks like a "link-only" question, can you please elaborate?
    – Dominique
    Commented Dec 12, 2022 at 8:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.