diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..a7923835b
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @danielgerlag @glucaci
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..21a4c65b6
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: [danielgerlag]
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..471baed24
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,20 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..bbcbbe7d6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..30d77c8db
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,15 @@
+
+**Describe the change**
+A clear and concise description of what the change is. Any PR submitted without a description of the change will not be reviewed.
+
+**Describe your implementation or design**
+How did you go about implementing the change?
+
+**Tests**
+Did you cover your changes with tests?
+
+**Breaking change**
+Do you changes break compatibility with previous versions?
+
+**Additional context**
+Any additional information you'd like to provide?
\ No newline at end of file
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 000000000..83e247208
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,162 @@
+name: .NET
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ Unit-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Unit Tests
+ run: dotnet test test/WorkflowCore.UnitTests --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ Integration-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Integration Tests
+ run: dotnet test test/WorkflowCore.IntegrationTests --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ MongoDB-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: MongoDB Tests
+ run: dotnet test test/WorkflowCore.Tests.MongoDB --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ MySQL-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: MySQL Tests
+ run: dotnet test test/WorkflowCore.Tests.MySQL --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ PostgreSQL-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: PostgreSQL Tests
+ run: dotnet test test/WorkflowCore.Tests.PostgreSQL --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ Redis-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Redis Tests
+ run: dotnet test test/WorkflowCore.Tests.Redis --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ SQLServer-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: SQL Server Tests
+ run: dotnet test test/WorkflowCore.Tests.SqlServer --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ Elasticsearch-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Elasticsearch Tests
+ run: dotnet test test/WorkflowCore.Tests.Elasticsearch --no-build --verbosity normal -p:ParallelizeTestCollections=false
+ Oracle-Tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Oracle Tests
+ run: dotnet test test/WorkflowCore.Tests.Oracle --no-build --verbosity normal -p:ParallelizeTestCollections=false
diff --git a/README.md b/README.md
index 7faad2599..ab398cc2c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Workflow Core
[](https://ci.appveyor.com/project/danielgerlag/workflow-core)
+[](https://api.gitsponsors.com/api/badge/link?p=xj6mObb7nZAJGyuABfd8nD5XWf3SE4oUfw0vmCgSiJeIfNlzJAej0FWX8oFdYm6D7bvZpCf6qANVBNPWid4dRQ==)
Workflow Core is a light weight embeddable workflow engine targeting .NET Standard. Think: long running processes with multiple tasks that need to track state. It supports pluggable persistence and concurrency providers to allow for multi-node clusters.
@@ -34,7 +35,7 @@ public class MyWorkflow : IWorkflow
## JSON / YAML Workflow Definitions
-Define your workflows in JSON or YAML
+Define your workflows in JSON or YAML, need to install WorkFlowCore.DSL
```json
{
@@ -65,7 +66,7 @@ Steps:
StepType: MyApp.GoodbyeWorld, MyApp
```
-### Sample use cases
+## Sample use cases
* New user workflow
```c#
@@ -132,12 +133,14 @@ There are several persistence providers available as separate Nuget packages.
* MemoryPersistenceProvider *(Default provider, for demo and testing purposes)*
* [MongoDB](src/providers/WorkflowCore.Persistence.MongoDB)
+* [Cosmos DB](src/providers/WorkflowCore.Providers.Azure)
* [Amazon DynamoDB](src/providers/WorkflowCore.Providers.AWS)
* [SQL Server](src/providers/WorkflowCore.Persistence.SqlServer)
* [PostgreSQL](src/providers/WorkflowCore.Persistence.PostgreSQL)
* [Sqlite](src/providers/WorkflowCore.Persistence.Sqlite)
* [MySQL](src/providers/WorkflowCore.Persistence.MySQL)
* [Redis](src/providers/WorkflowCore.Providers.Redis)
+* [Oracle](src/providers/WorkflowCore.Persistence.Oracle)
## Search
@@ -160,6 +163,8 @@ These are also available as separate Nuget packages.
* [Parallel ForEach](src/samples/WorkflowCore.Sample09)
+* [Sync ForEach](src/samples/WorkflowCore.Sample09s)
+
* [While Loop](src/samples/WorkflowCore.Sample10)
* [If Statement](src/samples/WorkflowCore.Sample11)
@@ -180,12 +185,12 @@ These are also available as separate Nuget packages.
* [Deferred execution & re-entrant steps](src/samples/WorkflowCore.Sample05)
+* [Human(User) Workflow](src/samples/WorkflowCore.Sample08)
+
* [Looping](src/samples/WorkflowCore.Sample02)
* [Exposing a REST API](src/samples/WebApiSample)
-* [Human(User) Workflow](src/samples/WorkflowCore.Sample08)
-
* [Testing](src/samples/WorkflowCore.TestSample01)
diff --git a/ReleaseNotes/3.3.0.md b/ReleaseNotes/3.3.0.md
new file mode 100644
index 000000000..da825c7ee
--- /dev/null
+++ b/ReleaseNotes/3.3.0.md
@@ -0,0 +1,284 @@
+# Workflow 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](https://www.frakkingsweet.com/net-core-log-correlation-easy-access-to-headers/), [retries](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly), and other use-cases.
+
+This is done by implementing and registering `IWorkflowMiddleware` for workflows or `IWorkflowStepMiddleware` 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](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) or [`HttpClient`'s `DelegatingHandler` middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#outgoing-request-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.
+
+```cs
+public class LogCorrelationStepMiddleware : IWorkflowStepMiddleware
+{
+ private readonly ILogger _log;
+
+ public LogCorrelationStepMiddleware(
+ ILogger log)
+ {
+ _log = log;
+ }
+
+ public async Task 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 scope
+ using (_log.BeginScope("{@WorkflowId}", workflowId))
+ using (_log.BeginScope("{@StepId}", stepId))
+ {
+ // Calling next ensures step gets executed
+ return await next();
+ }
+ }
+}
+```
+
+Here's another example of a middleware that uses the [Polly](https://github.com/App-vNext/Polly) dotnet resiliency library to implement retries on workflow steps based off a custom retry policy.
+
+```cs
+public class PollyRetryStepMiddleware : IWorkflowStepMiddleware
+{
+ private const string StepContextKey = "WorkflowStepContext";
+ private const int MaxRetries = 3;
+ private readonly ILogger _log;
+
+ public PollyRetryMiddleware(ILogger log)
+ {
+ _log = log;
+ }
+
+ // Consult Polly's docs for more information on how to build
+ // retry policies:
+ // https://github.com/App-vNext/Polly
+ public IAsyncPolicy GetRetryPolicy() =>
+ Policy
+ .Handle()
+ .RetryAsync(
+ MaxRetries,
+ (result, retryCount, context) =>
+ UpdateRetryCount(
+ result.Exception,
+ retryCount,
+ context[StepContextKey] as IStepExecutionContext)
+ );
+
+ public async Task 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 policy
+ new Dictionary
+ {
+ { 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 the `WorkflowInstance` 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.
+
+```cs
+// AddDescriptionWorkflowMiddleware.cs
+public 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.cs
+public interface IDescriptiveWorkflowParams
+{
+ string Description { get; }
+}
+
+// MyWorkflowParams.cs
+public 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 of `StartWorkflow` to handle the exception and act accordingly.
+
+```cs
+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.
+
+```cs
+public class PrintWorkflowSummaryMiddleware : IWorkflowMiddleware
+{
+ private readonly ILogger _log;
+
+ public PrintWorkflowSummaryMiddleware(
+ ILogger 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($@"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.
+
+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.
+
+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.
+
+```cs
+// CustomMiddlewareErrorHandler.cs
+public class CustomHandler : IWorkflowMiddlewareErrorHandler
+{
+ public Task HandleAsync(Exception ex)
+ {
+ // Handle your error asynchronously
+ }
+}
+
+// Startup.cs
+public void ConfigureServices(IServiceCollection services)
+{
+ // Other workflow configuration
+ services.AddWorkflow();
+
+ // Should go after .AddWorkflow()
+ services.AddTransient();
+}
+```
+
+## 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.
+
+```cs
+public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ ...
+
+ // Add workflow middleware
+ services.AddWorkflowMiddleware();
+ services.AddWorkflowMiddleware();
+
+ // Add step middleware
+ services.AddWorkflowStepMiddleware();
+ services.AddWorkflowStepMiddleware();
+
+ ...
+ }
+}
+```
+
+## More Information
+
+See the [Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) sample for full examples of workflow middleware in action.
+
+
+Many thanks to Danil Flores @dflor003 for this contribution!
\ No newline at end of file
diff --git a/ReleaseNotes/3.4.0.md b/ReleaseNotes/3.4.0.md
new file mode 100644
index 000000000..e3708be5d
--- /dev/null
+++ b/ReleaseNotes/3.4.0.md
@@ -0,0 +1,64 @@
+# Workflow Core 3.4.0
+
+## Execute Workflow Middleware
+
+These middleware get run after each workflow execution and can be used to perform additional actions or build metrics/statistics for all workflows in your app.
+
+The following example illustrates how you can use a execute workflow middleware to build [prometheus](https://prometheus.io/) metrics.
+
+Note that you use `WorkflowMiddlewarePhase.ExecuteWorkflow` to specify that it runs after each workflow execution.
+
+**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs.
+
+```cs
+public class MetricsMiddleware : IWorkflowMiddleware
+{
+ private readonly ConcurrentHashSet() _suspendedWorkflows =
+ new ConcurrentHashSet();
+
+ private readonly Counter _completed;
+ private readonly Counter _suspended;
+
+ public MetricsMiddleware()
+ {
+ _completed = Prometheus.Metrics.CreateCounter(
+ "workflow_completed", "Workflow completed");
+
+ _suspended = Prometheus.Metrics.CreateCounter(
+ "workflow_suspended", "Workflow suspended");
+ }
+
+ public WorkflowMiddlewarePhase Phase =>
+ WorkflowMiddlewarePhase.ExecuteWorkflow;
+
+ public Task HandleAsync(
+ WorkflowInstance workflow,
+ WorkflowDelegate next)
+ {
+ switch (workflow.Status)
+ {
+ case WorkflowStatus.Complete:
+ TryDecrementSuspended(workflow);
+ _completed.Inc();
+ break;
+ case WorkflowStatus.Suspended:
+ _suspendedWorkflows.Add(workflow.Id);
+ _suspended.Inc();
+ break;
+ default:
+ TryDecrementSuspended(workflow);
+ break;
+ }
+
+ return next();
+ }
+
+ private void TryDecrementSuspended(WorkflowInstance workflow)
+ {
+ if (_suspendedWorkflows.TryRemove(workflow.Id))
+ {
+ _suspended.Dec();
+ }
+ }
+}
+```
\ No newline at end of file
diff --git a/ReleaseNotes/3.6.0.md b/ReleaseNotes/3.6.0.md
new file mode 100644
index 000000000..697976a46
--- /dev/null
+++ b/ReleaseNotes/3.6.0.md
@@ -0,0 +1,8 @@
+# Workflow Core 3.6.0
+
+## Scheduled Commands
+
+Introduces the ability to schedule delayed commands to process a workflow or event, by persisting them to storage.
+This is the first step toward removing constant polling of the DB. It also filters out duplicate work items on the queue which is the current problem the greylist tries to solve.
+Initial implementation is supported by MongoDb, SQL Server, PostgeSQL, MySQL and SQLite.
+Additional support from the other persistence providers to follow.
diff --git a/WorkflowCore.sln b/WorkflowCore.sln
index 047ccf0c3..25c2016d2 100644
--- a/WorkflowCore.sln
+++ b/WorkflowCore.sln
@@ -1,13 +1,13 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29509.3
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33424.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF47161E-E399-451C-BDE8-E92AAD3BD761}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6AC9AEB-24EF-475A-B190-AA4D9E01270A}"
ProjectSection(SolutionItems) = preProject
- readme.md = readme.md
+ README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5080DB09-CBE8-4C45-9957-C3BB7651755E}"
@@ -103,11 +103,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReleaseNotes", "ReleaseNote
ReleaseNotes\2.1.2.md = ReleaseNotes\2.1.2.md
ReleaseNotes\3.0.0.md = ReleaseNotes\3.0.0.md
ReleaseNotes\3.1.0.md = ReleaseNotes\3.1.0.md
+ ReleaseNotes\3.3.0.md = ReleaseNotes\3.3.0.md
+ ReleaseNotes\3.4.0.md = ReleaseNotes\3.4.0.md
+ ReleaseNotes\3.6.0.md = ReleaseNotes\3.6.0.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample14", "src\samples\WorkflowCore.Sample14\WorkflowCore.Sample14.csproj", "{6BC66637-B42A-4334-ADFB-DBEC9F29D293}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Testing", "test\WorkflowCore.Testing\WorkflowCore.Testing.csproj", "{62A9709E-27DA-42EE-B94F-5AF431D86354}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Testing", "src\WorkflowCore.Testing\WorkflowCore.Testing.csproj", "{62A9709E-27DA-42EE-B94F-5AF431D86354}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.TestSample01", "src\samples\WorkflowCore.TestSample01\WorkflowCore.TestSample01.csproj", "{0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}"
EndProject
@@ -141,6 +144,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\Wor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample09s", "src\samples\WorkflowCore.Sample09s\WorkflowCore.Sample09s.csproj", "{E32CF21A-29CC-46D1-8044-FCC327F2B281}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "test\ScratchPad\ScratchPad.csproj", "{51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.QueueProviders.RabbitMQ", "test\WorkflowCore.Tests.QueueProviders.RabbitMQ\WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj", "{54DE20BA-EBA7-4BF0-9BD9-F03766849716}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample19", "src\samples\WorkflowCore.Sample19\WorkflowCore.Sample19.csproj", "{1223ED47-3E5E-4960-B70D-DFAF550F6666}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.RavenDB", "src\providers\WorkflowCore.Persistence.RavenDB\WorkflowCore.Persistence.RavenDB.csproj", "{AF205715-C8B7-42EF-BF14-AFC9E7F27242}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.Oracle", "src\providers\WorkflowCore.Persistence.Oracle\WorkflowCore.Persistence.Oracle.csproj", "{635629BC-9D5C-40C6-BBD0-060550ECE290}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Oracle", "test\WorkflowCore.Tests.Oracle\WorkflowCore.Tests.Oracle.csproj", "{A2837F1C-3740-4375-9069-81AE32C867CA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -343,6 +360,34 @@ Global
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.Build.0 = Release|Any CPU
+ {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -400,6 +445,13 @@ Global
{78217204-B873-40B9-8875-E3925B2FBCEC} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
{20B98905-08CB-4854-8E2C-A31A078383E9} = {EF47161E-E399-451C-BDE8-E92AAD3BD761}
{5BE6D628-B9DB-4C76-AAEB-8F3800509A84} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
+ {E32CF21A-29CC-46D1-8044-FCC327F2B281} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
+ {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
+ {54DE20BA-EBA7-4BF0-9BD9-F03766849716} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
+ {1223ED47-3E5E-4960-B70D-DFAF550F6666} = {5080DB09-CBE8-4C45-9957-C3BB7651755E}
+ {AF205715-C8B7-42EF-BF14-AFC9E7F27242} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2}
+ {635629BC-9D5C-40C6-BBD0-060550ECE290} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2}
+ {A2837F1C-3740-4375-9069-81AE32C867CA} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4}
diff --git a/docs/elastic-search.md b/docs/elastic-search.md
index ce187de80..e921f958d 100644
--- a/docs/elastic-search.md
+++ b/docs/elastic-search.md
@@ -21,7 +21,7 @@ dotnet add package WorkflowCore.Providers.Elasticsearch
Use the `.UseElasticsearch` extension method on `IServiceCollection` when building your service provider
-```C#
+```
using Nest;
...
services.AddWorkflow(cfg =>
@@ -50,7 +50,7 @@ This will do a full text search on the following default fields
In addition you can search data within your own custom data object if it implements `ISearchable`
- ```c#
+ ```
using WorkflowCore.Interfaces;
...
public class MyData : ISearchable
@@ -72,7 +72,7 @@ This will do a full text search on the following default fields
##### Examples
Search all fields for "puppies"
- ```c#
+ ```
searchIndex.Search("puppies", 0, 10);
```
@@ -96,7 +96,7 @@ The following filter types are available
##### Examples
Filtering by reference
- ```c#
+ ```
using WorkflowCore.Models.Search;
...
@@ -104,22 +104,22 @@ The following filter types are available
```
Filtering by workflows started after a date
- ```c#
+ ```
searchIndex.Search("", 0, 10, DateRangeFilter.After(x => x.CreateTime, startDate));
```
Filtering by workflows completed within a period
- ```c#
+ ```
searchIndex.Search("", 0, 10, DateRangeFilter.Between(x => x.CompleteTime, startDate, endDate));
```
Filtering by workflows in a state
- ```c#
+ ```
searchIndex.Search("", 0, 10, StatusFilter.Equals(WorkflowStatus.Complete));
```
Filtering against your own custom data class
- ```c#
+ ```
class MyData
{
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 48ee3b904..26bc01879 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -20,7 +20,7 @@ public class HelloWorld : StepBody
```
*The `StepBody` and `StepBodyAsync` class implementations are constructed by the workflow host which first tries to use IServiceProvider for dependency injection, if it can't construct it with this method, it will search for a parameterless constructor*
-### Then we define the workflow structure by composing a chain of steps. The is done by implementing the IWorkflow interface
+### Then we define the workflow structure by composing a chain of steps. This is done by implementing the IWorkflow interface
```C#
public class HelloWorldWorkflow : IWorkflow
@@ -142,7 +142,7 @@ public class MyDataClass
{
public int Value1 { get; set; }
public int Value2 { get; set; }
- public int Value3 { get; set; }
+ public int Answer { get; set; }
}
//Our workflow definition with strongly typed internal data and mapped inputs & outputs
@@ -154,9 +154,9 @@ public class PassingDataWorkflow : IWorkflow
.StartWith()
.Input(step => step.Input1, data => data.Value1)
.Input(step => step.Input2, data => data.Value2)
- .Output(data => data.Value3, step => step.Output)
+ .Output(data => data.Answer, step => step.Output)
.Then()
- .Input(step => step.Message, data => "The answer is " + data.Value3.ToString());
+ .Input(step => step.Message, data => "The answer is " + data.Answer.ToString());
}
...
}
@@ -175,8 +175,8 @@ or in jSON format
"StepType": "MyApp.AddNumbers, MyApp",
"NextStepId": "ShowResult",
"Inputs": {
- "Value1": "data.Value1",
- "Value2": "data.Value2"
+ "Input1": "data.Value1",
+ "Input2": "data.Value2"
},
"Outputs": {
"Answer": "step.Output"
@@ -186,7 +186,7 @@ or in jSON format
"Id": "ShowResult",
"StepType": "MyApp.CustomMessage, MyApp",
"Inputs": {
- "Message": "\"The answer is \" + data.Value1"
+ "Message": "\"The answer is \" + data.Answer"
}
}
]
@@ -203,14 +203,14 @@ Steps:
StepType: MyApp.AddNumbers, MyApp
NextStepId: ShowResult
Inputs:
- Value1: data.Value1
- Value2: data.Value2
+ Input1: data.Value1
+ Input2: data.Value2
Outputs:
Answer: step.Output
- Id: ShowResult
StepType: MyApp.CustomMessage, MyApp
Inputs:
- Message: '"The answer is " + data.Value1'
+ Message: '"The answer is " + data.Answer'
```
diff --git a/docs/images/performance-test-workflows-latency.png b/docs/images/performance-test-workflows-latency.png
new file mode 100644
index 000000000..621fc2760
Binary files /dev/null and b/docs/images/performance-test-workflows-latency.png differ
diff --git a/docs/images/performance-test-workflows-per-second.png b/docs/images/performance-test-workflows-per-second.png
new file mode 100644
index 000000000..8c4b61754
Binary files /dev/null and b/docs/images/performance-test-workflows-per-second.png differ
diff --git a/docs/performance.md b/docs/performance.md
new file mode 100644
index 000000000..77fa625a6
--- /dev/null
+++ b/docs/performance.md
@@ -0,0 +1,87 @@
+# Performance Test
+
+Workflow-core version 3.7.0 was put under test to evaluate its performance. The setup used was single node with the default MemoryPersistenceProvider persistence provider.
+
+## Methodology
+
+- Test Environment - Test were run on following two environments one after the other to see how workflow-core performance with a lower vs higher hardware configuration.
+ - Lower configuration
+ - Cores: 8 vCPU ([Standard_D8s_v3](https://learn.microsoft.com/azure/virtual-machines/dv3-dsv3-series))
+ - RAM: 32 GB
+ - OS: Linux Ubuntu 20.04
+ - dotNet 6
+ - Higher configuration
+ - Cores: 32 vCPU ([Standard_D32as_v4](https://learn.microsoft.com/azure/virtual-machines/dav4-dasv4-series))
+ - RAM: 128 GB
+ - OS: Linux Ubuntu 20.04
+ - dotNet 6
+- Test Workflow: Workflow consist of 3 basic steps. These 3 simple steps were chosen to test the performance of the workflow engine with minimal yet sufficient complexity and to avoid any external dependencies.
+ - Step1 : Generate a [random number](https://learn.microsoft.com/dotnet/api/system.random?view=net-6.0) between 1 to 10 and print it on standard output.
+ - Step2 : [Conditional step](https://github.com/danielgerlag/workflow-core/blob/master/docs/control-structures.md)
+ - Step 2.1: If value generate in step1 is > 5 then print it on standard output.
+ - Step 2.2: If value generate in step1 is <= 5 then print it on standard output.
+ - Step3: Prints a good bye message on standard output.
+- Test tools:
+ - [NBomber](https://nbomber.com/docs/getting-started/overview/) was used as performance testing framework with C# console app as base.
+
+- Test scenarios:
+ - Each type of test run executed for 20 minutes.
+ - NBomber Load Simulation of type [KeepConstant](https://nbomber.com/docs/using-nbomber/basic-api/load-simulation#keep-constant) copies was used. This type of simulation keep a constant amount of Scenario copies(instances) for a specific period.
+ - Concurrent copies [1,2,3,4,5,6,7,8,10,12,14,16,32,64,128,256,512,1024] were tested.
+ - For example if we take Concurrent copies=4 and Duration=20 minutes this means that NBomber will ensure that we have 4 instance of Test Workflow running in parallel for 20 minutes.
+
+## Results
+
+- Workflow per seconds - Below tables shows how many workflows we are able to execute per second on two different environment with increasing number of concurrent copies.
+
+| **Concurrent Copies** | **8 vCPU** | **32 vCPU** |
+| :-------------------: | :--------: | :---------: |
+| **1** | 300.6 | 504.7 |
+| **2** | 310.3 | 513.1 |
+| **3** | 309.6 | 519.3 |
+| **4** | 314.7 | 521.3 |
+| **5** | 312.4 | 519.0 |
+| **6** | 314.7 | 517.7 |
+| **7** | 318.9 | 516.7 |
+| **8** | 318.4 | 517.5 |
+| **10** | 322.6 | 517.1 |
+| **12** | 319.7 | 517.6 |
+| **14** | 322.4 | 518.1 |
+| **16** | 327.0 | 515.5 |
+| **32** | 327.7 | 515.8 |
+| **64** | 330.7 | 523.7 |
+| **128** | 332.8 | 526.9 |
+| **256** | 332.8 | 529.1 |
+| **512** | 332.8 | 529.1 |
+| **1024** | 341.3 | 529.1 |
+
+
+
+- Latency - Shows Mean, P99 and P50 latency in milliseconds on two different environment with increasing number of concurrent copies.
+
+| **Concurrent Copies** | **Mean 8 vCPU** | **Mean 32 vCPU** | **P.99 8 vCPU** | **P.99 32 vCPU** | **P.50 8 vCPU** | **P.50 32 vCPU** |
+| :-------------------: | :-------------: | :--------------: | :-------------: | :--------------: | :-------------: | :--------------: |
+| **1** | 3.32 | 1.98 | 12.67 | 2.49 | 3.13 | 1.85 |
+| **2** | 6.43 | 3.89 | 19.96 | 5.67 | 6.17 | 3.65 |
+| **3** | 9.67 | 5.77 | 24.96 | 8.2 | 9.14 | 5.46 |
+| **4** | 12.7 | 7.76 | 27.44 | 13.57 | 12.02 | 7.22 |
+| **5** | 15.99 | 9.63 | 34.59 | 41.89 | 15.14 | 9.08 |
+| **6** | 19.05 | 11.58 | 38.69 | 45.92 | 18.02 | 10.93 |
+| **7** | 21.94 | 13.54 | 42.18 | 48.9 | 20.72 | 12.66 |
+| **8** | 25.11 | 15.45 | 44.35 | 51.04 | 23.92 | 14.54 |
+| **10** | 30.98 | 19.33 | 52.29 | 56.64 | 29.31 | 18.21 |
+| **12** | 37.52 | 23.18 | 59.2 | 63.33 | 35.42 | 21.82 |
+| **14** | 43.44 | 27.01 | 67.33 | 67.58 | 41.28 | 25.55 |
+| **16** | 48.93 | 31.03 | 72.06 | 72.77 | 46.11 | 28.93 |
+| **32** | 97.65 | 62.03 | 130.05 | 104.96 | 94.91 | 58.02 |
+| **64** | 193.53 | 122.24 | 235.14 | 168.45 | 191.49 | 115.26 |
+| **128** | 384.63 | 243.74 | 449.79 | 294.65 | 379.65 | 236.67 |
+| **256** | 769.13 | 486.82 | 834.07 | 561.66 | 766.46 | 498.22 |
+| **512** | 1538.29 | 968.02 | 1725.44 | 1052.67 | 1542.14 | 962.05 |
+| **1024** | 2999.36 | 1935.32 | 3219.46 | 2072.57 | 3086.34 | 1935.36 |
+
+
+
+## References
+
+- [NBomber](https://nbomber.com/docs/getting-started/overview/)
diff --git a/docs/persistence.md b/docs/persistence.md
index 81a880c63..00799090a 100644
--- a/docs/persistence.md
+++ b/docs/persistence.md
@@ -9,4 +9,6 @@ There are several persistence providers available as separate Nuget packages.
* [PostgreSQL](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.PostgreSQL)
* [Sqlite](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.Sqlite)
* [Amazon DynamoDB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.AWS)
+* [Cosmos DB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.Azure)
* [Redis](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.Redis)
+* [Oracle](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.Oracle)
\ No newline at end of file
diff --git a/docs/samples.md b/docs/samples.md
index a1b862799..f69290c57 100644
--- a/docs/samples.md
+++ b/docs/samples.md
@@ -32,4 +32,6 @@
[Exposing a REST API](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WebApiSample)
-[Human(User) Workflow](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample08)
\ No newline at end of file
+[Human(User) Workflow](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample08)
+
+[Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19)
diff --git a/docs/workflow-middleware.md b/docs/workflow-middleware.md
new file mode 100644
index 000000000..488d0c7ad
--- /dev/null
+++ b/docs/workflow-middleware.md
@@ -0,0 +1,279 @@
+# 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](https://www.frakkingsweet.com/net-core-log-correlation-easy-access-to-headers/), [retries](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly), and other use-cases.
+
+This is done by implementing and registering `IWorkflowMiddleware` for workflows or `IWorkflowStepMiddleware` 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](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) or [`HttpClient`'s `DelegatingHandler` middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#outgoing-request-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.
+
+```cs
+public class LogCorrelationStepMiddleware : IWorkflowStepMiddleware
+{
+ private readonly ILogger _log;
+
+ public LogCorrelationStepMiddleware(
+ ILogger log)
+ {
+ _log = log;
+ }
+
+ public async Task 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 scope
+ using (_log.BeginScope("{@WorkflowId}", workflowId))
+ using (_log.BeginScope("{@StepId}", stepId))
+ {
+ // Calling next ensures step gets executed
+ return await next();
+ }
+ }
+}
+```
+
+Here's another example of a middleware that uses the [Polly](https://github.com/App-vNext/Polly) dotnet resiliency library to implement retries on workflow steps based off a custom retry policy.
+
+```cs
+public class PollyRetryStepMiddleware : IWorkflowStepMiddleware
+{
+ private const string StepContextKey = "WorkflowStepContext";
+ private const int MaxRetries = 3;
+ private readonly ILogger _log;
+
+ public PollyRetryMiddleware(ILogger log)
+ {
+ _log = log;
+ }
+
+ // Consult Polly's docs for more information on how to build
+ // retry policies:
+ // https://github.com/App-vNext/Polly
+ public IAsyncPolicy GetRetryPolicy() =>
+ Policy
+ .Handle()
+ .RetryAsync(
+ MaxRetries,
+ (result, retryCount, context) =>
+ UpdateRetryCount(
+ result.Exception,
+ retryCount,
+ context[StepContextKey] as IStepExecutionContext)
+ );
+
+ public async Task 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 policy
+ new Dictionary
+ {
+ { 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 the `WorkflowInstance` 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.
+
+```cs
+// AddDescriptionWorkflowMiddleware.cs
+public 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.cs
+public interface IDescriptiveWorkflowParams
+{
+ string Description { get; }
+}
+
+// MyWorkflowParams.cs
+public 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 of `StartWorkflow` to handle the exception and act accordingly.
+
+```cs
+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.
+
+```cs
+public class PrintWorkflowSummaryMiddleware : IWorkflowMiddleware
+{
+ private readonly ILogger _log;
+
+ public PrintWorkflowSummaryMiddleware(
+ ILogger 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($@"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.
+
+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.
+
+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.
+
+```cs
+// CustomMiddlewareErrorHandler.cs
+public class CustomHandler : IWorkflowMiddlewareErrorHandler
+{
+ public Task HandleAsync(Exception ex)
+ {
+ // Handle your error asynchronously
+ }
+}
+
+// Startup.cs
+public void ConfigureServices(IServiceCollection services)
+{
+ // Other workflow configuration
+ services.AddWorkflow();
+
+ // Should go after .AddWorkflow()
+ services.AddTransient();
+}
+```
+
+## 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.
+
+```cs
+public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ ...
+
+ // Add workflow middleware
+ services.AddWorkflowMiddleware();
+ services.AddWorkflowMiddleware();
+
+ // Add step middleware
+ services.AddWorkflowStepMiddleware();
+ services.AddWorkflowStepMiddleware();
+
+ ...
+ }
+}
+```
+
+## More Information
+
+See the [Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) sample for full examples of workflow middleware in action.
diff --git a/mkdocs.yml b/mkdocs.yml
index 1081d0f05..57ed12c94 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,15 +4,16 @@ nav:
- Getting started: getting-started.md
- External events: external-events.md
- Activity workers: activities.md
- - Error handing: error-handling.md
+ - Error handling: error-handling.md
- Control structures: control-structures.md
- Saga transactions: sagas.md
- JSON / YAML Definitions: json-yaml.md
- Persistence: persistence.md
+ - Middleware: workflow-middleware.md
- Multi-node clusters: multi-node-clusters.md
- ASP.NET Core: using-with-aspnet-core.md
- Elasticsearch plugin: elastic-search.md
- Test helpers: test-helpers.md
- Extensions: extensions.md
- Samples: samples.md
-theme: readthedocs
\ No newline at end of file
+theme: readthedocs
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 000000000..219e6f838
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,13 @@
+
+
+ https://github.com/danielgerlag/workflow-core
+ https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md
+ git
+ https://github.com/danielgerlag/workflow-core.git
+ 3.16.0
+ 3.16.0.0
+ 3.16.0.0
+ https://github.com/danielgerlag/workflow-core/raw/master/src/logo.png
+ 3.16.0
+
+
diff --git a/src/WorkflowCore.DSL/Interface/ITypeResolver.cs b/src/WorkflowCore.DSL/Interface/ITypeResolver.cs
new file mode 100644
index 000000000..e9e54e49b
--- /dev/null
+++ b/src/WorkflowCore.DSL/Interface/ITypeResolver.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Linq;
+
+namespace WorkflowCore.Interface
+{
+ public interface ITypeResolver
+ {
+ Type FindType(string name);
+ }
+}
\ No newline at end of file
diff --git a/src/WorkflowCore.DSL/Models/DefinitionSource.cs b/src/WorkflowCore.DSL/Models/DefinitionSource.cs
index 5d2bf0333..ec23f81c4 100644
--- a/src/WorkflowCore.DSL/Models/DefinitionSource.cs
+++ b/src/WorkflowCore.DSL/Models/DefinitionSource.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Text;
namespace WorkflowCore.Models.DefinitionStorage
{
diff --git a/src/WorkflowCore.DSL/Models/Envelope.cs b/src/WorkflowCore.DSL/Models/Envelope.cs
index 77448f446..7258a6d5d 100644
--- a/src/WorkflowCore.DSL/Models/Envelope.cs
+++ b/src/WorkflowCore.DSL/Models/Envelope.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Text;
namespace WorkflowCore.Models.DefinitionStorage
{
diff --git a/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs
index 1de64c35e..46f0521ca 100644
--- a/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs
+++ b/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Text;
namespace WorkflowCore.Models.DefinitionStorage.v1
{
diff --git a/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs
index 94c21e3be..d4d920544 100644
--- a/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs
+++ b/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs
@@ -1,6 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Text;
namespace WorkflowCore.Models.DefinitionStorage.v1
{
diff --git a/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs
index 82969d765..07480225a 100644
--- a/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs
+++ b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs
@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
-using System.Text;
namespace WorkflowCore.Models.DefinitionStorage.v1
{
public class StepSourceV1
{
public string StepType { get; set; }
-
+
public string Id { get; set; }
public string Name { get; set; }
@@ -30,8 +29,9 @@ public class StepSourceV1
public ExpandoObject Inputs { get; set; } = new ExpandoObject();
public Dictionary Outputs { get; set; } = new Dictionary();
-
+
public Dictionary SelectNextStep { get; set; } = new Dictionary();
+ public bool ProceedOnCancel { get; set; } = false;
}
}
diff --git a/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs b/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs
index 113236e91..a4958e6b4 100644
--- a/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs
+++ b/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs
@@ -1,8 +1,5 @@
using System;
-using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
using WorkflowCore.Interface;
using WorkflowCore.Services.DefinitionStorage;
@@ -12,6 +9,7 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddWorkflowDSL(this IServiceCollection services)
{
+ services.AddTransient();
services.AddTransient();
return services;
}
diff --git a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs
index 810d6338b..aa22988eb 100644
--- a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs
+++ b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs
@@ -1,17 +1,15 @@
-using Newtonsoft.Json;
-using System;
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Reflection;
-using System.Text;
+using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using WorkflowCore.Primitives;
-using WorkflowCore.Models.DefinitionStorage;
using WorkflowCore.Models.DefinitionStorage.v1;
using WorkflowCore.Exceptions;
@@ -20,10 +18,46 @@ namespace WorkflowCore.Services.DefinitionStorage
public class DefinitionLoader : IDefinitionLoader
{
private readonly IWorkflowRegistry _registry;
+ private readonly ITypeResolver _typeResolver;
+
+ // ParsingConfig to allow access to commonly used .NET methods like object.Equals
+ private static readonly ParsingConfig ParsingConfig = new ParsingConfig
+ {
+ AllowNewToEvaluateAnyType = true,
+ AreContextKeywordsEnabled = true
+ };
+
+ // Transform expressions to be compatible with System.Linq.Dynamic.Core 1.6.0+
+ private static string TransformExpression(string expression)
+ {
+ if (string.IsNullOrEmpty(expression))
+ return expression;
+
+ // Transform object.Equals(a, b) to Convert.ToBoolean(a) == Convert.ToBoolean(b)
+ // This is a simple regex replacement for the common pattern
+ var objectEqualsPattern = @"object\.Equals\s*\(\s*([^,]+)\s*,\s*([^)]+)\s*\)";
+ var transformed = Regex.Replace(expression, objectEqualsPattern,
+ match =>
+ {
+ var arg1 = match.Groups[1].Value.Trim();
+ var arg2 = match.Groups[2].Value.Trim();
+
+ // If arg2 is a boolean literal, convert arg1 to boolean and compare
+ if (arg2 == "true" || arg2 == "false")
+ {
+ return $"Convert.ToBoolean({arg1}) == {arg2}";
+ }
+ // Otherwise, convert both to strings for comparison
+ return $"Convert.ToString({arg1}) == Convert.ToString({arg2})";
+ });
+
+ return transformed;
+ }
- public DefinitionLoader(IWorkflowRegistry registry)
+ public DefinitionLoader(IWorkflowRegistry registry, ITypeResolver typeResolver)
{
_registry = registry;
+ _typeResolver = typeResolver;
}
public WorkflowDefinition LoadDefinition(string source, Func deserializer)
@@ -68,8 +102,22 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty
var nextStep = stack.Pop();
var stepType = FindType(nextStep.StepType);
- var containerType = typeof(WorkflowStep<>).MakeGenericType(stepType);
- var targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep);
+
+ WorkflowStep targetStep;
+
+ Type containerType;
+ if (stepType.GetInterfaces().Contains(typeof(IStepBody)))
+ {
+ containerType = typeof(WorkflowStep<>).MakeGenericType(stepType);
+
+ targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep);
+ }
+ else
+ {
+ targetStep = stepType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep;
+ if (targetStep != null)
+ stepType = targetStep.BodyType;
+ }
if (nextStep.Saga)
{
@@ -81,7 +129,7 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty
{
var cancelExprType = typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(dataType, typeof(bool)));
var dataParameter = Expression.Parameter(dataType, "data");
- var cancelExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter }, typeof(bool), nextStep.CancelCondition);
+ var cancelExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter }, typeof(bool), TransformExpression(nextStep.CancelCondition));
targetStep.CancelCondition = cancelExpr;
}
@@ -90,10 +138,11 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty
targetStep.ErrorBehavior = nextStep.ErrorBehavior;
targetStep.RetryInterval = nextStep.RetryInterval;
targetStep.ExternalId = $"{nextStep.Id}";
+ targetStep.ProceedOnCancel = nextStep.ProceedOnCancel;
AttachInputs(nextStep, dataType, stepType, targetStep);
AttachOutputs(nextStep, dataType, stepType, targetStep);
-
+
if (nextStep.Do != null)
{
foreach (var branch in nextStep.Do)
@@ -203,47 +252,134 @@ private void AttachOutputs(StepSourceV1 source, Type dataType, Type stepType, Wo
foreach (var output in source.Outputs)
{
var stepParameter = Expression.Parameter(stepType, "step");
- var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { stepParameter }, typeof(object), output.Value);
+ var sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { stepParameter }, typeof(object), TransformExpression(output.Value));
var dataParameter = Expression.Parameter(dataType, "data");
- Expression targetProperty;
- // Check if our datatype has a matching property
- var propertyInfo = dataType.GetProperty(output.Key);
- if (propertyInfo != null)
+
+ if (output.Key.Contains(".") || output.Key.Contains("["))
{
- targetProperty = Expression.Property(dataParameter, propertyInfo);
- var targetExpr = Expression.Lambda(targetProperty, dataParameter);
- step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr));
+ AttachNestedOutput(output, step, source, sourceExpr, dataParameter);
}
else
{
- // If we did not find a matching property try to find a Indexer with string parameter
- propertyInfo = dataType.GetProperty("Item");
- targetProperty = Expression.Property(dataParameter, propertyInfo, Expression.Constant(output.Key));
+ AttachDirectlyOutput(output, step, dataType, sourceExpr, dataParameter);
+ }
+ }
+ }
+
+ private void AttachDirectlyOutput(KeyValuePair output, WorkflowStep step, Type dataType, LambdaExpression sourceExpr, ParameterExpression dataParameter)
+ {
+ Expression targetProperty;
+
+ // Check if our datatype has a matching property
+ var propertyInfo = dataType.GetProperty(output.Key);
+ if (propertyInfo != null)
+ {
+ targetProperty = Expression.Property(dataParameter, propertyInfo);
+ var targetExpr = Expression.Lambda(targetProperty, dataParameter);
+ step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr));
+ }
+ else
+ {
+ // If we did not find a matching property try to find a Indexer with string parameter
+ propertyInfo = dataType.GetProperty("Item");
+ targetProperty = Expression.Property(dataParameter, propertyInfo, Expression.Constant(output.Key));
+
+ Action acn = (pStep, pData) =>
+ {
+ object resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); ;
+ propertyInfo.SetValue(pData, resolvedValue, new object[] { output.Key });
+ };
+
+ step.Outputs.Add(new ActionParameter(acn));
+ }
+
+ }
+
+ private void AttachNestedOutput(KeyValuePair output, WorkflowStep step, StepSourceV1 source, LambdaExpression sourceExpr, ParameterExpression dataParameter)
+ {
+ PropertyInfo propertyInfo = null;
+ String[] paths = output.Key.Split('.');
+
+ Expression targetProperty = dataParameter;
+
+ bool hasAddOutput = false;
+
+ foreach (String propertyName in paths)
+ {
+ if (hasAddOutput)
+ {
+ throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}");
+ }
+
+ if (targetProperty == null)
+ {
+ break;
+ }
+
+ if (propertyName.Contains("["))
+ {
+ String[] items = propertyName.Split('[');
+
+ if (items.Length != 2)
+ {
+ throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}");
+ }
+
+ items[1] = items[1].Trim().TrimEnd(']').Trim().Trim('"');
+
+ MemberExpression memberExpression = Expression.Property(targetProperty, items[0]);
+
+ if (memberExpression == null)
+ {
+ throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}");
+ }
+ propertyInfo = ((PropertyInfo)memberExpression.Member).PropertyType.GetProperty("Item");
Action acn = (pStep, pData) =>
{
+ var targetExpr = Expression.Lambda(memberExpression, dataParameter);
+ object data = targetExpr.Compile().DynamicInvoke(pData);
object resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); ;
- propertyInfo.SetValue(pData, resolvedValue, new object[] { output.Key });
+ propertyInfo.SetValue(data, resolvedValue, new object[] { items[1] });
};
step.Outputs.Add(new ActionParameter(acn));
+ hasAddOutput = true;
+ }
+ else
+ {
+ try
+ {
+ targetProperty = Expression.Property(targetProperty, propertyName);
+ }
+ catch
+ {
+ targetProperty = null;
+ break;
+ }
}
}
+
+ if (targetProperty != null && !hasAddOutput)
+ {
+ var targetExpr = Expression.Lambda(targetProperty, dataParameter);
+ step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr));
+ }
}
private void AttachOutcomes(StepSourceV1 source, Type dataType, WorkflowStep step)
{
if (!string.IsNullOrEmpty(source.NextStepId))
- step.Outcomes.Add(new ValueOutcome() { ExternalNextStepId = $"{source.NextStepId}" });
+ step.Outcomes.Add(new ValueOutcome { ExternalNextStepId = $"{source.NextStepId}" });
var dataParameter = Expression.Parameter(dataType, "data");
var outcomeParameter = Expression.Parameter(typeof(object), "outcome");
foreach (var nextStep in source.SelectNextStep)
- {
- var sourceDelegate = DynamicExpressionParser.ParseLambda(new[] { dataParameter, outcomeParameter }, typeof(object), nextStep.Value).Compile();
+ {
+ var sourceDelegate = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, outcomeParameter }, typeof(object), TransformExpression(nextStep.Value)).Compile();
Expression> sourceExpr = (data, outcome) => System.Convert.ToBoolean(sourceDelegate.DynamicInvoke(data, outcome));
step.Outcomes.Add(new ExpressionOutcome