How to implement a simple workflow pipeline fluent api method chaining?

1k views Asked by At

I would like to find a good design pattern on how to implement this example business workflow. Instead of using one giant monolithic procedural-like method call, I was thinking I would like to use a fluent method chaining -- basically, a simple workflow pipeline without using one of those workflow or BPM frameworks. Suggestions on best practice, perhaps a known design pattern?

My Example

  1. get configuration / user preferences
  2. validate config/preferences
  3. look up / standardize additional config/preferences
  4. get report 1 (with above input)
  5. get report 2, etc.
  6. email reports

The inputs/user preferences causes a lot of if/else logic, so I don't want to have my method have to contain all my if/else logic to see if each step was successful or not, and handle. (i.e. I do NOT want)

   myOutput1 = CallMethod1(param1, param2, our errorMsg)
   if (error)
   { // do something, then break }

   myOutput2 = CallMethod2(param1, param2, our errorMsg)
   if (error)
   { // do something, then break }

   ...

   myOutput9 = CallMethod9(param1, param2, our errorMsg)
   if (error)
   { // do something, then break }

Sample Idea Pipeline code Perhaps something like this? Would it work? How can I improve upon it?

public class Reporter
{
   private ReportSettings Settings {get; set;}
   private ReportResponse Response {get; set;}

   public ReportResponse GenerateAndSendReports(string groupName)
   {
       ReportResponse response = this.GetInputConfiguration()
                                 .ValidateConfiguration()
                                 .StandardizeConfiguration(groupName)
                                 .PopulateReport1()
                                 .PopulateReport2()
                                 .PopulateReport99()
                                 .EmailReports()
                                 .Output();


      return response;
    }

   public Reporter GetInputConfiguration()
   {
        this.Response = new ReportResponse();
        this.Settings = new ReportSetting();

        this.Settings.IsReport1Enabled = ConfigurationManager.GetSetting("EnableReport1");
        this.Settings.Blah1 = ConfigurationManager.GetSetting("Blah1");
        this.Settings.Blah2 = ConfigurationManager.GetSetting("Blah2");
         return this;
}

   public Reporter StandardizeConfiguration(string groupName)
   {
        this.Settings.Emails = myDataService.GetEmails(groupName);
        return this;
   }

public Reporter PopulateReport1()
{
    if (!this.Setting.HasError && this.Settings.IsReport1Enabled)
    {
        try
        {
            this.Response.Report1Content = myReportService.GetReport1(this.Settings.Blah1, this.Blah2)
        }
        catch (Exception ex)
        {
            this.Response.HasError = true;
            this.Response.Message = ex.ToString();
        }
    }
    return this;
}
}

I was thinking of something like this

2

There are 2 answers

0
Jason On

I know this is an old question, but this is a pretty recent video explaining how to build a nice Fluent API. One thing mentioned, that I think is great, is the idea of using interfaces to enforce the correct order to call the APIs.

https://www.youtube.com/watch?v=1JAdZul-aRQ

0
Ama On

You are mentioning two distinct concepts: fluent mechanism, and the pipeline (or chain of responsibility) pattern.

Pipeline Pattern

  • Must define an interface IPipeline which contains DoThings();.
  • The implementations of IPipeline must contain an IPipeline GetNext();

Fluent

  • All actions must return a reference to the object modified by the action: IFluent.
  • If you which to better control what options are available and when in your workflow, you could have the Fluent actions returning distinct interfaces: for example IValidatedData could expose IStandardizedData Standardize(), and IStandardizedData could expose IStandardizedData PopulateReport(var param) and IStandardizedData PopulateEmail(var param). This is what LINQ does with enumerables, lists, etc.

However, in your example it looks like you are mostly looking for a Fluent mechanism. The pipeline pattern helps with data flows (HTTP request handlers, for example). In your case you are just applying properties to a single object Reporter, so the pipeline pattern does not really apply.

For those who ended here because they are looking for a two-way (push and pull) fluent pipeline, you want your fluent actions to build the pipeline, by returning an IPipelineStep. The behaviour of the pipeline is defined by the implementation of each IPipelineStep.

You can achieve this as follows:

  • PipelineStep implements IPipelineStep
  • PipelineStep contains a private IPipelineStep NextStep(get;set);
  • IPipelineBuilder contains the fluent actions available to build the pipeline.
  • Your fluent actions return a concretion which implement both IPipelineStep and IPipelineBuilder.
  • Before returning, the fluent action updates this.NextStep.
  • IPipelineStep contains a var Push(var input); and var Pull(var input);
  • Push does things and then calls this.NextStep.Push
  • Pull calls this.NextStep.Pull and then does things and returns

You also need to consider how you want to use the pipeline once built: from top to bottom or the other way around.