Releases: danielgerlag/workflow-core
v1.7
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 ofWorkflowCore.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
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
v1.6.6
Workflow Core 1.6.6
- Added optional
Reference
parameter to StartWorkflow methods
v1.6.0
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
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 theStep
class and the value would be an expression with both thedata
andcontext
parameters at your disposal. - The
Outputs
collection, the key would match a property on theData
class and the value would be an expression with both thestep
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
Workflow Core 1.3.3
- Added cancel condition parameter to
WaitFor
method on the step builder
v1.3.2
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
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
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);
}
}