Changelog History
Page 1
-
v3.3 Changes
November 03, 2020Workflow Core 3.3.0
Workflow Middleware
๐ฒ Workflows can be extended with Middleware that run before/after workflows start/complete as well as around workflow steps to provide flexibility in implementing cross-cutting concerns such as log correlation, retries, and other use-cases.
This is done by implementing and registering
IWorkflowMiddleware
for workflows orIWorkflowStepMiddleware
for steps.Step Middleware
๐ Step middleware lets you run additional code around the execution of a given step and alter its behavior. Implementing a step middleware should look familiar to anyone familiar with ASP.NET Core's middleware pipeline or
HttpClient
'sDelegatingHandler
middleware.Usage
๐ฒ First, create your own middleware class that implements
IWorkflowStepMiddleware
. Here's an example of a middleware that adds workflow ID and step ID to the log correlation context of every workflow step in your app.Important: You must make sure to call
next()
as part of your middleware. If you do not do this, your step will never run.public class LogCorrelationStepMiddleware : IWorkflowStepMiddleware{ private readonly ILogger\<LogCorrelationStepMiddleware\> \_log; public LogCorrelationStepMiddleware( ILogger\<LogCorrelationStepMiddleware\> log) { \_log = log; } public async Task\<ExecutionResult\> HandleAsync( IStepExecutionContext context, IStepBody body, WorkflowStepDelegate next) { var workflowId = context.Workflow.Id; var stepId = context.Step.Id; // Uses log scope to add a few attributes to the scopeusing (\_log.BeginScope("{@WorkflowId}", workflowId)) using (\_log.BeginScope("{@StepId}", stepId)) { // Calling next ensures step gets executedreturn await next(); } } }
Here's another example of a middleware that uses the Polly dotnet resiliency library to implement retries on workflow steps based off a custom retry policy.
public class PollyRetryStepMiddleware : IWorkflowStepMiddleware{ private const string StepContextKey = "WorkflowStepContext"; private const int MaxRetries = 3; private readonly ILogger\<PollyRetryStepMiddleware\> \_log; public PollyRetryMiddleware(ILogger\<PollyRetryStepMiddleware\> log) { \_log = log; } // Consult Polly's docs for more information on how to build// retry policies:// https://github.com/App-vNext/Pollypublic IAsyncPolicy\<ExecutionResult\> GetRetryPolicy() =\>Policy\<ExecutionResult\> .Handle\<TimeoutException\>() .RetryAsync( MaxRetries, (result, retryCount, context) =\>UpdateRetryCount( result.Exception, retryCount, context[StepContextKey] as IStepExecutionContext) ); public async Task\<ExecutionResult\> HandleAsync( IStepExecutionContext context, IStepBody body, WorkflowStepDelegate next ) { return await GetRetryPolicy().ExecuteAsync( ctx =\> next(), // The step execution context gets passed down so that// the step is accessible within the retry policynew Dictionary\<string, object\> { { StepContextKey, context } }); } private Task UpdateRetryCount( Exception exception, int retryCount, IStepExecutionContext stepContext) { var stepInstance = stepContext.ExecutionPointer; stepInstance.RetryCount = retryCount; return Task.CompletedTask; } }
Pre/Post Workflow Middleware
Workflow middleware run either before a workflow starts or after a workflow completes and can be used to hook into the workflow lifecycle or alter the workflow itself before it is started.
Pre Workflow Middleware
These middleware get run before the workflow is started and can potentially alter properties on the
WorkflowInstance
.The following example illustrates setting the
Description
property on theWorkflowInstance
using a middleware that interprets the data on the passed workflow. This is useful in cases where you want the description of the workflow to be derived from the data passed to the workflow.Note that you use
WorkflowMiddlewarePhase.PreWorkflow
to specify that it runs before the workflow starts.Important: You should call
next
as part of the workflow middleware to ensure that the next workflow in the chain runs.// AddDescriptionWorkflowMiddleware.cspublic class AddDescriptionWorkflowMiddleware : IWorkflowMiddleware{ public WorkflowMiddlewarePhase Phase =\>WorkflowMiddlewarePhase.PreWorkflow; public Task HandleAsync( WorkflowInstance workflow, WorkflowDelegate next ) { if (workflow.Data is IDescriptiveWorkflowParams descriptiveParams) { workflow.Description = descriptiveParams.Description; } return next(); } }// IDescriptiveWorkflowParams.cspublic interface IDescriptiveWorkflowParams{ string Description { get; } }// MyWorkflowParams.cspublic MyWorkflowParams : IDescriptiveWorkflowParams{ public string Description =\> $"Run task '{TaskName}'"; public string TaskName { get; set; } }
๐ป Exception Handling in Pre Workflow Middleware
๐ป Pre workflow middleware exception handling gets treated differently from post workflow middleware. Since the middleware runs before the workflow starts, any exceptions thrown within a pre workflow middleware will bubble up to the
StartWorkflow
method and it is up to the caller ofStartWorkflow
to handle the exception and act accordingly.public async Task MyMethodThatStartsAWorkflow() { try { await host.StartWorkflow("HelloWorld", 1, null); } catch(Exception ex) { // Handle the exception appropriately } }
Post Workflow Middleware
These middleware get run after the workflow has completed and can be used to perform additional actions for all workflows in your app.
๐จ The following example illustrates how you can use a post workflow middleware to print a summary of the workflow to console.
Note that you use
WorkflowMiddlewarePhase.PostWorkflow
to specify that it runs after the workflow completes.Important: You should call
next
as part of the workflow middleware to ensure that the next workflow in the chain runs.public class PrintWorkflowSummaryMiddleware : IWorkflowMiddleware{ private readonly ILogger\<PrintWorkflowSummaryMiddleware\> \_log; public PrintWorkflowSummaryMiddleware( ILogger\<PrintWorkflowSummaryMiddleware\> log ) { \_log = log; } public WorkflowMiddlewarePhase Phase =\>WorkflowMiddlewarePhase.PostWorkflow; public Task HandleAsync( WorkflowInstance workflow, WorkflowDelegate next ) { if (!workflow.CompleteTime.HasValue) { return next(); } var duration = workflow.CompleteTime.Value - workflow.CreateTime; \_log.LogInformation([email protected]"Workflow {workflow.Description} completed in {duration:g}"); foreach (var step in workflow.ExecutionPointers) { var stepName = step.StepName; var stepDuration = (step.EndTime - step.StartTime) ?? TimeSpan.Zero; \_log.LogInformation($" - Step {stepName} completed in {stepDuration:g}"); } return next(); } }
๐ป Exception Handling in Post Workflow Middleware
๐ป Post workflow middleware exception handling gets treated differently from pre workflow middleware. At the time that the workflow completes, your workflow has ran already so an uncaught exception would be difficult to act on.
0๏ธโฃ By default, if a workflow middleware throws an exception, it will be logged and the workflow will complete as normal. This behavior can be changed, however.
0๏ธโฃ To override the default post workflow error handling for all workflows in your app, just register a new
IWorkflowMiddlewareErrorHandler
in the dependency injection framework with your custom behavior as follows.// CustomMiddlewareErrorHandler.cspublic class CustomHandler : IWorkflowMiddlewareErrorHandler{ public Task HandleAsync(Exception ex) { // Handle your error asynchronously } }// Startup.cspublic void ConfigureServices(IServiceCollection services) { // Other workflow configurationservices.AddWorkflow(); // Should go after .AddWorkflow()services.AddTransient\<IWorkflowMiddlewareErrorHandler, CustomHandler\>(); }
Registering Middleware
In order for middleware to take effect, they must be registered with the built-in dependency injection framework using the convenience helpers.
Note: Middleware will be run in the order that they are registered with middleware that are registered earlier running earlier in the chain and finishing later in the chain. For pre/post workflow middleware, all pre middleware will be run before a workflow starts and all post middleware will be run after a workflow completes.
public class Startup{ public void ConfigureServices(IServiceCollection services) { ... // Add workflow middlewareservices.AddWorkflowMiddleware\<AddDescriptionWorkflowMiddleware\>(); services.AddWorkflowMiddleware\<PrintWorkflowSummaryMiddleware\>(); // Add step middlewareservices.AddWorkflowStepMiddleware\<LogCorrelationStepMiddleware\>(); services.AddWorkflowStepMiddleware\<PollyRetryMiddleware\>(); ... } }
More Information
๐ See the Workflow Middleware sample for full examples of workflow middleware in action.
Many thanks to Danil Flores @dflor003 for this contribution!
-
v3.2 Changes
August 08, 2020 -
v3.1.0 Changes
January 05, 2020Workflow Core 3.1
Decision Branching
You can define multiple independent branches within your workflow and select one based on an expression value.
๐ For the fluent API, we define our branches with the
CreateBranch()
method on the workflow builder. We can then select a branch using theDecision
step.This workflow will select
branch1
if the value ofdata.Value1
isone
, andbranch2
if it istwo
.var branch1 = builder.CreateBranch() .StartWith\<PrintMessage\>() .Input(step =\> step.Message, data =\> "hi from 1") .Then\<PrintMessage\>() .Input(step =\> step.Message, data =\> "bye from 1");var branch2 = builder.CreateBranch() .StartWith\<PrintMessage\>() .Input(step =\> step.Message, data =\> "hi from 2") .Then\<PrintMessage\>() .Input(step =\> step.Message, data =\> "bye from 2");builder .StartWith\<HelloWorld\>() .Decide(data =\> data.Value1) .Branch("one", branch1) .Branch("two", branch2);
The JSON representation would look something like this.
{ "Id": "DecisionWorkflow", "Version": 1, "DataType": "MyApp.MyData, MyApp", "Steps": [{ "Id": "decide", "StepType": "WorkflowCore.Primitives.Decide, WorkflowCore", "Inputs": { "Expression": "data.Value1" }, "OutcomeSteps": { "Print1": "\"one\"", "Print2": "\"two\"" } }, { "Id": "Print1", "StepType": "MyApp.PrintMessage, MyApp", "Inputs": { "Message": "\"Hello from 1\"" } }, { "Id": "Print2", "StepType": "MyApp.PrintMessage, MyApp", "Inputs": { "Message": "\"Hello from 2\"" } }] }
Outcomes for JSON workflows
You can now specify
OutcomeSteps
for a step in JSON and YAML workflow definitions."OutcomeSteps": { "<<Step1 Id>>": "<<expression>>", "<<Step2 Id>>": "<<expression>>" }
โฑ If the outcome of a step matches a particular expression, that step would be scheduled as the next step to execute.
-
v3.0.0 Changes
December 22, 2019Workflow Core 3.0.0
๐ Support for PostgeSQL is delayed because of this issue with upstream libraries
๐ฆ Split DSL into own package
๐ฆ The JSON and YAML definition features into their own package.
Migration required for existing projects:
- ๐ฆ Install the
WorkflowCore.DSL
package from nuget. - Call
AddWorkflowDSL()
on your service collection.
Activities
An activity is defined as an item on an external queue of work, that a workflow can wait for.
In this example the workflow will wait for
activity-1
, before proceeding. It also passes the value ofdata.Value1
to the activity, it then maps the result of the activity todata.Value2
.๐ท Then we create a worker to process the queue of activity items. It uses the
GetPendingActivity
method to get an activity and the data that a workflow is waiting for.public class ActivityWorkflow : IWorkflow\<MyData\> { public void Build(IWorkflowBuilder\<MyData\> builder) { builder .StartWith\<HelloWorld\>() .Activity("activity-1", (data) =\> data.Value1) .Output(data =\> data.Value2, step =\> step.Result) .Then\<PrintMessage\>() .Input(step =\> step.Message, data =\> data.Value2); } } ...var activity = host.GetPendingActivity("activity-1", "worker1", TimeSpan.FromMinutes(1)).Result;if (activity != null) { Console.WriteLine(activity.Parameters); host.SubmitActivitySuccess(activity.Token, "Some response data"); }
The JSON representation of this step would look like this
{ "Id": "activity-step", "StepType": "WorkflowCore.Primitives.Activity, WorkflowCore", "Inputs": { "ActivityName": "\"activity-1\"", "Parameters": "data.Value1" }, "Outputs": { "Value2": "step.Result" } }
- ๐ฆ Install the
-
v2.1.2 Changes
October 06, 2019Workflow Core 2.1.2
- โ Adds a feature to purge old workflows from the persistence store.
๐ New
IWorkflowPurger
service that can be injected from the IoC containerTask PurgeWorkflows(WorkflowStatus status, DateTime olderThan)
Implementations are currently only for SQL Server, Postgres and MongoDB
-
v2.1.0 Changes
September 15, 2019Workflow Core 2.1.0
- โ Adds the
SyncWorkflowRunner
service that enables workflows to be executed synchronously, you can also avoid persisting the state to the persistence store entirely
usage
var runner = serviceProvider.GetService\<ISyncWorkflowRunner\>(); ...var worfklow = await runner.RunWorkflowSync("my-workflow", 1, data, TimeSpan.FromSeconds(10));
- โ Adds the
-
v2.0.0 Changes
June 30, 2019Workflow Core 2.0.0
โฌ๏ธ Upgrade notes
Existing JSON definitions will be loaded as follows
using WorkflowCore.Services.DefinitionStorage; ...DefinitionLoader.LoadDefinition(json, Deserializers.Json);
Targets .NET Standard 2.0
The core library now targets .NET Standard 2.0, in order to leverage newer features.
๐ Support for YAML definitions
โ Added support for YAML workflow definitions, which can be loaded as follows
using WorkflowCore.Services.DefinitionStorage; ...DefinitionLoader.LoadDefinition(json, Deserializers.Yaml);
Existing JSON definitions will be loaded as follows
using WorkflowCore.Services.DefinitionStorage; ...DefinitionLoader.LoadDefinition(json, Deserializers.Json);
Object graphs and inline expressions on input properties
You can now pass object graphs to step inputs as opposed to just scalar values
"inputs": { "Body": { "Value1": 1, "Value2": 2 }, "Headers": { "Content-Type": "application/json" } },
If you want to evaluate an expression for a given property of your object, simply prepend and
@
and pass an expression string"inputs": { "Body": { "@Value1": "data.MyValue * 2", "Value2": 5 }, "Headers": { "Content-Type": "application/json" } },
๐ Support for enum values on input properties
If your step has an enum property, you can now just pass the string representation of the enum value and it will be automatically converted.
Environment variables available in input expressions
You can now access environment variables from within input expressions.
usage:environment["VARIABLE_NAME"]
-
v1.9.3 Changes
April 07, 2019Workflow Core 1.9.3
- ๐ Fixes the order of processing for multiple events with same name/key
- โ Adds
UseMaxConcurrentWorkflows
to WorkflowOptions to allow overriding the max number of concurrent workflows for a given node
-
v1.9.2 Changes
March 31, 2019Workflow Core 1.9.2
๐ Changes the default retry behavior for steps within a saga to bubble up to the saga container.
This means you do not have to explicitly set each step within the saga toCompensate
. -
v1.8.3 Changes
February 24, 2019โ Added
Attach
andId
to fluent API
This will enable one to attach the flow from a step to any other step with anId
Control structure scope will be preserved.StartWith<Step1>() .Id("step1") .Then<Step2>() .Attach("step1")
โ Added index queue ahead of upcoming feature for async indexing
๐ Various performance improvements