Skip to content

Releases: danielgerlag/workflow-core

v1.7

13 Jan 16:30
b09e07e
Compare
Choose a tag to compare

Workflow Core 1.7.0

  • Various performance optimizations, any users of the EntityFramework persistence providers will have to update their persistence libraries to the latest version as well.

  • Added CancelCondition to fluent builder API.

    .CancelCondition(data => <<expression>>, <<Continue after cancellation>>)
    
    

    This allows you to specify a condition under which any active step can be prematurely cancelled.
    For example, suppose you create a future scheduled task, but you want to cancel the future execution of this task if some condition becomes true.

    builder
        .StartWith(context => Console.WriteLine("Hello"))
        .Schedule(data => TimeSpan.FromSeconds(5)).Do(schedule => schedule
            .StartWith<DoSomething>()
            .Then<DoSomethingFurther>()
        )
        .CancelCondition(data => !data.SheduledTaskRequired)
        .Then(context => Console.WriteLine("Doing normal tasks"));

    You could also use this implement a parallel flow where once a single path completes, all the other paths are cancelled.

    .Parallel()
        .Do(then => then
            .StartWith<DoSomething>()
            .WaitFor("Approval", (data, context) => context.Workflow.IdNow)
        )
        .Do(then => then
            .StartWith<DoSomething>()
            .Delay(data => TimeSpan.FromDays(3))
            .Then<EscalateIssue>()
        )
    .Join()
        .CancelCondition(data => data.IsApproved, true)
    .Then<MoveAlong>();
  • Deprecated WorkflowCore.LockProviders.RedLock in favour of WorkflowCore.Providers.Redis

  • Create a new WorkflowCore.Providers.Redis library that includes providers for distributed locking, queues and event hubs.

    • Provides Queueing support backed by Redis.
    • Provides Distributed locking support backed by Redis.
    • Provides event hub support backed by Redis.

    This makes it possible to have a cluster of nodes processing your workflows.

    Installing

    Install the NuGet package "WorkflowCore.Providers.Redis"

    Using Nuget package console
    PM> Install-Package WorkflowCore.Providers.Redis
    Using .NET CLI
    dotnet add package WorkflowCore.Providers.Redis

    Usage

    Use the IServiceCollection extension methods when building your service provider

    • .UseRedisQueues
    • .UseRedisLocking
    • .UseRedisEventHub
    services.AddWorkflow(cfg =>
    {
        cfg.UseRedisLocking("localhost:6379");
        cfg.UseRedisQueues("localhost:6379", "my-app");
        cfg.UseRedisEventHub("localhost:6379", "my-channel")
    });

v1.6.9

30 Dec 18:57
156d5bf
Compare
Choose a tag to compare

Workflow Core 1.6.9

This release adds functionality to subscribe to workflow life cycle events (WorkflowStarted, WorkflowComplete, WorkflowError, WorkflowSuspended, WorkflowResumed, StepStarted, StepCompleted, etc...)
This can be achieved by either grabbing the ILifeCycleEventHub implementation from the IoC container and subscribing to events there, or attach an event on the workflow host class IWorkflowHost.OnLifeCycleEvent.
This implementation only publishes events to the local node... we will still need to implement a distributed version of the EventHub to solve the problem for multi-node clusters.

v1.6.8

21 Oct 22:19
4378cca
Compare
Choose a tag to compare

Workflow Core 1.6.8

  • Fixed the order in which multiple compensating steps execute within a saga transaction. Issue 191

v1.6.6

22 Jul 14:29
Compare
Choose a tag to compare

Workflow Core 1.6.6

  • Added optional Reference parameter to StartWorkflow methods

v1.6.0

24 Dec 01:59
eb00c95
Compare
Choose a tag to compare

Workflow Core 1.6.0

  • Added Saga transaction feature
  • Added .CompensateWith feature

Specifying compensation steps for each component of a saga transaction

In this sample, if Task2 throws an exception, then UndoTask2 and UndoTask1 will be triggered.

builder
    .StartWith<SayHello>()
        .CompensateWith<UndoHello>()
    .Saga(saga => saga
        .StartWith<DoTask1>()
            .CompensateWith<UndoTask1>()
        .Then<DoTask2>()
            .CompensateWith<UndoTask2>()
        .Then<DoTask3>()
            .CompensateWith<UndoTask3>()
    )
    .Then<SayGoodbye>();

Retrying a failed transaction

This particular example will retry the entire saga every 5 seconds

builder
    .StartWith<SayHello>()
        .CompensateWith<UndoHello>()
    .Saga(saga => saga
        .StartWith<DoTask1>()
	    .CompensateWith<UndoTask1>()
	.Then<DoTask2>()
	    .CompensateWith<UndoTask2>()
	.Then<DoTask3>()
	    .CompensateWith<UndoTask3>()
	)		
	.OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5))
	.Then<SayGoodbye>();

Compensating the entire transaction

You could also only specify a master compensation step, as follows

builder
	.StartWith<SayHello>()
		.CompensateWith<UndoHello>()
	.Saga(saga => saga
		.StartWith<DoTask1>()
		.Then<DoTask2>()
		.Then<DoTask3>()
	)		
        .CompensateWithSequence(comp => comp
            .StartWith<UndoTask1>()
            .Then<UndoTask2>()
	    .Then<UndoTask3>()
        )
	.Then<SayGoodbye>();

Passing parameters

Parameters can be passed to a compensation step as follows

builder
    .StartWith<SayHello>()
    .CompensateWith<PrintMessage>(compensate => 
    {
        compensate.Input(step => step.Message, data => "undoing...");
    })

Expressing a saga in JSON

A saga transaction can be expressed in JSON, by using the WorkflowCore.Primitives.Sequence step and setting the Saga parameter to true.

The compensation steps can be defined by specifying the CompensateWith parameter.

{
  "Id": "Saga-Sample",
  "Version": 1,
  "DataType": "MyApp.MyDataClass, MyApp",
  "Steps": [
    {
      "Id": "Hello",
      "StepType": "MyApp.HelloWorld, MyApp",
      "NextStepId": "MySaga"
    },    
    {
      "Id": "MySaga",
      "StepType": "WorkflowCore.Primitives.Sequence, WorkflowCore",
      "NextStepId": "Bye",
      "Saga": true,
      "Do": [
        [
          {
            "Id": "do1",
            "StepType": "MyApp.Task1, MyApp",
            "NextStepId": "do2",
            "CompensateWith": [
              {
                "Id": "undo1",
                "StepType": "MyApp.UndoTask1, MyApp"
              }
            ]
          },
          {
            "Id": "do2",
            "StepType": "MyApp.Task2, MyApp",
            "CompensateWith": [
              {
                "Id": "undo2-1",
                "NextStepId": "undo2-2",
                "StepType": "MyApp.UndoTask2, MyApp"
              },
              {
                "Id": "undo2-2",
                "StepType": "MyApp.DoSomethingElse, MyApp"
              }
            ]
          }
        ]
      ]
    },    
    {
      "Id": "Bye",
      "StepType": "MyApp.GoodbyeWorld, MyApp"
    }
  ]
}

v1.4.0

03 Dec 18:01
Compare
Choose a tag to compare

Workflow Core 1.4.0

  • Changed MongoDB persistence provider to store custom workflow data as BsonDocument instead of serialized JSON string
  • Changed .Output builder method value expression type, so inferred generic type is not taken from value expression
  • Added a feature that enables the loading of workflow definitions from JSON files

Loading workflow definitions from JSON

Simply grab the DefinitionLoader from the IoC container and call the .LoadDefinition method

var loader = serviceProvider.GetService<IDefinitionLoader>();
loader.LoadDefinition(...);

Format of the JSON definition

Basics

The JSON format defines the steps within the workflow by referencing the fully qualified class names.

Field Description
Id Workflow Definition ID
Version Workflow Definition Version
DataType Fully qualified assembly class name of the custom data object
Steps[].Id Step ID (required unique key for each step)
Steps[].StepStepType Fully qualified assembly class name of the step
Steps[].NextStepId Step ID of the next step after this one completes
Steps[].Inputs Optional Key/value pair of step inputs
Steps[].Outputs Optional Key/value pair of step outputs
Steps[].CancelCondition Optional cancel condition
{
  "Id": "HelloWorld",
  "Version": 1,
  "Steps": [
    {
      "Id": "Hello",
      "StepType": "MyApp.HelloWorld, MyApp",
      "NextStepId": "Bye"
    },        
    {
      "Id": "Bye",
      "StepType": "MyApp.GoodbyeWorld, MyApp"
    }
  ]
}

Inputs and Outputs

Inputs and outputs can be bound to a step as a key/value pair object,

  • The Inputs collection, the key would match a property on the Step class and the value would be an expression with both the data and context parameters at your disposal.
  • The Outputs collection, the key would match a property on the Data class and the value would be an expression with both the step as a parameter at your disposal.

Full details of the capabilities of expression language can be found here

{
  "Id": "AddWorkflow",
  "Version": 1,
  "DataType": "MyApp.MyDataClass, MyApp",
  "Steps": [
    {
      "Id": "Hello",
      "StepType": "MyApp.HelloWorld, MyApp",
      "NextStepId": "Add"
    },
	{
      "Id": "Add",
      "StepType": "MyApp.AddNumbers, MyApp",
      "NextStepId": "Bye",
      "Inputs": { 
          "Value1": "data.Value1",
          "Value2": "data.Value2" 
       },
      "Outputs": { 
          "Answer": "step.Result" 
      }
    },    
    {
      "Id": "Bye",
      "StepType": "MyApp.GoodbyeWorld, MyApp"
    }
  ]
}
{
  "Id": "AddWorkflow",
  "Version": 1,
  "DataType": "MyApp.MyDataClass, MyApp",
  "Steps": [
    {
      "Id": "Hello",
      "StepType": "MyApp.HelloWorld, MyApp",
      "NextStepId": "Print"
    },
    {
      "Id": "Print",
      "StepType": "MyApp.PrintMessage, MyApp",
      "Inputs": { "Message": "\"Hi there!\"" }
    }
  ]
}

WaitFor

The .WaitFor can be implemented using 3 inputs as follows

Field Description
CancelCondition Optional expression to specify a cancel condition
Inputs.EventName Expression to specify the event name
Inputs.EventKey Expression to specify the event key
Inputs.EffectiveDate Optional expression to specify the effective date
{
    "Id": "MyWaitStep",
    "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore",
    "NextStepId": "...",
    "CancelCondition": "...",
    "Inputs": {
        "EventName": "\"Event1\"",
        "EventKey": "\"Key1\"",
        "EffectiveDate": "DateTime.Now"
    }
}

If

The .If can be implemented as follows

{
      "Id": "MyIfStep",
      "StepType": "WorkflowCore.Primitives.If, WorkflowCore",
      "NextStepId": "...",
      "Inputs": { "Condition": "<<expression to evaluate>>" },
      "Do": [[
          {
            "Id": "do1",
            "StepType": "MyApp.DoSomething1, MyApp",
            "NextStepId": "do2"
          },
          {
            "Id": "do2",
            "StepType": "MyApp.DoSomething2, MyApp"
          }
      ]]
}

While

The .While can be implemented as follows

{
      "Id": "MyWhileStep",
      "StepType": "WorkflowCore.Primitives.While, WorkflowCore",
      "NextStepId": "...",
      "Inputs": { "Condition": "<<expression to evaluate>>" },
      "Do": [[
          {
            "Id": "do1",
            "StepType": "MyApp.DoSomething1, MyApp",
            "NextStepId": "do2"
          },
          {
            "Id": "do2",
            "StepType": "MyApp.DoSomething2, MyApp"
          }
      ]]
}

ForEach

The .ForEach can be implemented as follows

{
      "Id": "MyForEachStep",
      "StepType": "WorkflowCore.Primitives.ForEach, WorkflowCore",
      "NextStepId": "...",
      "Inputs": { "Collection": "<<expression to evaluate>>" },
      "Do": [[
          {
            "Id": "do1",
            "StepType": "MyApp.DoSomething1, MyApp",
            "NextStepId": "do2"
          },
          {
            "Id": "do2",
            "StepType": "MyApp.DoSomething2, MyApp"
          }
      ]]
}

Delay

The .Delay can be implemented as follows

{
      "Id": "MyDelayStep",
      "StepType": "WorkflowCore.Primitives.Delay, WorkflowCore",
      "NextStepId": "...",
      "Inputs": { "Period": "<<expression to evaluate>>" }
}

Parallel

The .Parallel can be implemented as follows

{
      "Id": "MyParallelStep",
      "StepType": "WorkflowCore.Primitives.Sequence, WorkflowCore",
      "NextStepId": "...",
      "Do": [
		[ /* Branch 1 */
		  {
		    "Id": "Branch1.Step1",
		    "StepType": "MyApp.DoSomething1, MyApp",
		    "NextStepId": "Branch1.Step2"
		  },
		  {
		    "Id": "Branch1.Step2",
		    "StepType": "MyApp.DoSomething2, MyApp"
		  }
		],			
		[ /* Branch 2 */
		  {
		    "Id": "Branch2.Step1",
		    "StepType": "MyApp.DoSomething1, MyApp",
		    "NextStepId": "Branch2.Step2"
		  },
		  {
		    "Id": "Branch2.Step2",
		    "StepType": "MyApp.DoSomething2, MyApp"
		  }
		]
	  ]
}

Schedule

The .Schedule can be implemented as follows

{
      "Id": "MyScheduleStep",
      "StepType": "WorkflowCore.Primitives.Schedule, WorkflowCore",
      "Inputs": { "Interval": "<<expression to evaluate>>" },
      "Do": [[
          {
            "Id": "do1",
            "StepType": "MyApp.DoSomething1, MyApp",
            "NextStepId": "do2"
          },
          {
            "Id": "do2",
            "StepType": "MyApp.DoSomething2, MyApp"
          }
      ]]
}

Recur

The .Recur can be implemented as follows

{
      "Id": "MyScheduleStep",
      "StepType": "WorkflowCore.Primitives.Recur, WorkflowCore",
      "Inputs": { 
        "Interval": "<<expression to evaluate>>",
        "StopCondition": "<<expression to evaluate>>" 
      },
      "Do": [[
          {
            "Id": "do1",
            "StepType": "MyApp.DoSomething1, MyApp",
            "NextStepId": "do2"
          },
          {
            "Id": "do2",
            "StepType": "MyApp.DoSomething2, MyApp"
          }
      ]]
}

v1.3.3

21 Nov 04:14
Compare
Choose a tag to compare

Workflow Core 1.3.3

  • Added cancel condition parameter to WaitFor method on the step builder

v1.3.2

09 Sep 15:33
Compare
Choose a tag to compare

Workflow Core 1.3.2

  • Added WorkflowController service

Use the WorkflowController service to control workflows without having to run an exection node.

var controller = serviceProvider.GetService<IWorkflowController>();

Exposed methods

  • StartWorkflow
  • PublishEvent
  • RegisterWorkflow
  • SuspendWorkflow
  • ResumeWorkflow
  • TerminateWorkflow

v1.3.0

22 Jul 17:59
Compare
Choose a tag to compare

Workflow Core 1.3.0

  • Added support for async steps

Simply inherit from StepBodyAsync instead of StepBody

public class DoSomething : StepBodyAsync
{
    public override async Task<ExecutionResult> RunAsync(IStepExecutionContext context)
    {
        await Task.Delay(2000);
        return ExecutionResult.Next();
    }
}
  • Migrated from managing own thread pool to TPL datablocks for queue consumers

  • After executing a workflow, will determine if it is scheduled to run before the next poll, if so, will delay queue it

v1.2.9-r2

03 Jul 20:17
Compare
Choose a tag to compare

Test helpers for Workflow Core

Provides support writing tests for workflows built on WorkflowCore

Installing

Install the NuGet package "WorkflowCore.Testing"

PM> Install-Package WorkflowCore.Testing

Usage

With xUnit

  • Create a class that inherits from WorkflowTest
  • Call the Setup() method in the constructor
  • Implement your tests using the helper methods
    • StartWorkflow()
    • WaitForWorkflowToComplete()
    • WaitForEventSubscription()
    • GetStatus()
    • GetData()
    • UnhandledStepErrors
public class xUnitTest : WorkflowTest<MyWorkflow, MyDataClass>
{
    public xUnitTest()
    {
        Setup();
    }

    [Fact]
    public void MyWorkflow()
    {
        var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 });
        WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30));

        GetStatus(workflowId).Should().Be(WorkflowStatus.Complete);
        UnhandledStepErrors.Count.Should().Be(0);
        GetData(workflowId).Value3.Should().Be(5);
    }
}

With NUnit

  • Create a class that inherits from WorkflowTest and decorate it with the TestFixture attribute
  • Override the Setup method and decorate it with the SetUp attribute
  • Implement your tests using the helper methods
    • StartWorkflow()
    • WaitForWorkflowToComplete()
    • WaitForEventSubscription()
    • GetStatus()
    • GetData()
    • UnhandledStepErrors
[TestFixture]
public class NUnitTest : WorkflowTest<MyWorkflow, MyDataClass>
{
    [SetUp]
    protected override void Setup()
    {
        base.Setup();
    }

    [Test]
    public void NUnit_workflow_test_sample()
    {
        var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 });
        WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30));

        GetStatus(workflowId).Should().Be(WorkflowStatus.Complete);
        UnhandledStepErrors.Count.Should().Be(0);
        GetData(workflowId).Value3.Should().Be(5);
    }

}