Building a simple workflow system with ASP.NET Core

Have you ever had the requirement to implement some sort of business processes and ended up implementing them all over the place in your codebase with lots of if and else statements? Yes, then maybe this post might be interesting to you.



Some background

In my past I had the chance to work on a powerful workflow engine. From the customers point of view a workflow system can bring huge benefits when it comes to execute workflows and the user of the system can basically start a workflow instance, click through it and let itself guide by the system. All he or she needs to do is to fill out the presented forms and hit save which processes the current state of the workflow. The result could be that the next form gets presented or based on the workflow some emails will be sent or maybe a document needs to get printed and sent to a client.

Since then I had always been interested in workflow systems and studied the patterns and had a look at different workflow engine systems like Windows Workflow Foundations or BPMN Workflow Engine.

microwf - A simple finite state machine (FSM) with workflow character

Recently I came across the requirement to have such a workflow engine available for a project of mine. So I went out and looked again for a suitable workflow engine that fitted my needs.

Because my requirements weren’t that big and I wanted to avoid to have a huge installation and configuration ceremony I built a small library microwf which is available on nuget.

Processing a workflow with microwf

If you think of how to process a workflow you usually have some sort of a workflow definition IWorkflowDefinition. Within the definition we define all the required transitions that our workflow need to have.

The interface IWorkflowDefinition.

public interface IWorkflowDefinition
{
  /// <summary>
  /// Returns a unique name for the workflow definition.
  /// </summary>
  string Type { get; }

  /// <summary>
  /// Returns a list of states for the workflow.
  /// </summary>
  List<string> States { get; }

  /// <summary>
  /// Returns a list of triggers for the workflow.
  /// </summary>
  List<string> Triggers { get; }

  /// <summary>
  /// Returns a list of transitions for the workflow.
  /// </summary>
  List<Transition> Transitions { get; }
}

Each Transition can control if the transition can be done, see the CanMakeTransition-hook. If this results to true then the BeforeTransition and AfterTransition-hook will be executed if defined. Within the BeforeTransition-hook we are still able to abort the entire transition.

public class Transition
{
  public string State { get; set; }

  public string Trigger { get; set; }

  public string TargetState { get; set; }

  public Func<TransitionContext, bool> CanMakeTransition { get; set; }

  public Action<TransitionContext> BeforeTransition { get; set; }

  public Action<TransitionContext> AfterTransition { get; set; }

  public Transition()
  {
    CanMakeTransition = triggerContext => true;
  }
}

When it comes to the processing part you need an object instance IWorkflow that represents an instance of the workflow definition and possibly some context.

The interface IWorkflow.

public interface IWorkflow
{
  /// <summary>
  /// Defines a unique workflow type.
  /// </summary>
  string Type { get; }

  /// <summary>
  /// Defines the state of the workflow.
  /// </summary>
  string State { get; set; }
}

The WorkflowExecution class accepts an IWorkflowDefinition and an IWorkflow instance. Based on this information it is executing the desired trigger action. The result is always an instance of TriggerResult where you get the information whether the trigger has been executed or aborted.

A sample is always better

Let’s have a look at a workflow definition for a simple “Holiday Approval”.

In code it looks like the following:

public class HolidayApprovalWorkflow : WorkflowDefinitionBase
{
  public const string TYPE = "HolidayApprovalWorkflow";

  public override string Type
  {
    get { return TYPE; }
  }

  public override List<Transition> Transitions
  {
    get
    {
      return new List<Transition>
      {
        new Transition {
          State = "New",
          Trigger = "Apply",
          TargetState ="Applied",
          CanMakeTransition = MeApplyingForHolidays
        },
        new Transition {
          State = "Applied",
          Trigger = "Approve",
          TargetState ="Approved",
          CanMakeTransition = BossIsApproving,
          AfterTransition = ThankBossForApproving
        },
        new Transition {
          State = "Applied",
          Trigger = "Reject",
          TargetState ="Rejected"
        }
      };
    }
  }

  private bool MeApplyingForHolidays(TransitionContext context)
  {
    var holiday = context.GetInstance<Holiday>();

    return holiday.Me == "Me";
  }

  private bool BossIsApproving(TransitionContext context)
  {
    var holiday = context.GetInstance<Holiday>();

    return holiday.Boss == "NiceBoss";
  }

  private void ThankBossForApproving(TransitionContext context)
  {
    // SendMail("Thank you!!!");
  }
}

public class Holiday : IWorkflow
{
  // IWorkflow properties
  public string Type { get; set; }
  public string State { get; set; }

  // some other properties
  public string Me { get; set; }
  public string Boss { get; set; }

  public Holiday()
  {
    this.State = "New";
  }
}

Given the code above we could apply for holiday like that:

var holiday = new Holiday();
var triggerParam = new TriggerParam("Apply", holiday);
var workflowDefinition = new HolidayApprovalWorkflow();
var workflowExecution = new WorkflowExecution(workflowDefinition);

TriggerResult result = workflowExecution.Trigger(triggerParam);

If everything went fine the result would be that the state of the IWorkflow instance “holiday” would be “Applied”.

Now you could argue that why not just setting the state-property on the “holiday” instance directly? Why so much ceremony?

My answer to that question would be:

  1. Instances of Holiday objects will be treated always the same because the HolidayApprovalWorkflow definition exactly defines the transition behavior for each transition.
  2. Imagine a bit more complex workflow and you will be thankful having the transition logic exactly in one place and not distributed within your codebase.
  3. And last but not least imagine the situation where you need to version a workflow!

Summary

This was the first introductory part of microwf. I have planned to create one or two other posts that describe the usage of this tiny library within a web api project and an angular frontend.

My ultimate goal would be to have at least the following features:

  • super simple user administration with permissions
  • assignable user and system workflows

If you go to the github repo you will find a sample ASP.NET Core WebApi application where I started my work.

Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use. To find out more, see here.