Releases: danielgerlag/workflow-core
v1.8.1
Workflow Core 1.8.1
Thank you to @MarioAndron
This release adds a feature where a DI scope is created around the construction of steps that are registered with your IoC container.
This enables steps to consume services registered as scoped.
v1.8
Workflow Core 1.8
Elasticsearch plugin for Workflow Core
A search index plugin for Workflow Core backed by Elasticsearch, enabling you to index your workflows and search against the data and state of them.
Configuration
Use the .UseElasticsearch extension method on IServiceCollection when building your service provider
using Nest;
...
services.AddWorkflow(cfg =>
{
...
cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://localhost:9200")), "index_name");
});Usage
Inject the ISearchIndex service into your code and use the Search method.
Search(string terms, int skip, int take, params SearchFilter[] filters)
terms
A whitespace separated string of search terms, an empty string will match everything.
This will do a full text search on the following default fields
- Reference
- Description
- Status
- Workflow Definition
In addition you can search data within your own custom data object if it implements ISearchable
using WorkflowCore.Interfaces;
...
public class MyData : ISearchable
{
public string StrValue1 { get; set; }
public string StrValue2 { get; set; }
public IEnumerable<string> GetSearchTokens()
{
return new List<string>()
{
StrValue1,
StrValue2
};
}
}Examples
Search all fields for "puppies"
searchIndex.Search("puppies", 0, 10);skip & take
Use skip and take to page your search results. Where skip is the result number to start from and take is the page size.
filters
You can also supply a list of filters to apply to the search, these can be applied to both the standard fields as well as any field within your custom data objects.
There is no need to implement ISearchable on your data object in order to use filters against it.
The following filter types are available
- ScalarFilter
- DateRangeFilter
- NumericRangeFilter
- StatusFilter
These exist in the WorkflowCore.Models.Search namespace.
Examples
Filtering by reference
using WorkflowCore.Models.Search;
...
searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Reference, "My Reference"));Filtering by workflows started after a date
searchIndex.Search("", 0, 10, DateRangeFilter.After(x => x.CreateTime, startDate));Filtering by workflows completed within a period
searchIndex.Search("", 0, 10, DateRangeFilter.Between(x => x.CompleteTime, startDate, endDate));Filtering by workflows in a state
searchIndex.Search("", 0, 10, StatusFilter.Equals(WorkflowStatus.Complete));Filtering against your own custom data class
class MyData
{
public string Value1 { get; set; }
public int Value2 { get; set; }
}
searchIndex.Search("", 0, 10, ScalarFilter.Equals<MyData>(x => x.Value1, "blue moon"));
searchIndex.Search("", 0, 10, NumericRangeFilter.LessThan<MyData>(x => x.Value2, 5))Action Inputs / Outputs
Added the action Input & Output overloads on the fluent step builder.
Input(Action<TStepBody, TData> action);This will allow one to manipulate properties on the step before it executes and properties on the data object after it executes, for example
Input((step, data) => step.Value1 = data.Value1).Output((step, data) => data["Value3"] = step.Output).Output((step, data) => data.MyCollection.Add(step.Output))Breaking changes
The existing ability to assign values to entries in dictionaries or dynamic objects on .Output was problematic,
since it broke the ability to pass collections on the Output mappings.
.Output(data => data["Value3"], step => step.Output)This feature has been removed, and it is advised to use the action Output API instead, for example
.Output((step, data) => data["Value3"] = step.Output)This functionality remains intact for JSON defined workflows.
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
CancelConditionto 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.RedLockin favour ofWorkflowCore.Providers.Redis -
Create a new
WorkflowCore.Providers.Redislibrary 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.RedisUsage
Use the
IServiceCollectionextension 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
Referenceparameter to StartWorkflow methods
v1.6.0
Workflow Core 1.6.0
- Added Saga transaction feature
- Added
.CompensateWithfeature
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
.Outputbuilder 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
Inputscollection, the key would match a property on theStepclass and the value would be an expression with both thedataandcontextparameters at your disposal. - The
Outputscollection, the key would match a property on theDataclass and the value would be an expression with both thestepas 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
WaitFormethod on the step builder
v1.3.2
Workflow Core 1.3.2
- Added
WorkflowControllerservice
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