diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..43e8ab1e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +docker-compose.yml +docker-compose.*.yml +*/bin +*/obj 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..83afb451b --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,137 @@ +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: | + 3.1.x + 6.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: | + 3.1.x + 6.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: | + 3.1.x + 6.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: | + 3.1.x + 6.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: | + 3.1.x + 6.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: | + 3.1.x + 6.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@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 3.1.x + 6.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: | + 3.1.x + 6.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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77a8c2951..085776751 100644 --- a/.gitignore +++ b/.gitignore @@ -246,3 +246,4 @@ ModelManifest.xml *.migration_in_place_backup .idea/.idea.WorkflowCore/.idea riderModule.iml +.DS_Store diff --git a/README.md b/README.md index 9ad87fb24..53ef98486 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,18 @@ [![Build status](https://ci.appveyor.com/api/projects/status/xnby6p5v4ur04u76?svg=true)](https://ci.appveyor.com/project/danielgerlag/workflow-core) -Workflow Core is a light weight 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. +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. + +### Announcements + +#### New related project: Conductor +Conductor is a stand-alone workflow server as opposed to a library that uses Workflow Core internally. It exposes an API that allows you to store workflow definitions, track running workflows, manage events and define custom steps and scripts for usage in your workflows. + +https://github.com/danielgerlag/conductor ## Documentation -See [Tutorial here.](https://github.com/danielgerlag/workflow-core/wiki) +See [Tutorial here.](https://workflow-core.readthedocs.io) ## Fluent API @@ -20,12 +27,45 @@ public class MyWorkflow : IWorkflow builder .StartWith() .Then() - .Then; + .Then(); + } +} +``` + +## JSON / YAML Workflow Definitions + +Define your workflows in JSON or YAML, need to install WorkFlowCore.DSL + +```json +{ + "Id": "HelloWorld", + "Version": 1, + "Steps": [ + { + "Id": "Hello", + "StepType": "MyApp.HelloWorld, MyApp", + "NextStepId": "Bye" + }, + { + "Id": "Bye", + "StepType": "MyApp.GoodbyeWorld, MyApp" } + ] } ``` -### Sample use cases +```yaml +Id: HelloWorld +Version: 1 +Steps: +- Id: Hello + StepType: MyApp.HelloWorld, MyApp + NextStepId: Bye +- Id: Bye + StepType: MyApp.GoodbyeWorld, MyApp +``` + +## Sample use cases * New user workflow ```c# @@ -44,7 +84,7 @@ public class MyWorkflow : IWorkflow .StartWith() .Input(step => step.Email, data => data.Email) .Input(step => step.Password, data => data.Password) - .Output(data => data.UserId, step => step.UserId); + .Output(data => data.UserId, step => step.UserId) .Then() .WaitFor("confirmation", data => data.UserId) .Then() @@ -92,15 +132,23 @@ 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) -* Redis *(coming soon...)* +* [MySQL](src/providers/WorkflowCore.Persistence.MySQL) +* [Redis](src/providers/WorkflowCore.Providers.Redis) + +## Search + +A search index provider can be plugged in to Workflow Core, enabling you to index your workflows and search against the data and state of them. +These are also available as separate Nuget packages. +* [Elasticsearch](src/providers/WorkflowCore.Providers.Elasticsearch) ## Extensions * [User (human) workflows](src/extensions/WorkflowCore.Users) -* [HTTP wrapper for Workflow Host Service](src/extensions/WorkflowCore.WebAPI) ## Samples @@ -113,12 +161,16 @@ There are several persistence providers 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) * [Events](src/samples/WorkflowCore.Sample04) +* [Activity Workers](src/samples/WorkflowCore.Sample18) + * [Parallel Tasks](src/samples/WorkflowCore.Sample13) * [Saga Transactions (with compensation)](src/samples/WorkflowCore.Sample17) @@ -133,22 +185,29 @@ There are several persistence providers available as separate Nuget packages. * [Looping](src/samples/WorkflowCore.Sample02) -* [Exposing a REST API](src/samples/WorkflowCore.Sample07) +* [Exposing a REST API](src/samples/WebApiSample) * [Human(User) Workflow](src/samples/WorkflowCore.Sample08) * [Testing](src/samples/WorkflowCore.TestSample01) -## Authors +## Contributors * **Daniel Gerlag** - *Initial work* +* **Jackie Ja** +* **Aaron Scribner** +* **Roberto Paterlini** + +## Related Projects + +* [Conductor](https://github.com/danielgerlag/conductor) (Stand-alone workflow server built on Workflow Core) ## Ports * [JWorkflow (Java)](https://github.com/danielgerlag/jworkflow) * [workflow-es (Node.js)](https://github.com/danielgerlag/workflow-es) -* [workflow_rb (Ruby)](https://github.com/danielgerlag/workflow_rb) +* [liteflow (Python)](https://github.com/danielgerlag/liteflow) ## License diff --git a/ReleaseNotes/1.6.6.md b/ReleaseNotes/1.6.6.md new file mode 100644 index 000000000..5355bdb10 --- /dev/null +++ b/ReleaseNotes/1.6.6.md @@ -0,0 +1,3 @@ +# Workflow Core 1.6.6 + +* Added optional `Reference` parameter to StartWorkflow methods \ No newline at end of file diff --git a/ReleaseNotes/1.6.8.md b/ReleaseNotes/1.6.8.md new file mode 100644 index 000000000..3c5781d88 --- /dev/null +++ b/ReleaseNotes/1.6.8.md @@ -0,0 +1,3 @@ +# Workflow Core 1.6.8 + +* Fixed the order in which multiple compensating steps execute within a saga transaction. [Issue 191](https://github.com/danielgerlag/workflow-core/issues/191) \ No newline at end of file diff --git a/ReleaseNotes/1.6.9.md b/ReleaseNotes/1.6.9.md new file mode 100644 index 000000000..a5a2dd3a6 --- /dev/null +++ b/ReleaseNotes/1.6.9.md @@ -0,0 +1,5 @@ +# 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. \ No newline at end of file diff --git a/ReleaseNotes/1.7.0.md b/ReleaseNotes/1.7.0.md new file mode 100644 index 000000000..d4bef0b4b --- /dev/null +++ b/ReleaseNotes/1.7.0.md @@ -0,0 +1,80 @@ +# Workflow Core 1.7.0 + +* Various performance optimizations, any users of the EntityFramework persistence providers will have to update their persistence libraries to the latest version as well. +* Added `CancelCondition` to fluent builder API. + + ``` + .CancelCondition(data => <>, <>) + + ``` + + 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. + + + ```c# + builder + .StartWith(context => Console.WriteLine("Hello")) + .Schedule(data => TimeSpan.FromSeconds(5)).Do(schedule => schedule + .StartWith() + .Then() + ) + .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. + + ```c# + .Parallel() + .Do(then => then + .StartWith() + .WaitFor("Approval", (data, context) => context.Workflow.IdNow) + ) + .Do(then => then + .StartWith() + .Delay(data => TimeSpan.FromDays(3)) + .Then() + ) + .Join() + .CancelCondition(data => data.IsApproved, true) + .Then(); + ``` + +* Deprecated `WorkflowCore.LockProviders.RedLock` in favour of `WorkflowCore.Providers.Redis` +* Create a new `WorkflowCore.Providers.Redis` library that includes providers for distributed locking, queues and event hubs. + * Provides Queueing support backed by Redis. + * Provides Distributed locking support backed by Redis. + * Provides event hub support backed by Redis. + + This makes it possible to have a cluster of nodes processing your workflows. + + ## Installing + + Install the NuGet package "WorkflowCore.Providers.Redis" + + Using Nuget package console + ``` + PM> Install-Package WorkflowCore.Providers.Redis + ``` + Using .NET CLI + ``` + dotnet add package WorkflowCore.Providers.Redis + ``` + + + ## Usage + + Use the `IServiceCollection` extension methods when building your service provider + * .UseRedisQueues + * .UseRedisLocking + * .UseRedisEventHub + + ```C# + services.AddWorkflow(cfg => + { + cfg.UseRedisLocking("localhost:6379"); + cfg.UseRedisQueues("localhost:6379", "my-app"); + cfg.UseRedisEventHub("localhost:6379", "my-channel") + }); + ``` diff --git a/ReleaseNotes/1.8.0.md b/ReleaseNotes/1.8.0.md new file mode 100644 index 000000000..a4222eba8 --- /dev/null +++ b/ReleaseNotes/1.8.0.md @@ -0,0 +1,160 @@ + +## 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 + +```C# +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` + + ```c# + using WorkflowCore.Interfaces; + ... + public class MyData : ISearchable +{ + public string StrValue1 { get; set; } + public string StrValue2 { get; set; } + + public IEnumerable GetSearchTokens() + { + return new List() + { + StrValue1, + StrValue2 + }; + } +} + ``` + + ##### Examples + + Search all fields for "puppies" + ```c# + 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 + ```c# + using WorkflowCore.Models.Search; + ... + + searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Reference, "My Reference")); + ``` + + 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 + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Value1, "blue moon")); + searchIndex.Search("", 0, 10, NumericRangeFilter.LessThan(x => x.Value2, 5)) + ``` + + +## Action Inputs / Outputs + +Added the action Input & Output overloads on the fluent step builder. + +```c# +Input(Action 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 + +```c# +Input((step, data) => step.Value1 = data.Value1) +``` + +```c# +.Output((step, data) => data["Value3"] = step.Output) +``` + +```c# +.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. + + +```c# +.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 + + +```c# +.Output((step, data) => data["Value3"] = step.Output) +``` + +This functionality remains intact for JSON defined workflows. \ No newline at end of file diff --git a/ReleaseNotes/1.8.1.md b/ReleaseNotes/1.8.1.md new file mode 100644 index 000000000..119083765 --- /dev/null +++ b/ReleaseNotes/1.8.1.md @@ -0,0 +1,6 @@ +# 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`. \ No newline at end of file diff --git a/ReleaseNotes/1.9.0.md b/ReleaseNotes/1.9.0.md new file mode 100644 index 000000000..fcb0b3c50 --- /dev/null +++ b/ReleaseNotes/1.9.0.md @@ -0,0 +1,6 @@ +# Workflow Core 1.9.0 + +* Indexing is now asynchronous with some simple retry mechanisms to deal with transient errors +* Removed global static state from `MemoryPersistenceProvider` +* Removed dependency on data flow library +* Optimized background queue consumers (workflow, event and index) dispatching task \ No newline at end of file diff --git a/ReleaseNotes/1.9.2.md b/ReleaseNotes/1.9.2.md new file mode 100644 index 000000000..fd84e938d --- /dev/null +++ b/ReleaseNotes/1.9.2.md @@ -0,0 +1,4 @@ +# Workflow 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 to `Compensate`. \ No newline at end of file diff --git a/ReleaseNotes/1.9.3.md b/ReleaseNotes/1.9.3.md new file mode 100644 index 000000000..585336a22 --- /dev/null +++ b/ReleaseNotes/1.9.3.md @@ -0,0 +1,4 @@ +# Workflow 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 \ No newline at end of file diff --git a/ReleaseNotes/2.0.0.md b/ReleaseNotes/2.0.0.md new file mode 100644 index 000000000..c4021f4e8 --- /dev/null +++ b/ReleaseNotes/2.0.0.md @@ -0,0 +1,72 @@ +# Workflow Core 2.0.0 + +### Upgrade notes +Existing JSON definitions will be loaded as follows + ```c# + 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 + ```c# + using WorkflowCore.Services.DefinitionStorage; + ... + DefinitionLoader.LoadDefinition(json, Deserializers.Yaml); + ``` + + Existing JSON definitions will be loaded as follows + ```c# + 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"] + ``` + diff --git a/ReleaseNotes/2.1.0.md b/ReleaseNotes/2.1.0.md new file mode 100644 index 000000000..69501a2d5 --- /dev/null +++ b/ReleaseNotes/2.1.0.md @@ -0,0 +1,10 @@ +# Workflow 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 +```c# +var runner = serviceProvider.GetService(); +... +var worfklow = await runner.RunWorkflowSync("my-workflow", 1, data, TimeSpan.FromSeconds(10)); +``` \ No newline at end of file diff --git a/ReleaseNotes/2.1.2.md b/ReleaseNotes/2.1.2.md new file mode 100644 index 000000000..77cc487f0 --- /dev/null +++ b/ReleaseNotes/2.1.2.md @@ -0,0 +1,9 @@ +# Workflow 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 container +```c# +Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan) +``` +Implementations are currently only for SQL Server, Postgres and MongoDB diff --git a/ReleaseNotes/3.0.0.md b/ReleaseNotes/3.0.0.md new file mode 100644 index 000000000..348d3a4a2 --- /dev/null +++ b/ReleaseNotes/3.0.0.md @@ -0,0 +1,61 @@ +# Workflow Core 3.0.0 + +### 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 of `data.Value1` to the activity, it then maps the result of the activity to `data.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. + + + +```C# +public class ActivityWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Activity("activity-1", (data) => data.Value1) + .Output(data => data.Value2, step => step.Result) + .Then() + .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 + +```json +{ + "Id": "activity-step", + "StepType": "WorkflowCore.Primitives.Activity, WorkflowCore", + "Inputs": + { + "ActivityName": "\"activity-1\"", + "Parameters": "data.Value1" + }, + "Outputs": { "Value2": "step.Result" } +} +``` \ No newline at end of file diff --git a/ReleaseNotes/3.1.0.md b/ReleaseNotes/3.1.0.md new file mode 100644 index 000000000..5c1b6c008 --- /dev/null +++ b/ReleaseNotes/3.1.0.md @@ -0,0 +1,82 @@ +# Workflow Core 3.1.0 + +## 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 the `Branch` method. + +The select expressions will be matched to the branch listed via the `Branch` method, and the matching next step(s) will be scheduled to execute next. + +This workflow will select `branch1` if the value of `data.Value1` is `one`, and `branch2` if it is `two`. +```c# +var branch1 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 1") + .Then() + .Input(step => step.Message, data => "bye from 1"); + +var branch2 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 2") + .Then() + .Input(step => step.Message, data => "bye from 2"); + + +builder + .StartWith() + .Decide(data => data.Value1) + .Branch((data, outcome) => data.Value1 == "one", branch1) + .Branch((data, outcome) => data.Value1 == "two", branch2); +``` + +The JSON representation would look somthing like this. + +```json +{ + "Id": "DecisionWorkflow", + "Version": 1, + "DataType": "MyApp.MyData, MyApp", + "Steps": [ + { + "Id": "decide", + "StepType": "...", + "SelectNextStep": + { + "Print1": "data.Value1 == \"one\"", + "Print2": "data.Value1 == \"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. + +``` +"SelectNextStep": +{ + "<>": "<>", + "<>": "<>" +} +``` +If the outcome of a step matches a particular expression, that step would be scheduled as the next step to execute. \ No newline at end of file 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 e027e43a5..e7fb81a2e 100644 --- a/WorkflowCore.sln +++ b/WorkflowCore.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29509.3 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}" @@ -40,8 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.Po EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.Sqlite", "src\providers\WorkflowCore.Persistence.Sqlite\WorkflowCore.Persistence.Sqlite.csproj", "{86BC1E05-E9CE-4E53-B324-885A2FDBCE74}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.LockProviders.Redlock", "src\providers\WorkflowCore.LockProviders.Redlock\WorkflowCore.LockProviders.Redlock.csproj", "{05250D58-A59E-4212-8D55-E7BC0396E9F5}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.QueueProviders.RabbitMQ", "src\providers\WorkflowCore.QueueProviders.RabbitMQ\WorkflowCore.QueueProviders.RabbitMQ.csproj", "{AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample06", "src\samples\WorkflowCore.Sample06\WorkflowCore.Sample06.csproj", "{8FEAFD74-C304-4F75-BA38-4686BE55C891}" @@ -91,11 +89,28 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReleaseNotes", "ReleaseNote ReleaseNotes\1.3.3.md = ReleaseNotes\1.3.3.md ReleaseNotes\1.4.0.md = ReleaseNotes\1.4.0.md ReleaseNotes\1.6.0.md = ReleaseNotes\1.6.0.md + ReleaseNotes\1.6.6.md = ReleaseNotes\1.6.6.md + ReleaseNotes\1.6.8.md = ReleaseNotes\1.6.8.md + ReleaseNotes\1.6.9.md = ReleaseNotes\1.6.9.md + ReleaseNotes\1.7.0.md = ReleaseNotes\1.7.0.md + ReleaseNotes\1.8.0.md = ReleaseNotes\1.8.0.md + ReleaseNotes\1.8.1.md = ReleaseNotes\1.8.1.md + ReleaseNotes\1.9.0.md = ReleaseNotes\1.9.0.md + ReleaseNotes\1.9.2.md = ReleaseNotes\1.9.2.md + ReleaseNotes\1.9.3.md = ReleaseNotes\1.9.3.md + ReleaseNotes\2.0.0.md = ReleaseNotes\2.0.0.md + ReleaseNotes\2.1.0.md = ReleaseNotes\2.1.0.md + 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 @@ -105,10 +120,40 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample15", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample16", "src\samples\WorkflowCore.Sample16\WorkflowCore.Sample16.csproj", "{0C9617A9-C8B7-45F6-A54A-261A23AC881B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "test\ScratchPad\ScratchPad.csproj", "{6396453F-4D0E-4CD4-BC89-87E8970F2A80}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample17", "src\samples\WorkflowCore.Sample17\WorkflowCore.Sample17.csproj", "{42F475BC-95F4-42E1-8CCD-7B9C27487E33}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.QueueProviders.SqlServer", "src\providers\WorkflowCore.QueueProviders.SqlServer\WorkflowCore.QueueProviders.SqlServer.csproj", "{7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Providers.AWS", "src\providers\WorkflowCore.Providers.AWS\WorkflowCore.Providers.AWS.csproj", "{5E82A137-0954-46A1-8C46-13C00F0E4842}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.MySQL", "src\providers\WorkflowCore.Persistence.MySQL\WorkflowCore.Persistence.MySQL.csproj", "{453E260D-DBDC-4DDC-BC9C-CA500CED7897}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.MySQL", "test\WorkflowCore.Tests.MySQL\WorkflowCore.Tests.MySQL.csproj", "{DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.DynamoDB", "test\WorkflowCore.Tests.DynamoDB\WorkflowCore.Tests.DynamoDB.csproj", "{3ECEC028-7E2C-4983-B928-26C073B51BB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Providers.Redis", "src\providers\WorkflowCore.Providers.Redis\WorkflowCore.Providers.Redis.csproj", "{435C6263-C6F8-4E93-B417-D861E9C22E18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Providers.Elasticsearch", "src\providers\WorkflowCore.Providers.Elasticsearch\WorkflowCore.Providers.Elasticsearch.csproj", "{F6348170-B695-4D97-BAE6-4F0F643F3BEF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Elasticsearch", "test\WorkflowCore.Tests.Elasticsearch\WorkflowCore.Tests.Elasticsearch.csproj", "{44644716-0CE8-4837-B189-AB65AE2106AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Redis", "test\WorkflowCore.Tests.Redis\WorkflowCore.Tests.Redis.csproj", "{78217204-B873-40B9-8875-E3925B2FBCEC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\WorkflowCore.DSL\WorkflowCore.DSL.csproj", "{20B98905-08CB-4854-8E2C-A31A078383E9}" +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,10 +204,6 @@ Global {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Debug|Any CPU.Build.0 = Debug|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|Any CPU.ActiveCfg = Release|Any CPU {86BC1E05-E9CE-4E53-B324-885A2FDBCE74}.Release|Any CPU.Build.0 = Release|Any CPU - {05250D58-A59E-4212-8D55-E7BC0396E9F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {05250D58-A59E-4212-8D55-E7BC0396E9F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05250D58-A59E-4212-8D55-E7BC0396E9F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {05250D58-A59E-4212-8D55-E7BC0396E9F5}.Release|Any CPU.Build.0 = Release|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Debug|Any CPU.Build.0 = Debug|Any CPU {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -267,14 +308,74 @@ Global {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C9617A9-C8B7-45F6-A54A-261A23AC881B}.Release|Any CPU.Build.0 = Release|Any CPU - {6396453F-4D0E-4CD4-BC89-87E8970F2A80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6396453F-4D0E-4CD4-BC89-87E8970F2A80}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6396453F-4D0E-4CD4-BC89-87E8970F2A80}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6396453F-4D0E-4CD4-BC89-87E8970F2A80}.Release|Any CPU.Build.0 = Release|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Debug|Any CPU.Build.0 = Debug|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|Any CPU.ActiveCfg = Release|Any CPU {42F475BC-95F4-42E1-8CCD-7B9C27487E33}.Release|Any CPU.Build.0 = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A}.Release|Any CPU.Build.0 = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E82A137-0954-46A1-8C46-13C00F0E4842}.Release|Any CPU.Build.0 = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Debug|Any CPU.Build.0 = Debug|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|Any CPU.ActiveCfg = Release|Any CPU + {453E260D-DBDC-4DDC-BC9C-CA500CED7897}.Release|Any CPU.Build.0 = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE}.Release|Any CPU.Build.0 = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ECEC028-7E2C-4983-B928-26C073B51BB7}.Release|Any CPU.Build.0 = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {435C6263-C6F8-4E93-B417-D861E9C22E18}.Release|Any CPU.Build.0 = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6348170-B695-4D97-BAE6-4F0F643F3BEF}.Release|Any CPU.Build.0 = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44644716-0CE8-4837-B189-AB65AE2106AA}.Release|Any CPU.Build.0 = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78217204-B873-40B9-8875-E3925B2FBCEC}.Release|Any CPU.Build.0 = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20B98905-08CB-4854-8E2C-A31A078383E9}.Release|Any CPU.Build.0 = Release|Any CPU + {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -294,7 +395,6 @@ Global {1DE96D4F-F2CA-4740-8764-BADD1000040A} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {9274B938-3996-4FBA-AE2F-0C82009B1116} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {86BC1E05-E9CE-4E53-B324-885A2FDBCE74} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} - {05250D58-A59E-4212-8D55-E7BC0396E9F5} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {AFAD87C7-B2EE-451E-BA7E-3F5A91358C48} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} {8FEAFD74-C304-4F75-BA38-4686BE55C891} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} {37B598A8-B054-4ABA-884D-96AEF2511600} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} @@ -321,8 +421,23 @@ Global {EC497168-5347-4E70-9D9E-9C2F826C1CDF} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} {9B7811AC-68D6-4D19-B1E9-65423393ED83} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} {0C9617A9-C8B7-45F6-A54A-261A23AC881B} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} - {6396453F-4D0E-4CD4-BC89-87E8970F2A80} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} {42F475BC-95F4-42E1-8CCD-7B9C27487E33} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} + {7EDD9353-F5C2-414C-AE51-4B0F1C5E105A} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {5E82A137-0954-46A1-8C46-13C00F0E4842} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {453E260D-DBDC-4DDC-BC9C-CA500CED7897} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {DF7F7ECA-1771-40C9-9CD0-AFEFC44E60DE} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {3ECEC028-7E2C-4983-B928-26C073B51BB7} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {435C6263-C6F8-4E93-B417-D861E9C22E18} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {F6348170-B695-4D97-BAE6-4F0F643F3BEF} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {44644716-0CE8-4837-B189-AB65AE2106AA} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4} diff --git a/WorkflowCore.sln.DotSettings b/WorkflowCore.sln.DotSettings new file mode 100644 index 000000000..f25600f78 --- /dev/null +++ b/WorkflowCore.sln.DotSettings @@ -0,0 +1,19 @@ + + NEXT_LINE + NEXT_LINE + NEXT_LINE + + NEXT_LINE + NEVER + NEXT_LINE + System + System.Linq + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/docs/activities.md b/docs/activities.md new file mode 100644 index 000000000..433555d9a --- /dev/null +++ b/docs/activities.md @@ -0,0 +1,86 @@ +# 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 of `data.Value1` to the activity, it then maps the result of the activity to `data.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. + + +```C# +public class ActivityWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Activity("activity-1", (data) => data.Value1) + .Output(data => data.Value2, step => step.Result) + .Then() + .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 + +```json +{ + "Id": "activity-step", + "StepType": "WorkflowCore.Primitives.Activity, WorkflowCore", + "Inputs": + { + "ActivityName": "\"activity-1\"", + "Parameters": "data.Value1" + }, + "Outputs": { "Value2": "step.Result" } +} +``` + +## JSON / YAML API + +The `Activity` step can be configured using inputs as follows + +| Field | Description | +| ---------------------- | --------------------------- | +| CancelCondition | Optional expression to specify a cancel condition | +| Inputs.ActivityName | Expression to specify the activity name | +| Inputs.Parameters | Expression to specify the parameters to pass the activity worker | +| Inputs.EffectiveDate | Optional expression to specify the effective date | + + +```json +{ + "Id": "MyActivityStep", + "StepType": "WorkflowCore.Primitives.Activity, WorkflowCore", + "NextStepId": "...", + "CancelCondition": "...", + "Inputs": { + "ActivityName": "\"my-activity\"", + "Parameters": "data.SomeValue" + } +} +``` +```yaml +Id: MyActivityStep +StepType: WorkflowCore.Primitives.Activity, WorkflowCore +NextStepId: "..." +CancelCondition: "..." +Inputs: + ActivityName: '"my-activity"' + EventKey: '"Key1"' + Parameters: data.SomeValue + +``` + diff --git a/docs/control-structures.md b/docs/control-structures.md new file mode 100644 index 000000000..a16ee2478 --- /dev/null +++ b/docs/control-structures.md @@ -0,0 +1,492 @@ +# Control Structures + +## Decision Branches + +You can define multiple independent branches within your workflow and select one based on an expression value. + +#### Fluent API + +For the fluent API, we define our branches with the `CreateBranch()` method on the workflow builder. We can then select a branch using the `Branch` method. + +The select expressions will be matched to the branch listed via the `Branch` method, and the matching next step(s) will be scheduled to execute next. Matching multiple next steps will result in parallel branches running. + +This workflow will select `branch1` if the value of `data.Value1` is `one`, and `branch2` if it is `two`. +```c# +var branch1 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 1") + .Then() + .Input(step => step.Message, data => "bye from 1"); + +var branch2 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 2") + .Then() + .Input(step => step.Message, data => "bye from 2"); + + +builder + .StartWith() + .Decide(data => data.Value1) + .Branch((data, outcome) => data.Value1 == "one", branch1) + .Branch((data, outcome) => data.Value1 == "two", branch2); +``` + +#### JSON / YAML API + +Hook up your branches via the `SelectNextStep` property, instead of a `NextStepId`. The expressions will be matched to the step Ids listed in `SelectNextStep`, and the matching next step(s) will be scheduled to execute next. + +```json +{ + "Id": "DecisionWorkflow", + "Version": 1, + "DataType": "MyApp.MyData, MyApp", + "Steps": [ + { + "Id": "decide", + "StepType": "...", + "SelectNextStep": + { + "Branch1": "<>", + "Branch2": "<>" + } + }, + { + "Id": "Branch1", + "StepType": "MyApp.PrintMessage, MyApp", + "Inputs": + { + "Message": "\"Hello from 1\"" + } + }, + { + "Id": "Branch2", + "StepType": "MyApp.PrintMessage, MyApp", + "Inputs": + { + "Message": "\"Hello from 2\"" + } + } + ] +} +``` + +```yaml +Id: DecisionWorkflow +Version: 1 +DataType: MyApp.MyData, MyApp +Steps: +- Id: decide + StepType: WorkflowCore.Primitives.Decide, WorkflowCore + Inputs: + Expression: <> + OutcomeSteps: + Branch1: '<>' + Branch2: '<>' +- Id: Branch1 + StepType: MyApp.PrintMessage, MyApp + Inputs: + Message: '"Hello from 1"' +- Id: Branch2 + StepType: MyApp.PrintMessage, MyApp + Inputs: + Message: '"Hello from 2"' +``` + + +## Parallel ForEach + +Use the .ForEach method to start a parallel for loop + +#### Fluent API + +```C# +public class ForEachWorkflow : IWorkflow +{ + public string Id => "Foreach"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .ForEach(data => new List() { 1, 2, 3, 4 }) + .Do(x => x + .StartWith() + .Input(step => step.Message, (data, context) => context.Item) + .Then()) + .Then(); + } +} +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyForEachStep", + "StepType": "WorkflowCore.Primitives.ForEach, WorkflowCore", + "NextStepId": "...", + "Inputs": { "Collection": "<>" }, + "Do": [[ + { + "Id": "do1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "do2" + }, + { + "Id": "do2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ]] +} +``` +```yaml +Id: MyForEachStep +StepType: WorkflowCore.Primitives.ForEach, WorkflowCore +NextStepId: "..." +Inputs: + Collection: "<>" +Do: +- - Id: do1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: do2 + - Id: do2 + StepType: MyApp.DoSomething2, MyApp +``` + + +## While Loops + +Use the .While method to start a while construct + +#### Fluent API + +```C# +public class WhileWorkflow : IWorkflow +{ + public string Id => "While"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .While(data => data.Counter < 3) + .Do(x => x + .StartWith() + .Then() + .Input(step => step.Value1, data => data.Counter) + .Output(data => data.Counter, step => step.Value2)) + .Then(); + } +} +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyWhileStep", + "StepType": "WorkflowCore.Primitives.While, WorkflowCore", + "NextStepId": "...", + "Inputs": { "Condition": "<>" }, + "Do": [[ + { + "Id": "do1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "do2" + }, + { + "Id": "do2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ]] +} +``` +```yaml +Id: MyWhileStep +StepType: WorkflowCore.Primitives.While, WorkflowCore +NextStepId: "..." +Inputs: + Condition: "<>" +Do: +- - Id: do1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: do2 + - Id: do2 + StepType: MyApp.DoSomething2, MyApp + +``` + + +## If Conditions + +Use the .If method to start an if condition + +#### Fluent API + +```C# +public class IfWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .If(data => data.Counter < 3).Do(then => then + .StartWith() + .Input(step => step.Message, data => "Value is less than 3") + ) + .If(data => data.Counter < 5).Do(then => then + .StartWith() + .Input(step => step.Message, data => "Value is less than 5") + ) + .Then(); + } +} +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyIfStep", + "StepType": "WorkflowCore.Primitives.If, WorkflowCore", + "NextStepId": "...", + "Inputs": { "Condition": "<>" }, + "Do": [[ + { + "Id": "do1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "do2" + }, + { + "Id": "do2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ]] +} +``` +```yaml +Id: MyIfStep +StepType: WorkflowCore.Primitives.If, WorkflowCore +NextStepId: "..." +Inputs: + Condition: "<>" +Do: +- - Id: do1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: do2 + - Id: do2 + StepType: MyApp.DoSomething2, MyApp + +``` + + +## Parallel Paths + +Use the .Parallel() method to branch parallel tasks + +#### Fluent API + +```C# +public class ParallelWorkflow : IWorkflow +{ + public string Id => "parallel-sample"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Parallel() + .Do(then => + then.StartWith() + .Then() + .Do(then => + then.StartWith() + .Then() + .Do(then => + then.StartWith() + .Then() + .Join() + .Then(); + } +} +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyParallelStep", + "StepType": "WorkflowCore.Primitives.Sequence, WorkflowCore", + "NextStepId": "...", + "Do": [ + [ + { + "Id": "Branch1.Step1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "Branch1.Step2" + }, + { + "Id": "Branch1.Step2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ], + [ + { + "Id": "Branch2.Step1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "Branch2.Step2" + }, + { + "Id": "Branch2.Step2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ] + ] +} +``` +```yaml +Id: MyParallelStep +StepType: WorkflowCore.Primitives.Sequence, WorkflowCore +NextStepId: "..." +Do: +- - Id: Branch1.Step1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: Branch1.Step2 + - Id: Branch1.Step2 + StepType: MyApp.DoSomething2, MyApp +- - Id: Branch2.Step1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: Branch2.Step2 + - Id: Branch2.Step2 + StepType: MyApp.DoSomething2, MyApp +``` + + +## Schedule + +Use `.Schedule` to register a future set of steps to run asynchronously in the background within your workflow. + +#### Fluent API + +```c# +builder + .StartWith(context => Console.WriteLine("Hello")) + .Schedule(data => TimeSpan.FromSeconds(5)).Do(schedule => schedule + .StartWith(context => Console.WriteLine("Doing scheduled tasks")) + ) + .Then(context => Console.WriteLine("Doing normal tasks")); +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyScheduleStep", + "StepType": "WorkflowCore.Primitives.Schedule, WorkflowCore", + "Inputs": { "Interval": "<>" }, + "Do": [[ + { + "Id": "do1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "do2" + }, + { + "Id": "do2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ]] +} +``` +```yaml +Id: MyScheduleStep +StepType: WorkflowCore.Primitives.Schedule, WorkflowCore +Inputs: + Interval: "<>" +Do: +- - Id: do1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: do2 + - Id: do2 + StepType: MyApp.DoSomething2, MyApp +``` + + +### Delay + +The `Delay` step will pause the current branch of your workflow for a specified period. + +#### JSON / YAML API + +```json +{ + "Id": "MyDelayStep", + "StepType": "WorkflowCore.Primitives.Delay, WorkflowCore", + "NextStepId": "...", + "Inputs": { "Period": "<>" } +} +``` +```yaml +Id: MyDelayStep +StepType: WorkflowCore.Primitives.Delay, WorkflowCore +NextStepId: "..." +Inputs: + Period: "<>" +``` + + +## Recur + +Use `.Recur` to setup a set of recurring background steps within your workflow, until a certain condition is met + +#### Fluent API + +```c# +builder + .StartWith(context => Console.WriteLine("Hello")) + .Recur(data => TimeSpan.FromSeconds(5), data => data.Counter > 5).Do(recur => recur + .StartWith(context => Console.WriteLine("Doing recurring task")) + ) + .Then(context => Console.WriteLine("Carry on")); +``` + +#### JSON / YAML API + +```json +{ + "Id": "MyScheduleStep", + "StepType": "WorkflowCore.Primitives.Recur, WorkflowCore", + "Inputs": { + "Interval": "<>", + "StopCondition": "<>" + }, + "Do": [[ + { + "Id": "do1", + "StepType": "MyApp.DoSomething1, MyApp", + "NextStepId": "do2" + }, + { + "Id": "do2", + "StepType": "MyApp.DoSomething2, MyApp" + } + ]] +} +``` +```yaml +Id: MyScheduleStep +StepType: WorkflowCore.Primitives.Recur, WorkflowCore +Inputs: + Interval: "<>" + StopCondition: "<>" +Do: +- - Id: do1 + StepType: MyApp.DoSomething1, MyApp + NextStepId: do2 + - Id: do2 + StepType: MyApp.DoSomething2, MyApp +``` + diff --git a/docs/elastic-search.md b/docs/elastic-search.md new file mode 100644 index 000000000..e921f958d --- /dev/null +++ b/docs/elastic-search.md @@ -0,0 +1,132 @@ +# 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. + +## Installing + +Install the NuGet package "WorkflowCore.Providers.Elasticsearch" + +Using Nuget package console +``` +PM> Install-Package WorkflowCore.Providers.Elasticsearch +``` + +Using .NET CLI +``` +dotnet add package WorkflowCore.Providers.Elasticsearch +``` + + +## 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 GetSearchTokens() + { + return new List() + { + 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(x => x.Value1, "blue moon")); + searchIndex.Search("", 0, 10, NumericRangeFilter.LessThan(x => x.Value2, 5)) + ``` diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 000000000..61d6f2f5b --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,38 @@ +# Error handling + +Each step can be configured with it's own error handling behavior, it can be retried at a later time, suspend the workflow or terminate the workflow. + +### Fluent API + +```C# +public void Build(IWorkflowBuilder builder) +{ + builder + .StartWith() + .OnError(WorkflowErrorHandling.Retry, TimeSpan.FromMinutes(10)) + .Then(); +} +``` + +### JSON / YAML API + +ErrorBehavior + +```json +{ + "Id": "...", + "StepType": "...", + "ErrorBehavior": "Retry / Suspend / Terminate / Compensate", + "RetryInterval": "00:10:00" +} +``` +```yaml +Id: "..." +StepType: "..." +ErrorBehavior: Retry / Suspend / Terminate / Compensate +RetryInterval: '00:10:00' +``` + +## Global Error handling + +The WorkflowHost service also has a `.OnStepError` event which can be used to intercept exceptions from workflow steps on a more global level. \ No newline at end of file diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 000000000..e370f2b81 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,3 @@ +## Extensions + +* [User (human) workflows](https://github.com/danielgerlag/workflow-core/tree/master/src/extensions/WorkflowCore.Users) \ No newline at end of file diff --git a/docs/external-events.md b/docs/external-events.md new file mode 100644 index 000000000..6fd8d5108 --- /dev/null +++ b/docs/external-events.md @@ -0,0 +1,64 @@ +# Events + +A workflow can also wait for an external event before proceeding. In the following example, the workflow will wait for an event called *"MyEvent"* with a key of *0*. Once an external source has fired this event, the workflow will wake up and continue processing, passing the data generated by the event onto the next step. + +```C# +public class EventSampleWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .WaitFor("MyEvent", data => "0") + .Output(data => data.Value, step => step.EventData) + .Then() + .Input(step => step.Message, data => "The data from the event is " + data.Value); + } +} +... +//External events are published via the host +//All workflows that have subscribed to MyEvent 0, will be passed "hello" +host.PublishEvent("MyEvent", "0", "hello"); +``` + +## Effective Date + +You can also specify an effective date when waiting for events, which allows you to respond to events that may have already occurred in the past, or only ones that occur after the effective date. + + +## JSON / YAML API + +The `.WaitFor` can be implemented using 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 | + + +```json +{ + "Id": "MyWaitStep", + "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", + "NextStepId": "...", + "CancelCondition": "...", + "Inputs": { + "EventName": "\"Event1\"", + "EventKey": "\"Key1\"", + "EffectiveDate": "DateTime.Now" + } +} +``` +```yaml +Id: MyWaitStep +StepType: WorkflowCore.Primitives.WaitFor, WorkflowCore +NextStepId: "..." +CancelCondition: "..." +Inputs: + EventName: '"Event1"' + EventKey: '"Key1"' + EffectiveDate: DateTime.Now + +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 000000000..26bc01879 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,270 @@ +# Basic Concepts + +## Steps + +A workflow consists of a series of connected steps. Each step can have inputs and produce outputs that can be passed back to the workflow within which it exists. + +Steps are defined by creating a class that inherits from the `StepBody` or `StepBodyAsync` abstract classes and implementing the Run/RunAsync method. They can also be created inline while defining the workflow structure. + +### First we define some steps + +```C# +public class HelloWorld : StepBody +{ + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Hello world"); + return ExecutionResult.Next(); + } +} +``` +*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. This is done by implementing the IWorkflow interface + +```C# +public class HelloWorldWorkflow : IWorkflow +{ + public string Id => "HelloWorld"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Then(); + } +} +``` +The *IWorkflow* interface also has a readonly Id property and readonly Version property. These are used by the workflow host to identify a workflow definition. + +This workflow implemented in JSON would look like this +```json +{ + "Id": "HelloWorld", + "Version": 1, + "Steps": [ + { + "Id": "Hello", + "StepType": "MyApp.HelloWorld, MyApp", + "NextStepId": "Bye" + }, + { + "Id": "Bye", + "StepType": "MyApp.GoodbyeWorld, MyApp" + } + ] +} +``` + + +### You can also define your steps inline + +```C# +public class HelloWorldWorkflow : IWorkflow +{ + public string Id => "HelloWorld"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => + { + Console.WriteLine("Hello world"); + return ExecutionResult.Next(); + }) + .Then(context => + { + Console.WriteLine("Goodbye world"); + return ExecutionResult.Next(); + }); + } +} +``` + +Each running workflow is persisted to the chosen persistence provider between each step, where it can be picked up at a later point in time to continue execution. The outcome result of your step can instruct the workflow host to defer further execution of the workflow until a future point in time or in response to an external event. + +## Host + +The workflow host is the service responsible for executing workflows. It does this by polling the persistence provider for workflow instances that are ready to run, executes them and then passes them back to the persistence provider to by stored for the next time they are run. It is also responsible for publishing events to any workflows that may be waiting on one. + +### Setup + +Use the *AddWorkflow* extension method for *IServiceCollection* to configure the workflow host upon startup of your application. +By default, it is configured with *MemoryPersistenceProvider* and *SingleNodeConcurrencyProvider* for testing purposes. You can also configure a DB persistence provider at this point. + +```C# +services.AddWorkflow(); +``` + +### Usage + +When your application starts, grab the workflow host from the built-in dependency injection framework *IServiceProvider*. Make sure you call *RegisterWorkflow*, so that the workflow host knows about all your workflows, and then call *Start()* to fire up the thread pool that executes workflows. Use the *StartWorkflow* method to initiate a new instance of a particular workflow. + + +```C# +var host = serviceProvider.GetService(); +host.RegisterWorkflow(); +host.Start(); + +host.StartWorkflow("HelloWorld", 1, null); + +Console.ReadLine(); +host.Stop(); +``` + +## Passing data between steps + +Each step is intended to be a black-box, therefore they support inputs and outputs. These inputs and outputs can be mapped to a data class that defines the custom data relevant to each workflow instance. + +The following sample shows how to define inputs and outputs on a step, it then shows how define a workflow with a typed class for internal data and how to map the inputs and outputs to properties on the custom data class. + +```C# +//Our workflow step with inputs and outputs +public class AddNumbers : StepBody +{ + public int Input1 { get; set; } + + public int Input2 { get; set; } + + public int Output { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Output = (Input1 + Input2); + return ExecutionResult.Next(); + } +} + +//Our class to define the internal data of our workflow +public class MyDataClass +{ + public int Value1 { get; set; } + public int Value2 { get; set; } + public int Answer { get; set; } +} + +//Our workflow definition with strongly typed internal data and mapped inputs & outputs +public class PassingDataWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Input(step => step.Input1, data => data.Value1) + .Input(step => step.Input2, data => data.Value2) + .Output(data => data.Answer, step => step.Output) + .Then() + .Input(step => step.Message, data => "The answer is " + data.Answer.ToString()); + } + ... +} + +``` + +or in jSON format +```json +{ + "Id": "AddWorkflow", + "Version": 1, + "DataType": "MyApp.MyDataClass, MyApp", + "Steps": [ + { + "Id": "Add", + "StepType": "MyApp.AddNumbers, MyApp", + "NextStepId": "ShowResult", + "Inputs": { + "Input1": "data.Value1", + "Input2": "data.Value2" + }, + "Outputs": { + "Answer": "step.Output" + } + }, + { + "Id": "ShowResult", + "StepType": "MyApp.CustomMessage, MyApp", + "Inputs": { + "Message": "\"The answer is \" + data.Answer" + } + } + ] +} +``` + +or in YAML format +```yaml +Id: AddWorkflow +Version: 1 +DataType: MyApp.MyDataClass, MyApp +Steps: +- Id: Add + StepType: MyApp.AddNumbers, MyApp + NextStepId: ShowResult + Inputs: + Input1: data.Value1 + Input2: data.Value2 + Outputs: + Answer: step.Output +- Id: ShowResult + StepType: MyApp.CustomMessage, MyApp + Inputs: + Message: '"The answer is " + data.Answer' +``` + + +## Injecting dependencies into steps + +If you register your step classes with the IoC container, the workflow host will use the IoC container to construct them and therefore inject any required dependencies. This example illustrates the use of dependency injection for workflow steps. + +Consider the following service + +```C# +public interface IMyService +{ + void DoTheThings(); +} +... +public class MyService : IMyService +{ + public void DoTheThings() + { + Console.WriteLine("Doing stuff..."); + } +} +``` + +Which is consumed by a workflow step as follows + +```C# +public class DoSomething : StepBody +{ + private IMyService _myService; + + public DoSomething(IMyService myService) + { + _myService = myService; + } + + public override ExecutionResult Run(IStepExecutionContext context) + { + _myService.DoTheThings(); + return ExecutionResult.Next(); + } +} +``` + +Simply add both the service and the workflow step as transients to the service collection when setting up your IoC container. +(Avoid registering steps as singletons, since multiple concurrent workflows may need to use them at once.) + +```C# +IServiceCollection services = new ServiceCollection(); +services.AddLogging(); +services.AddWorkflow(); + +services.AddTransient(); +services.AddTransient(); +``` + + 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/index.md b/docs/index.md new file mode 100644 index 000000000..ac91c1aad --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +# Workflow Core + +Workflow Core is a light weight 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. + +## Installing + +Install the NuGet package "WorkflowCore" + +Using nuget +``` +PM> Install-Package WorkflowCore +``` + +Using .net cli +``` +dotnet add package WorkflowCore +``` + +## Fluent API + +Define workflows with the fluent API. + +```c# +public class MyWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Then() + .Then; + } +} +``` \ No newline at end of file diff --git a/docs/json-yaml.md b/docs/json-yaml.md new file mode 100644 index 000000000..cfddd1306 --- /dev/null +++ b/docs/json-yaml.md @@ -0,0 +1,189 @@ +# Loading workflow definitions from JSON or YAML + +Install the `WorkflowCore.DSL` package from nuget and call `AddWorkflowDSL` on your service collection. +Then grab the `DefinitionLoader` from the IoC container and call the `.LoadDefinition` method + +```c# +using WorkflowCore.Interface; +... +var loader = serviceProvider.GetService(); +loader.LoadDefinition("<>", Deserializers.Json); +``` + +## Common DSL + +Both the JSON and YAML formats follow a common DSL, where step types within the workflow are referenced by the fully qualified class names. +Built-in step types typically live in the `WorklfowCore.Primitives` namespace. + +| 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[].StepType | 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 | + +```json +{ + "Id": "HelloWorld", + "Version": 1, + "Steps": [ + { + "Id": "Hello", + "StepType": "MyApp.HelloWorld, MyApp", + "NextStepId": "Bye" + }, + { + "Id": "Bye", + "StepType": "MyApp.GoodbyeWorld, MyApp" + } + ] +} +``` +```yaml +Id: HelloWorld +Version: 1 +Steps: +- Id: Hello + StepType: MyApp.HelloWorld, MyApp + NextStepId: Bye +- Id: Bye + StepType: MyApp.GoodbyeWorld, MyApp +``` + +### Inputs and Outputs + +Inputs and outputs can be bound to a step as a key/value pair object, +* The `Inputs` collection, the key would match a property on the `Step` class and the value would be an expression with both the `data` and `context` parameters at your disposal. +* The `Outputs` collection, the key would match a property on the `Data` class and the value would be an expression with both the `step` as a parameter at your disposal. + +Full details of the capabilities of expression language can be found [here](https://github.com/StefH/System.Linq.Dynamic.Core/wiki/Dynamic-Expressions#expression-language) + +```json +{ + "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" + } + ] +} +``` +```yaml +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 +``` + +```json +{ + "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!\"" } + } + ] +} +``` +```yaml +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!"' + +``` + +You can also pass object graphs to step inputs as opposed to just scalar values +```json +"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 +```json +"inputs": +{ + "Body": { + "@Value1": "data.MyValue * 2", + "Value2": 5 + }, + "Headers": { + "Content-Type": "application/json" + } +}, +``` + +#### Enums + +If your step has an enum property, you can just pass the string representation of the enum value and it will be automatically converted. + +#### Environment variables available in input expressions + +You can access environment variables from within input expressions. +usage: +``` +environment["VARIABLE_NAME"] +``` + diff --git a/docs/multi-node-clusters.md b/docs/multi-node-clusters.md new file mode 100644 index 000000000..c61c87164 --- /dev/null +++ b/docs/multi-node-clusters.md @@ -0,0 +1,20 @@ +# Multi-node clusters + +By default, the WorkflowHost service will run as a single node using the built-in queue and locking providers for a single node configuration. Should you wish to run a multi-node cluster, you will need to configure an external queueing mechanism and a distributed lock manager to co-ordinate the cluster. These are the providers that are currently available. + +## Queue Providers + +* SingleNodeQueueProvider *(Default built-in provider)* +* [Azure Storage Queues](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) +* [RabbitMQ](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.QueueProviders.RabbitMQ) +* [AWS Simple Queue Service](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.AWS) + + +## Distributed lock managers + +* SingleNodeLockProvider *(Default built-in provider)* +* [Azure Storage Leases](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) +* [AWS DynamoDB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.AWS) + 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 | + +![Workflows Per Second](./images/performance-test-workflows-per-second.png) + +- 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 | + +![Latency](./images/performance-test-workflows-latency.png) + +## References + +- [NBomber](https://nbomber.com/docs/getting-started/overview/) diff --git a/docs/persistence.md b/docs/persistence.md new file mode 100644 index 000000000..8a7fe55fd --- /dev/null +++ b/docs/persistence.md @@ -0,0 +1,13 @@ +# Persistence + +Since workflows are typically long running processes, they will need to be persisted to storage between steps. +There are several persistence providers available as separate Nuget packages. + +* MemoryPersistenceProvider *(Default provider, for demo and testing purposes)* +* [MongoDB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.MongoDB) +* [SQL Server](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.SqlServer) +* [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) diff --git a/docs/sagas.md b/docs/sagas.md new file mode 100644 index 000000000..083a6597a --- /dev/null +++ b/docs/sagas.md @@ -0,0 +1,161 @@ +# Saga transaction with compensation + +A Saga allows you to encapsulate a sequence of steps within a saga transaction and specify compensation steps for each. + +In the sample, `Task2` will throw an exception, then `UndoTask2` and `UndoTask1` will be triggered. + +```c# +builder + .StartWith(context => Console.WriteLine("Begin")) + .Saga(saga => saga + .StartWith() + .CompensateWith() + .Then() + .CompensateWith() + .Then() + .CompensateWith() + ) + .CompensateWith() + .Then(context => Console.WriteLine("End")); +``` + +## Retry policy for failed saga transaction + +This particular example will retry the saga every 5 seconds, but you could also simply fail completely, and process a master compensation task for the whole saga. + +```c# +builder + .StartWith(context => Console.WriteLine("Begin")) + .Saga(saga => saga + .StartWith() + .CompensateWith() + .Then() + .CompensateWith() + .Then() + .CompensateWith() + ) + .OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5)) + .Then(context => Console.WriteLine("End")); +``` + +## Compensate entire saga transaction + +You could also only specify a master compensation step, as follows + +```c# +builder + .StartWith(context => Console.WriteLine("Begin")) + .Saga(saga => saga + .StartWith() + .Then() + .Then() + ) + .CompensateWith() + .Then(context => Console.WriteLine("End")); +``` + +## Passing parameters to compensation steps + +Parameters can be passed to a compensation step as follows + +```c# +builder + .StartWith() + .CompensateWith(compensate => + { + compensate.Input(step => step.Message, data => "undoing..."); + }) +``` + +## Expressing a saga in JSON or YAML + +A saga transaction can be expressed in JSON or YAML, 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. + +```json +{ + "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" + } + ] +} +``` + +```yaml +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 + +``` diff --git a/docs/samples.md b/docs/samples.md new file mode 100644 index 000000000..f69290c57 --- /dev/null +++ b/docs/samples.md @@ -0,0 +1,37 @@ +# Samples + +[Hello World](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample01) + +[Passing Data](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample03) + +[Events](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample04) + +[Activity Workers](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample18) + +[Dependency Injection](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample15) + +[Parallel ForEach](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample09) + +[While loop](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample10) + +[If](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample11) + +[Parallel Tasks](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample13) + +[Saga Transactions](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample17) + +[Scheduled Background Tasks](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample16) + +[Recurring Background Tasks](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample14) + +[Multiple outcomes](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample12) + +[Deferred execution & re-entrant steps](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample05) + +[Looping](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample02) + +[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) + +[Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) diff --git a/docs/test-helpers.md b/docs/test-helpers.md new file mode 100644 index 000000000..9a448a551 --- /dev/null +++ b/docs/test-helpers.md @@ -0,0 +1,83 @@ +# Test helpers for Workflow Core + +Provides support writing tests for workflows built on WorkflowCore + +## Installing + +Install the NuGet package "WorkflowCore.Testing" + +``` +PM> Install-Package WorkflowCore.Testing +``` + +## Usage + +### With xUnit + +* Create a class that inherits from WorkflowTest +* Call the Setup() method in the constructor +* Implement your tests using the helper methods + * StartWorkflow() + * WaitForWorkflowToComplete() + * WaitForEventSubscription() + * GetStatus() + * GetData() + * UnhandledStepErrors + +```C# +public class xUnitTest : WorkflowTest +{ + public xUnitTest() + { + Setup(); + } + + [Fact] + public void MyWorkflow() + { + var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).Value3.Should().Be(5); + } +} +``` + + +### With NUnit + +* Create a class that inherits from WorkflowTest and decorate it with the *TestFixture* attribute +* Override the Setup method and decorate it with the *SetUp* attribute +* Implement your tests using the helper methods + * StartWorkflow() + * WaitForWorkflowToComplete() + * WaitForEventSubscription() + * GetStatus() + * GetData() + * UnhandledStepErrors + +```C# +[TestFixture] +public class NUnitTest : WorkflowTest +{ + [SetUp] + protected override void Setup() + { + base.Setup(); + } + + [Test] + public void NUnit_workflow_test_sample() + { + var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).Value3.Should().Be(5); + } + +} +``` diff --git a/docs/using-with-aspnet-core.md b/docs/using-with-aspnet-core.md new file mode 100644 index 000000000..51b0d54ac --- /dev/null +++ b/docs/using-with-aspnet-core.md @@ -0,0 +1,47 @@ +# Using with ASP.NET Core +## How to configure within an ASP.NET Core application + +In your startup class, use the `AddWorkflow` extension method to configure workflow core services, and then register your workflows and start the host when you configure the app. +```c# +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + services.AddWorkflow(cfg => + { + cfg.UseMongoDB(@"mongodb://mongo:27017", "workflow"); + cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://elastic:9200")), "workflows"); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + + var host = app.ApplicationServices.GetService(); + host.RegisterWorkflow(); + host.Start(); + } +} +``` + +## Usage + +Now simply inject the services you require into your controllers +* IWorkflowController +* IWorkflowHost +* ISearchIndex +* IPersistenceProvider \ No newline at end of file diff --git a/docs/wip/multiple-outcomes.md b/docs/wip/multiple-outcomes.md new file mode 100644 index 000000000..b8293f783 --- /dev/null +++ b/docs/wip/multiple-outcomes.md @@ -0,0 +1,21 @@ +### Multiple outcomes / forking + +A workflow can take a different path depending on the outcomes of preceeding steps. The following example shows a process where first a random number of 0 or 1 is generated and is the outcome of the first step. Then, depending on the outcome value, the workflow will either fork to (TaskA + TaskB) or (TaskC + TaskD) + +```C# +public class MultipleOutcomeWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(x => x.Name("Random Step")) + .When(data => 0).Do(then => then + .StartWith() + .Then()) + .When(data => 1).Do(then => then + .StartWith() + .Then()) + .Then(); + } +} +``` \ No newline at end of file diff --git a/docs/wip/steps-deep.md b/docs/wip/steps-deep.md new file mode 100644 index 000000000..a926eb925 --- /dev/null +++ b/docs/wip/steps-deep.md @@ -0,0 +1,16 @@ +The first time a particular step within the workflow is called, the PersistenceData property on the context object is *null*. The ExecutionResult produced by the Run method can either cause the workflow to proceed to the next step by providing an outcome value, instruct the workflow to sleep for a defined period or simply not move the workflow forward. If no outcome value is produced, then the step becomes re-entrant by setting PersistenceData, so the workflow host will call this step again in the future buy will populate the PersistenceData with it's previous value. + +For example, this step will initially run with *null* PersistenceData and put the workflow to sleep for 12 hours, while setting the PersistenceData to *new Object()*. 12 hours later, the step will be called again but context.PersistenceData will now contain the object constructed in the previous iteration, and will now produce an outcome value of *null*, causing the workflow to move forward. + +```C# +public class SleepStep : StepBody +{ + public override ExecutionResult Run(IStepExecutionContext context) + { + if (context.PersistenceData == null) + return ExecutionResult.Sleep(Timespan.FromHours(12), new Object()); + else + return ExecutionResult.Next(); + } +} +``` 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 new file mode 100644 index 000000000..57ed12c94 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,19 @@ +site_name: Workflow Core +nav: + - Home: index.md + - Getting started: getting-started.md + - External events: external-events.md + - Activity workers: activities.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 diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..b86b7e833 --- /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.9.1 + 3.9.1.0 + 3.9.1.0 + https://github.com/danielgerlag/workflow-core/raw/master/src/logo.png + 3.9.1 + + diff --git a/src/WorkflowCore.DSL/Interface/IDefinitionLoader.cs b/src/WorkflowCore.DSL/Interface/IDefinitionLoader.cs new file mode 100644 index 000000000..c8870e679 --- /dev/null +++ b/src/WorkflowCore.DSL/Interface/IDefinitionLoader.cs @@ -0,0 +1,11 @@ +using System; +using WorkflowCore.Models; +using WorkflowCore.Models.DefinitionStorage.v1; + +namespace WorkflowCore.Interface +{ + public interface IDefinitionLoader + { + WorkflowDefinition LoadDefinition(string source, Func deserializer); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Models/DefinitionStorage/DefinitionSource.cs b/src/WorkflowCore.DSL/Models/DefinitionSource.cs similarity index 82% rename from src/WorkflowCore/Models/DefinitionStorage/DefinitionSource.cs rename to src/WorkflowCore.DSL/Models/DefinitionSource.cs index 5d2bf0333..ec23f81c4 100644 --- a/src/WorkflowCore/Models/DefinitionStorage/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/Models/DefinitionStorage/Envelope.cs b/src/WorkflowCore.DSL/Models/Envelope.cs similarity index 79% rename from src/WorkflowCore/Models/DefinitionStorage/Envelope.cs rename to src/WorkflowCore.DSL/Models/Envelope.cs index 77448f446..7258a6d5d 100644 --- a/src/WorkflowCore/Models/DefinitionStorage/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/Models/DefinitionStorage/v1/DefinitionSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs similarity index 95% rename from src/WorkflowCore/Models/DefinitionStorage/v1/DefinitionSourceV1.cs rename to src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs index 1de64c35e..46f0521ca 100644 --- a/src/WorkflowCore/Models/DefinitionStorage/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/Models/DefinitionStorage/v1/MappingSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs similarity index 79% rename from src/WorkflowCore/Models/DefinitionStorage/v1/MappingSourceV1.cs rename to src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs index 94c21e3be..d4d920544 100644 --- a/src/WorkflowCore/Models/DefinitionStorage/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/Models/DefinitionStorage/v1/StepSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs similarity index 76% rename from src/WorkflowCore/Models/DefinitionStorage/v1/StepSourceV1.cs rename to src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs index fe351fa0f..07480225a 100644 --- a/src/WorkflowCore/Models/DefinitionStorage/v1/StepSourceV1.cs +++ b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Dynamic; namespace WorkflowCore.Models.DefinitionStorage.v1 { public class StepSourceV1 { public string StepType { get; set; } - + public string Id { get; set; } public string Name { get; set; } @@ -26,10 +26,12 @@ public class StepSourceV1 public string NextStepId { get; set; } - public Dictionary Inputs { get; set; } = new Dictionary(); + 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 new file mode 100644 index 000000000..5c4944f4f --- /dev/null +++ b/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Services.DefinitionStorage; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddWorkflowDSL(this IServiceCollection services) + { + services.AddTransient(); + return services; + } + } +} + diff --git a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs new file mode 100644 index 000000000..ef7fadc9a --- /dev/null +++ b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs @@ -0,0 +1,411 @@ +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 Newtonsoft.Json.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Primitives; +using WorkflowCore.Models.DefinitionStorage.v1; +using WorkflowCore.Exceptions; + +namespace WorkflowCore.Services.DefinitionStorage +{ + public class DefinitionLoader : IDefinitionLoader + { + private readonly IWorkflowRegistry _registry; + + public DefinitionLoader(IWorkflowRegistry registry) + { + _registry = registry; + } + + public WorkflowDefinition LoadDefinition(string source, Func deserializer) + { + var sourceObj = deserializer(source); + var def = Convert(sourceObj); + _registry.RegisterWorkflow(def); + return def; + } + + private WorkflowDefinition Convert(DefinitionSourceV1 source) + { + var dataType = typeof(object); + if (!string.IsNullOrEmpty(source.DataType)) + dataType = FindType(source.DataType); + + var result = new WorkflowDefinition + { + Id = source.Id, + Version = source.Version, + Steps = ConvertSteps(source.Steps, dataType), + DefaultErrorBehavior = source.DefaultErrorBehavior, + DefaultErrorRetryInterval = source.DefaultErrorRetryInterval, + Description = source.Description, + DataType = dataType + }; + + return result; + } + + + private WorkflowStepCollection ConvertSteps(ICollection source, Type dataType) + { + var result = new WorkflowStepCollection(); + int i = 0; + var stack = new Stack(source.Reverse()); + var parents = new List(); + var compensatables = new List(); + + while (stack.Count > 0) + { + var nextStep = stack.Pop(); + + var stepType = FindType(nextStep.StepType); + + 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) + { + containerType = typeof(SagaContainer<>).MakeGenericType(stepType); + targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep); + } + + if (!string.IsNullOrEmpty(nextStep.CancelCondition)) + { + 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); + targetStep.CancelCondition = cancelExpr; + } + + targetStep.Id = i; + targetStep.Name = nextStep.Name; + 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) + { + foreach (var child in branch.Reverse()) + stack.Push(child); + } + + if (nextStep.Do.Count > 0) + parents.Add(nextStep); + } + + if (nextStep.CompensateWith != null) + { + foreach (var compChild in nextStep.CompensateWith.Reverse()) + stack.Push(compChild); + + if (nextStep.CompensateWith.Count > 0) + compensatables.Add(nextStep); + } + + AttachOutcomes(nextStep, dataType, targetStep); + + result.Add(targetStep); + + i++; + } + + foreach (var step in result) + { + if (result.Any(x => x.ExternalId == step.ExternalId && x.Id != step.Id)) + throw new WorkflowDefinitionLoadException($"Duplicate step Id {step.ExternalId}"); + + foreach (var outcome in step.Outcomes) + { + if (result.All(x => x.ExternalId != outcome.ExternalNextStepId)) + throw new WorkflowDefinitionLoadException($"Cannot find step id {outcome.ExternalNextStepId}"); + + outcome.NextStep = result.Single(x => x.ExternalId == outcome.ExternalNextStepId).Id; + } + } + + foreach (var parent in parents) + { + var target = result.Single(x => x.ExternalId == parent.Id); + foreach (var branch in parent.Do) + { + var childTags = branch.Select(x => x.Id).ToList(); + target.Children.AddRange(result + .Where(x => childTags.Contains(x.ExternalId)) + .OrderBy(x => x.Id) + .Select(x => x.Id) + .Take(1) + .ToList()); + } + } + + foreach (var item in compensatables) + { + var target = result.Single(x => x.ExternalId == item.Id); + var tag = item.CompensateWith.Select(x => x.Id).FirstOrDefault(); + if (tag != null) + { + var compStep = result.FirstOrDefault(x => x.ExternalId == tag); + if (compStep != null) + target.CompensationStepId = compStep.Id; + } + } + + return result; + } + + private void AttachInputs(StepSourceV1 source, Type dataType, Type stepType, WorkflowStep step) + { + foreach (var input in source.Inputs) + { + var dataParameter = Expression.Parameter(dataType, "data"); + var contextParameter = Expression.Parameter(typeof(IStepExecutionContext), "context"); + var environmentVarsParameter = Expression.Parameter(typeof(IDictionary), "environment"); + var stepProperty = stepType.GetProperty(input.Key); + + if (stepProperty == null) + { + throw new ArgumentException($"Unknown property for input {input.Key} on {source.Id}"); + } + + if (input.Value is string) + { + var acn = BuildScalarInputAction(input, dataParameter, contextParameter, environmentVarsParameter, stepProperty); + step.Inputs.Add(new ActionParameter(acn)); + continue; + } + + if ((input.Value is IDictionary) || (input.Value is IDictionary)) + { + var acn = BuildObjectInputAction(input, dataParameter, contextParameter, environmentVarsParameter, stepProperty); + step.Inputs.Add(new ActionParameter(acn)); + continue; + } + + throw new ArgumentException($"Unknown type for input {input.Key} on {source.Id}"); + } + } + + private void AttachOutputs(StepSourceV1 source, Type dataType, Type stepType, WorkflowStep step) + { + foreach (var output in source.Outputs) + { + var stepParameter = Expression.Parameter(stepType, "step"); + var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { stepParameter }, typeof(object), output.Value); + + var dataParameter = Expression.Parameter(dataType, "data"); + + + if(output.Key.Contains(".") || output.Key.Contains("[")) + { + AttachNestedOutput(output, step, source, sourceExpr, dataParameter); + }else + { + 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(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}" }); + + 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(); + Expression> sourceExpr = (data, outcome) => System.Convert.ToBoolean(sourceDelegate.DynamicInvoke(data, outcome)); + step.Outcomes.Add(new ExpressionOutcome(sourceExpr) + { + ExternalNextStepId = $"{nextStep.Key}" + }); + } + } + + private Type FindType(string name) + { + return Type.GetType(name, true, true); + } + + private static Action BuildScalarInputAction(KeyValuePair input, ParameterExpression dataParameter, ParameterExpression contextParameter, ParameterExpression environmentVarsParameter, PropertyInfo stepProperty) + { + var expr = System.Convert.ToString(input.Value); + var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), expr); + + void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) + { + object resolvedValue = sourceExpr.Compile().DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); + if (stepProperty.PropertyType.IsEnum) + stepProperty.SetValue(pStep, Enum.Parse(stepProperty.PropertyType, (string)resolvedValue, true)); + else + { + if ((resolvedValue != null) && (stepProperty.PropertyType.IsAssignableFrom(resolvedValue.GetType()))) + stepProperty.SetValue(pStep, resolvedValue); + else + stepProperty.SetValue(pStep, System.Convert.ChangeType(resolvedValue, stepProperty.PropertyType)); + } + } + return acn; + } + + private static Action BuildObjectInputAction(KeyValuePair input, ParameterExpression dataParameter, ParameterExpression contextParameter, ParameterExpression environmentVarsParameter, PropertyInfo stepProperty) + { + void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) + { + var stack = new Stack(); + var destObj = JObject.FromObject(input.Value); + stack.Push(destObj); + + while (stack.Count > 0) + { + var subobj = stack.Pop(); + foreach (var prop in subobj.Properties().ToList()) + { + if (prop.Name.StartsWith("@")) + { + var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), prop.Value.ToString()); + object resolvedValue = sourceExpr.Compile().DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); + subobj.Remove(prop.Name); + subobj.Add(prop.Name.TrimStart('@'), JToken.FromObject(resolvedValue)); + } + } + + foreach (var child in subobj.Children()) + stack.Push(child); + } + + stepProperty.SetValue(pStep, destObj); + } + return acn; + } + + } +} diff --git a/src/WorkflowCore.DSL/Services/Deserializers.cs b/src/WorkflowCore.DSL/Services/Deserializers.cs new file mode 100644 index 000000000..4958f6f09 --- /dev/null +++ b/src/WorkflowCore.DSL/Services/Deserializers.cs @@ -0,0 +1,16 @@ +using System; +using Newtonsoft.Json; +using SharpYaml.Serialization; +using WorkflowCore.Models.DefinitionStorage.v1; + +namespace WorkflowCore.Services.DefinitionStorage +{ + public static class Deserializers + { + private static Serializer yamlSerializer = new Serializer(); + + public static Func Json = (source) => JsonConvert.DeserializeObject(source); + + public static Func Yaml = (source) => yamlSerializer.DeserializeInto(source, new DefinitionSourceV1()); + } +} diff --git a/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj b/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj new file mode 100644 index 000000000..94765a53e --- /dev/null +++ b/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + DSL extenstion for Workflow Core provding support for JSON and YAML workflow definitions. + Daniel Gerlag + + WorkflowCore + + + + + + + + + + + + + diff --git a/test/WorkflowCore.Testing/JsonWorkflowTest.cs b/src/WorkflowCore.Testing/JsonWorkflowTest.cs similarity index 88% rename from test/WorkflowCore.Testing/JsonWorkflowTest.cs rename to src/WorkflowCore.Testing/JsonWorkflowTest.cs index 0f15505cd..4e8f4281b 100644 --- a/test/WorkflowCore.Testing/JsonWorkflowTest.cs +++ b/src/WorkflowCore.Testing/JsonWorkflowTest.cs @@ -1,13 +1,12 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Services.DefinitionStorage; namespace WorkflowCore.Testing { @@ -16,6 +15,7 @@ public abstract class JsonWorkflowTest : IDisposable protected IWorkflowHost Host; protected IPersistenceProvider PersistenceProvider; protected IDefinitionLoader DefinitionLoader; + protected IWorkflowRegistry Registry; protected List UnhandledStepErrors = new List(); protected virtual void Setup() @@ -33,6 +33,7 @@ protected virtual void Setup() PersistenceProvider = serviceProvider.GetService(); DefinitionLoader = serviceProvider.GetService(); + Registry = serviceProvider.GetService(); Host = serviceProvider.GetService(); Host.OnStepError += Host_OnStepError; Host.Start(); @@ -40,7 +41,7 @@ protected virtual void Setup() private void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) { - UnhandledStepErrors.Add(new StepError() + UnhandledStepErrors.Add(new StepError { Exception = exception, Step = step, @@ -51,11 +52,12 @@ private void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exce protected virtual void ConfigureServices(IServiceCollection services) { services.AddWorkflow(); + services.AddWorkflowDSL(); } public string StartWorkflow(string json, object data) { - var def = DefinitionLoader.LoadDefinition(json); + var def = DefinitionLoader.LoadDefinition(json, Deserializers.Json); var workflowId = Host.StartWorkflow(def.Id, data).Result; return workflowId; } @@ -74,7 +76,7 @@ protected void WaitForWorkflowToComplete(string workflowId, TimeSpan timeOut) protected IEnumerable GetActiveSubscriptons(string eventName, string eventKey) { - return PersistenceProvider.GetSubcriptions(eventName, eventKey, DateTime.MaxValue).Result; + return PersistenceProvider.GetSubscriptions(eventName, eventKey, DateTime.MaxValue).Result; } protected void WaitForEventSubscription(string eventName, string eventKey, TimeSpan timeOut) @@ -104,5 +106,5 @@ public void Dispose() Host.Stop(); } } - + } diff --git a/test/WorkflowCore.Testing/WorkflowCore.Testing.csproj b/src/WorkflowCore.Testing/WorkflowCore.Testing.csproj similarity index 57% rename from test/WorkflowCore.Testing/WorkflowCore.Testing.csproj rename to src/WorkflowCore.Testing/WorkflowCore.Testing.csproj index d5f73e844..6102c9892 100644 --- a/test/WorkflowCore.Testing/WorkflowCore.Testing.csproj +++ b/src/WorkflowCore.Testing/WorkflowCore.Testing.csproj @@ -2,19 +2,21 @@ netstandard2.0 - 1.5.0 - 1.5.0.0 - 1.5.0.0 + 3.5.2 + 3.5.2.0 + 3.5.2.0 Facilitates testing of workflows built on Workflow-Core - - + + + + - \ No newline at end of file + diff --git a/src/WorkflowCore.Testing/WorkflowTest.cs b/src/WorkflowCore.Testing/WorkflowTest.cs new file mode 100644 index 000000000..bf0eb97ab --- /dev/null +++ b/src/WorkflowCore.Testing/WorkflowTest.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Testing +{ + public abstract class WorkflowTest : IDisposable + where TWorkflow : IWorkflow, new() + where TData : class, new() + { + protected IWorkflowHost Host; + protected IPersistenceProvider PersistenceProvider; + protected List UnhandledStepErrors = new List(); + + protected virtual void Setup() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(); + + PersistenceProvider = serviceProvider.GetService(); + Host = serviceProvider.GetService(); + Host.RegisterWorkflow(); + Host.OnStepError += Host_OnStepError; + Host.Start(); + } + + protected void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) + { + UnhandledStepErrors.Add(new StepError + { + Exception = exception, + Step = step, + Workflow = workflow + }); + } + + protected virtual void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(options => options.UsePollInterval(TimeSpan.FromSeconds(3))); + } + + public string StartWorkflow(TData data) + { + var def = new TWorkflow(); + var workflowId = Host.StartWorkflow(def.Id, data).Result; + return workflowId; + } + + public async Task StartWorkflowAsync(TData data) + { + var def = new TWorkflow(); + var workflowId = await Host.StartWorkflow(def.Id, data); + return workflowId; + } + + protected void WaitForWorkflowToComplete(string workflowId, TimeSpan timeOut) + { + var status = GetStatus(workflowId); + var counter = 0; + while ((status == WorkflowStatus.Runnable) && (counter < (timeOut.TotalMilliseconds / 100))) + { + Thread.Sleep(100); + counter++; + status = GetStatus(workflowId); + } + } + + protected async Task WaitForWorkflowToCompleteAsync(string workflowId, TimeSpan timeOut) + { + var status = GetStatus(workflowId); + var counter = 0; + while ((status == WorkflowStatus.Runnable) && (counter < (timeOut.TotalMilliseconds / 100))) + { + await Task.Delay(100); + counter++; + status = GetStatus(workflowId); + } + + return status; + } + + protected IEnumerable GetActiveSubscriptons(string eventName, string eventKey) + { + return PersistenceProvider.GetSubscriptions(eventName, eventKey, DateTime.MaxValue).Result; + } + + protected void WaitForEventSubscription(string eventName, string eventKey, TimeSpan timeOut) + { + var counter = 0; + while ((!GetActiveSubscriptons(eventName, eventKey).Any()) && (counter < (timeOut.TotalMilliseconds / 100))) + { + Thread.Sleep(100); + counter++; + } + } + + protected WorkflowStatus GetStatus(string workflowId) + { + var instance = PersistenceProvider.GetWorkflowInstance(workflowId).Result; + return instance.Status; + } + + protected TData GetData(string workflowId) + { + var instance = PersistenceProvider.GetWorkflowInstance(workflowId).Result; + return (TData)instance.Data; + } + + public void Dispose() + { + Host.Stop(); + } + } + + public class StepError + { + public WorkflowInstance Workflow { get; set; } + public WorkflowStep Step { get; set; } + public Exception Exception { get; set; } + } +} diff --git a/test/WorkflowCore.Testing/WorkflowTest.cs b/src/WorkflowCore.Testing/YamlWorkflowTest.cs similarity index 78% rename from test/WorkflowCore.Testing/WorkflowTest.cs rename to src/WorkflowCore.Testing/YamlWorkflowTest.cs index 3ab2ec91b..11b8e9d93 100644 --- a/test/WorkflowCore.Testing/WorkflowTest.cs +++ b/src/WorkflowCore.Testing/YamlWorkflowTest.cs @@ -1,22 +1,21 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Services.DefinitionStorage; namespace WorkflowCore.Testing { - public abstract class WorkflowTest : IDisposable - where TWorkflow : IWorkflow, new() - where TData : class, new() + public abstract class YamlWorkflowTest : IDisposable { protected IWorkflowHost Host; protected IPersistenceProvider PersistenceProvider; + protected IDefinitionLoader DefinitionLoader; + protected IWorkflowRegistry Registry; protected List UnhandledStepErrors = new List(); protected virtual void Setup() @@ -33,15 +32,16 @@ protected virtual void Setup() //loggerFactory.AddConsole(LogLevel.Debug); PersistenceProvider = serviceProvider.GetService(); + DefinitionLoader = serviceProvider.GetService(); + Registry = serviceProvider.GetService(); Host = serviceProvider.GetService(); - Host.RegisterWorkflow(); Host.OnStepError += Host_OnStepError; Host.Start(); } private void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) { - UnhandledStepErrors.Add(new StepError() + UnhandledStepErrors.Add(new StepError { Exception = exception, Step = step, @@ -52,12 +52,13 @@ private void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exce protected virtual void ConfigureServices(IServiceCollection services) { services.AddWorkflow(); + services.AddWorkflowDSL(); } - public string StartWorkflow(TData data) + public string StartWorkflow(string json, object data) { - var def = new TWorkflow(); - var workflowId = Host.StartWorkflow(def.Id, data).Result; + var def = DefinitionLoader.LoadDefinition(json, Deserializers.Yaml); + var workflowId = Host.StartWorkflow(def.Id, data).Result; return workflowId; } @@ -75,7 +76,7 @@ protected void WaitForWorkflowToComplete(string workflowId, TimeSpan timeOut) protected IEnumerable GetActiveSubscriptons(string eventName, string eventKey) { - return PersistenceProvider.GetSubcriptions(eventName, eventKey, DateTime.MaxValue).Result; + return PersistenceProvider.GetSubscriptions(eventName, eventKey, DateTime.MaxValue).Result; } protected void WaitForEventSubscription(string eventName, string eventKey, TimeSpan timeOut) @@ -94,7 +95,7 @@ protected WorkflowStatus GetStatus(string workflowId) return instance.Status; } - protected TData GetData(string workflowId) + protected TData GetData(string workflowId) { var instance = PersistenceProvider.GetWorkflowInstance(workflowId).Result; return (TData)instance.Data; @@ -105,11 +106,4 @@ public void Dispose() Host.Stop(); } } - - public class StepError - { - public WorkflowInstance Workflow { get; set; } - public WorkflowStep Step { get; set; } - public Exception Exception { get; set; } - } } diff --git a/test/WorkflowCore.Testing/readme.md b/src/WorkflowCore.Testing/readme.md similarity index 100% rename from test/WorkflowCore.Testing/readme.md rename to src/WorkflowCore.Testing/readme.md diff --git a/src/WorkflowCore/Exceptions/ActivityFailedException.cs b/src/WorkflowCore/Exceptions/ActivityFailedException.cs new file mode 100644 index 000000000..4cc22efa2 --- /dev/null +++ b/src/WorkflowCore/Exceptions/ActivityFailedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace WorkflowCore.Exceptions +{ + public class ActivityFailedException : Exception + { + public ActivityFailedException(object data) + { + // + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Exceptions/NotFoundException.cs b/src/WorkflowCore/Exceptions/NotFoundException.cs new file mode 100644 index 000000000..b85c5073c --- /dev/null +++ b/src/WorkflowCore/Exceptions/NotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace WorkflowCore.Exceptions +{ + public class NotFoundException : Exception + { + + public NotFoundException() : base() + { + + } + + public NotFoundException(string message) : base(message) + { + // + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Exceptions/WorkflowDefinitionLoadException.cs b/src/WorkflowCore/Exceptions/WorkflowDefinitionLoadException.cs index 0ceb5ab38..6cc35d6f0 100644 --- a/src/WorkflowCore/Exceptions/WorkflowDefinitionLoadException.cs +++ b/src/WorkflowCore/Exceptions/WorkflowDefinitionLoadException.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Exceptions { diff --git a/src/WorkflowCore/Exceptions/WorkflowLockedException.cs b/src/WorkflowCore/Exceptions/WorkflowLockedException.cs new file mode 100644 index 000000000..23cdaf628 --- /dev/null +++ b/src/WorkflowCore/Exceptions/WorkflowLockedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace WorkflowCore.Exceptions +{ + public class WorkflowLockedException : Exception + { + public WorkflowLockedException(): base() + { + // + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IActivityController.cs b/src/WorkflowCore/Interface/IActivityController.cs new file mode 100644 index 000000000..46464e8e8 --- /dev/null +++ b/src/WorkflowCore/Interface/IActivityController.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace WorkflowCore.Interface +{ + public class PendingActivity + { + public string Token { get; set; } + public string ActivityName { get; set; } + public object Parameters { get; set; } + public DateTime TokenExpiry { get; set; } + + } + + public interface IActivityController + { + Task GetPendingActivity(string activityName, string workerId, TimeSpan? timeout = null); + Task ReleaseActivityToken(string token); + Task SubmitActivitySuccess(string token, object result); + Task SubmitActivityFailure(string token, object result); + + } +} diff --git a/src/WorkflowCore/Interface/ICancellationProcessor.cs b/src/WorkflowCore/Interface/ICancellationProcessor.cs new file mode 100644 index 000000000..f6cbb31fa --- /dev/null +++ b/src/WorkflowCore/Interface/ICancellationProcessor.cs @@ -0,0 +1,10 @@ +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface ICancellationProcessor + { + void ProcessCancellations(WorkflowInstance workflow, WorkflowDefinition workflowDef, WorkflowExecutorResult executionResult); + } +} diff --git a/src/WorkflowCore/Interface/IDateTimeProvider.cs b/src/WorkflowCore/Interface/IDateTimeProvider.cs index eec4026f3..13c5b86f3 100644 --- a/src/WorkflowCore/Interface/IDateTimeProvider.cs +++ b/src/WorkflowCore/Interface/IDateTimeProvider.cs @@ -5,5 +5,6 @@ namespace WorkflowCore.Interface public interface IDateTimeProvider { DateTime Now { get; } + DateTime UtcNow { get; } } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IDefinitionLoader.cs b/src/WorkflowCore/Interface/IDefinitionLoader.cs deleted file mode 100644 index cd1cf5fbb..000000000 --- a/src/WorkflowCore/Interface/IDefinitionLoader.cs +++ /dev/null @@ -1,9 +0,0 @@ -using WorkflowCore.Models; - -namespace WorkflowCore.Interface -{ - public interface IDefinitionLoader - { - WorkflowDefinition LoadDefinition(string json); - } -} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IDistributedLockProvider.cs b/src/WorkflowCore/Interface/IDistributedLockProvider.cs index 3c2018e42..498fafe5b 100644 --- a/src/WorkflowCore/Interface/IDistributedLockProvider.cs +++ b/src/WorkflowCore/Interface/IDistributedLockProvider.cs @@ -9,6 +9,12 @@ namespace WorkflowCore.Interface /// public interface IDistributedLockProvider { + /// + /// Acquire a lock on the specified resource. + /// + /// Resource ID to lock. + /// + /// `true`, if the lock was acquired. Task AcquireLock(string Id, CancellationToken cancellationToken); Task ReleaseLock(string Id); diff --git a/src/WorkflowCore/Interface/IExecutionPointerFactory.cs b/src/WorkflowCore/Interface/IExecutionPointerFactory.cs index f224dc43e..d23ee98fe 100644 --- a/src/WorkflowCore/Interface/IExecutionPointerFactory.cs +++ b/src/WorkflowCore/Interface/IExecutionPointerFactory.cs @@ -4,9 +4,9 @@ namespace WorkflowCore.Interface { public interface IExecutionPointerFactory { - ExecutionPointer BuildStartingPointer(WorkflowDefinition def); + ExecutionPointer BuildGenesisPointer(WorkflowDefinition def); ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int compensationStepId); - ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, StepOutcome outcomeTarget); + ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, IStepOutcome outcomeTarget); ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPointer pointer, int childDefinitionId, object branch); } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IExecutionResultProcessor.cs b/src/WorkflowCore/Interface/IExecutionResultProcessor.cs index 7d5e67c03..3ea353ada 100644 --- a/src/WorkflowCore/Interface/IExecutionResultProcessor.cs +++ b/src/WorkflowCore/Interface/IExecutionResultProcessor.cs @@ -5,7 +5,7 @@ namespace WorkflowCore.Interface { public interface IExecutionResultProcessor { - void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step); + void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception); void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, ExecutionResult result, WorkflowExecutorResult workflowResult); } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IGreyList.cs b/src/WorkflowCore/Interface/IGreyList.cs new file mode 100644 index 000000000..3d2e0093a --- /dev/null +++ b/src/WorkflowCore/Interface/IGreyList.cs @@ -0,0 +1,9 @@ +namespace WorkflowCore.Interface +{ + public interface IGreyList + { + void Add(string id); + void Remove(string id); + bool Contains(string id); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/ILifeCycleEventHub.cs b/src/WorkflowCore/Interface/ILifeCycleEventHub.cs new file mode 100644 index 000000000..6ce3fb177 --- /dev/null +++ b/src/WorkflowCore/Interface/ILifeCycleEventHub.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Interface +{ + public interface ILifeCycleEventHub + { + Task PublishNotification(LifeCycleEvent evt); + void Subscribe(Action action); + Task Start(); + Task Stop(); + } +} diff --git a/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs b/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs new file mode 100644 index 000000000..db703659a --- /dev/null +++ b/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs @@ -0,0 +1,10 @@ +using System; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Interface +{ + public interface ILifeCycleEventPublisher : IBackgroundTask + { + void PublishNotification(LifeCycleEvent evt); + } +} diff --git a/src/WorkflowCore/Interface/IPersistenceProvider.cs b/src/WorkflowCore/Interface/IPersistenceProvider.cs deleted file mode 100644 index 709d20c3b..000000000 --- a/src/WorkflowCore/Interface/IPersistenceProvider.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using WorkflowCore.Models; - -namespace WorkflowCore.Interface -{ - /// - /// The implemention of this interface will be responsible for - /// persisiting running workflow instances to a durable store - /// - public interface IPersistenceProvider - { - Task CreateNewWorkflow(WorkflowInstance workflow); - - Task PersistWorkflow(WorkflowInstance workflow); - - Task> GetRunnableInstances(DateTime asAt); - - Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take); - - Task GetWorkflowInstance(string Id); - - Task CreateEventSubscription(EventSubscription subscription); - - Task> GetSubcriptions(string eventName, string eventKey, DateTime asOf); - - Task TerminateSubscription(string eventSubscriptionId); - - Task CreateEvent(Event newEvent); - - Task GetEvent(string id); - - Task> GetRunnableEvents(DateTime asAt); - - Task> GetEvents(string eventName, string eventKey, DateTime asOf); - - Task MarkEventProcessed(string id); - - Task MarkEventUnprocessed(string id); - - Task PersistErrors(IEnumerable errors); - - void EnsureStoreExists(); - - } -} diff --git a/src/WorkflowCore/Interface/IQueueProvider.cs b/src/WorkflowCore/Interface/IQueueProvider.cs index dfbc2f14e..7c73607e0 100644 --- a/src/WorkflowCore/Interface/IQueueProvider.cs +++ b/src/WorkflowCore/Interface/IQueueProvider.cs @@ -32,5 +32,5 @@ public interface IQueueProvider : IDisposable Task Stop(); } - public enum QueueType { Workflow = 0, Event = 1 } + public enum QueueType { Workflow = 0, Event = 1, Index = 2 } } diff --git a/src/WorkflowCore/Interface/IScopeProvider.cs b/src/WorkflowCore/Interface/IScopeProvider.cs new file mode 100644 index 000000000..bab5f94d2 --- /dev/null +++ b/src/WorkflowCore/Interface/IScopeProvider.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace WorkflowCore.Interface +{ + /// + /// The implemention of this interface will be responsible for + /// providing a new service scope for a DI container + /// + public interface IScopeProvider + { + /// + /// Create a new service scope + /// + /// + IServiceScope CreateScope(IStepExecutionContext context); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/ISearchIndex.cs b/src/WorkflowCore/Interface/ISearchIndex.cs new file mode 100644 index 000000000..0aea590a8 --- /dev/null +++ b/src/WorkflowCore/Interface/ISearchIndex.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; + +namespace WorkflowCore.Interface +{ + public interface ISearchIndex + { + Task IndexWorkflow(WorkflowInstance workflow); + + Task> Search(string terms, int skip, int take, params SearchFilter[] filters); + + Task Start(); + + Task Stop(); + } +} diff --git a/src/WorkflowCore/Interface/ISearchable.cs b/src/WorkflowCore/Interface/ISearchable.cs new file mode 100644 index 000000000..e769f916c --- /dev/null +++ b/src/WorkflowCore/Interface/ISearchable.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.Interface +{ + public interface ISearchable + { + IEnumerable GetSearchTokens(); + } +} diff --git a/src/WorkflowCore/Interface/IStepBuilder.cs b/src/WorkflowCore/Interface/IStepBuilder.cs index a9fc7b299..e20d585c5 100644 --- a/src/WorkflowCore/Interface/IStepBuilder.cs +++ b/src/WorkflowCore/Interface/IStepBuilder.cs @@ -1,12 +1,10 @@ using System; -using System.Collections; using System.Linq.Expressions; using WorkflowCore.Models; -using WorkflowCore.Primitives; namespace WorkflowCore.Interface { - public interface IStepBuilder + public interface IStepBuilder : IWorkflowModifier where TStepBody : IStepBody { @@ -22,42 +20,40 @@ public interface IStepBuilder IStepBuilder Name(string name); /// - /// Specify the next step in the workflow + /// Specifies a custom Id to reference this step /// - /// The type of the step to execute - /// Configure additional parameters for this step + /// A custom Id to reference this step /// - IStepBuilder Then(Action> stepSetup = null) where TStep : IStepBody; + IStepBuilder Id(string id); /// - /// Specify the next step in the workflow + /// Specify the next step in the workflow by Id /// - /// - /// + /// /// - IStepBuilder Then(IStepBuilder newStep) where TStep : IStepBody; + IStepBuilder Attach(string id); /// - /// Specify an inline next step in the workflow + /// Configure an outcome for this step, then wire it to another step /// - /// + /// /// - IStepBuilder Then(Func body); + [Obsolete] + IStepOutcomeBuilder When(object outcomeValue, string label = null); /// - /// Specify an inline next step in the workflow + /// Configure an outcome branch for this step, then wire it to another step /// - /// + /// /// - IStepBuilder Then(Action body); + IStepBuilder Branch(object outcomeValue, IStepBuilder branch) where TStep : IStepBody; /// - /// Configure an outcome for this step, then wire it to another step + /// Configure an outcome branch for this step, then wire it to another step /// - /// + /// /// - [Obsolete] - IStepOutcomeBuilder When(object outcomeValue, string label = null); + IStepBuilder Branch(Expression> outcomeExpression, IStepBuilder branch) where TStep : IStepBody; /// /// Map properties on the step to properties on the workflow data object before the step executes @@ -77,6 +73,14 @@ public interface IStepBuilder /// IStepBuilder Input(Expression> stepProperty, Expression> value); + /// + /// Manipulate properties on the step before its executed. + /// + /// + /// + IStepBuilder Input(Action action); + IStepBuilder Input(Action action); + /// /// Map properties on the workflow data object to properties on the step after the step executes /// @@ -87,25 +91,12 @@ public interface IStepBuilder IStepBuilder Output(Expression> dataProperty, Expression> value); /// - /// Wait here until to specified event is published + /// Manipulate properties on the data object after the step executes /// - /// The name used to identify the kind of event to wait for - /// A specific key value within the context of the event to wait for - /// Listen for events as of this effective date - /// A conditon that when true will cancel this WaitFor + /// /// - IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, Expression> cancelCondition = null); + IStepBuilder Output(Action action); - /// - /// Wait here until to specified event is published - /// - /// The name used to identify the kind of event to wait for - /// A specific key value within the context of the event to wait for - /// Listen for events as of this effective date - /// A conditon that when true will cancel this WaitFor - /// - IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, Expression> cancelCondition = null); - IStepBuilder End(string name) where TStep : IStepBody; /// @@ -122,69 +113,6 @@ public interface IStepBuilder /// IStepBuilder EndWorkflow(); - /// - /// Wait for a specified period - /// - /// - /// - IStepBuilder Delay(Expression> period); - - /// - /// Execute a block of steps, once for each item in a collection in a parallel foreach - /// - /// Resolves a collection for iterate over - /// - IContainerStepBuilder ForEach(Expression> collection); - - /// - /// Repeat a block of steps until a condition becomes true - /// - /// Resolves a condition to break out of the while loop - /// - IContainerStepBuilder While(Expression> condition); - - /// - /// Execute a block of steps if a condition is true - /// - /// Resolves a condition to evaluate - /// - IContainerStepBuilder If(Expression> condition); - - /// - /// Configure an outcome for this step, then wire it to a sequence - /// - /// - /// - IContainerStepBuilder When(Expression> outcomeValue, string label = null); - - /// - /// Execute multiple blocks of steps in parallel - /// - /// - IParallelStepBuilder Parallel(); - - /// - /// Execute a sequence of steps in a container - /// - /// - IStepBuilder Saga(Action> builder); - - /// - /// Schedule a block of steps to execute in parallel sometime in the future - /// - /// The time span to wait before executing the block - /// - IContainerStepBuilder Schedule(Expression> time); - - /// - /// Schedule a block of steps to execute in parallel sometime in the future at a recurring interval - /// - /// The time span to wait between recurring executions - /// Resolves a condition to stop the recurring task - /// - IContainerStepBuilder Recur(Expression> interval, Expression> until); - - /// /// Undo step if unhandled exception is thrown by this step /// @@ -213,6 +141,12 @@ public interface IStepBuilder /// /// IStepBuilder CompensateWithSequence(Action> builder); - + + /// + /// Prematurely cancel the execution of this step on a condition + /// + /// + /// + IStepBuilder CancelCondition(Expression> cancelCondition, bool proceedAfterCancel = false); } -} \ No newline at end of file +} diff --git a/src/WorkflowCore/Interface/IStepExecutionContext.cs b/src/WorkflowCore/Interface/IStepExecutionContext.cs index e59198a13..7aab7123c 100644 --- a/src/WorkflowCore/Interface/IStepExecutionContext.cs +++ b/src/WorkflowCore/Interface/IStepExecutionContext.cs @@ -1,4 +1,5 @@ -using WorkflowCore.Models; +using System.Threading; +using WorkflowCore.Models; namespace WorkflowCore.Interface { @@ -12,6 +13,8 @@ public interface IStepExecutionContext WorkflowStep Step { get; set; } - WorkflowInstance Workflow { get; set; } + WorkflowInstance Workflow { get; set; } + + CancellationToken CancellationToken { get; set; } } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IStepExecutor.cs b/src/WorkflowCore/Interface/IStepExecutor.cs new file mode 100644 index 000000000..aed3d3026 --- /dev/null +++ b/src/WorkflowCore/Interface/IStepExecutor.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + /// + /// Executes a workflow step. + /// + public interface IStepExecutor + { + /// + /// Runs the passed in the given . + /// + /// The in which to execute the step. + /// The body. + /// A to wait for the result of running the step + Task ExecuteStep( + IStepExecutionContext context, + IStepBody body + ); + } +} diff --git a/src/WorkflowCore/Interface/IStepOutcome.cs b/src/WorkflowCore/Interface/IStepOutcome.cs new file mode 100644 index 000000000..368371006 --- /dev/null +++ b/src/WorkflowCore/Interface/IStepOutcome.cs @@ -0,0 +1,14 @@ +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IStepOutcome + { + string ExternalNextStepId { get; set; } + string Label { get; set; } + int NextStep { get; set; } + + bool Matches(object data); + bool Matches(ExecutionResult executionResult, object data); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IStepOutcomeBuilder.cs b/src/WorkflowCore/Interface/IStepOutcomeBuilder.cs index a9c6ca866..0145ca1db 100644 --- a/src/WorkflowCore/Interface/IStepOutcomeBuilder.cs +++ b/src/WorkflowCore/Interface/IStepOutcomeBuilder.cs @@ -8,7 +8,7 @@ public interface IStepOutcomeBuilder { IWorkflowBuilder WorkflowBuilder { get; } - StepOutcome Outcome { get; } + ValueOutcome Outcome { get; } IStepBuilder Then(Action> stepSetup = null) where TStep : IStepBody; diff --git a/src/WorkflowCore/Interface/IStepParameter.cs b/src/WorkflowCore/Interface/IStepParameter.cs new file mode 100644 index 000000000..5bbbb2fbf --- /dev/null +++ b/src/WorkflowCore/Interface/IStepParameter.cs @@ -0,0 +1,8 @@ +namespace WorkflowCore.Interface +{ + public interface IStepParameter + { + void AssignInput(object data, IStepBody body, IStepExecutionContext context); + void AssignOutput(object data, IStepBody body, IStepExecutionContext context); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs b/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs new file mode 100644 index 000000000..051b59e91 --- /dev/null +++ b/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface ISyncWorkflowRunner + { + Task RunWorkflowSync(string workflowId, int version, TData data, string reference, TimeSpan timeOut, bool persistSate = true) + where TData : new(); + + Task RunWorkflowSync(string workflowId, int version, TData data, string reference, CancellationToken token, bool persistSate = true) + where TData : new(); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowBuilder.cs b/src/WorkflowCore/Interface/IWorkflowBuilder.cs index 7b0b90ffb..b7b371882 100644 --- a/src/WorkflowCore/Interface/IWorkflowBuilder.cs +++ b/src/WorkflowCore/Interface/IWorkflowBuilder.cs @@ -7,17 +7,21 @@ namespace WorkflowCore.Interface { public interface IWorkflowBuilder { - int LastStep { get; } + List Steps { get; } + + int LastStep { get; } IWorkflowBuilder UseData(); WorkflowDefinition Build(string id, int version); void AddStep(WorkflowStep step); + + void AttachBranch(IWorkflowBuilder branch); } - public interface IWorkflowBuilder : IWorkflowBuilder - { + public interface IWorkflowBuilder : IWorkflowBuilder, IWorkflowModifier + { IStepBuilder StartWith(Action> stepSetup = null) where TStep : IStepBody; IStepBuilder StartWith(Func body); @@ -27,5 +31,7 @@ public interface IWorkflowBuilder : IWorkflowBuilder IEnumerable GetUpstreamSteps(int id); IWorkflowBuilder UseDefaultErrorBehavior(WorkflowErrorHandling behavior, TimeSpan? retryInterval = null); + + IWorkflowBuilder CreateBranch(); } -} \ No newline at end of file +} diff --git a/src/WorkflowCore/Interface/IWorkflowController.cs b/src/WorkflowCore/Interface/IWorkflowController.cs index 48b7ca405..924925d2c 100644 --- a/src/WorkflowCore/Interface/IWorkflowController.cs +++ b/src/WorkflowCore/Interface/IWorkflowController.cs @@ -1,20 +1,18 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace WorkflowCore.Interface { public interface IWorkflowController { - Task StartWorkflow(string workflowId, object data = null); - Task StartWorkflow(string workflowId, int? version, object data = null); - Task StartWorkflow(string workflowId, TData data = null) where TData : class; - Task StartWorkflow(string workflowId, int? version, TData data = null) where TData : class; + Task StartWorkflow(string workflowId, object data = null, string reference=null); + Task StartWorkflow(string workflowId, int? version, object data = null, string reference=null); + Task StartWorkflow(string workflowId, TData data = null, string reference=null) where TData : class, new(); + Task StartWorkflow(string workflowId, int? version, TData data = null, string reference=null) where TData : class, new(); Task PublishEvent(string eventName, string eventKey, object eventData, DateTime? effectiveDate = null); - void RegisterWorkflow() where TWorkflow : IWorkflow, new(); - void RegisterWorkflow() where TWorkflow : IWorkflow, new() where TData : new(); + void RegisterWorkflow() where TWorkflow : IWorkflow; + void RegisterWorkflow() where TWorkflow : IWorkflow where TData : new(); /// /// Suspend the execution of a given workflow until .ResumeWorkflow is called diff --git a/src/WorkflowCore/Interface/IWorkflowErrorHandler.cs b/src/WorkflowCore/Interface/IWorkflowErrorHandler.cs new file mode 100644 index 000000000..fa0734118 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowErrorHandler.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IWorkflowErrorHandler + { + WorkflowErrorHandling Type { get; } + void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue); + } +} diff --git a/src/WorkflowCore/Interface/IWorkflowExecutor.cs b/src/WorkflowCore/Interface/IWorkflowExecutor.cs index 798057fdc..d0df49d40 100644 --- a/src/WorkflowCore/Interface/IWorkflowExecutor.cs +++ b/src/WorkflowCore/Interface/IWorkflowExecutor.cs @@ -1,10 +1,11 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using WorkflowCore.Models; namespace WorkflowCore.Interface { public interface IWorkflowExecutor { - Task Execute(WorkflowInstance workflow); + Task Execute(WorkflowInstance workflow, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowHost.cs b/src/WorkflowCore/Interface/IWorkflowHost.cs index cc7ee151f..8e279096c 100644 --- a/src/WorkflowCore/Interface/IWorkflowHost.cs +++ b/src/WorkflowCore/Interface/IWorkflowHost.cs @@ -1,11 +1,12 @@ using Microsoft.Extensions.Logging; using System; -using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Interface { - public interface IWorkflowHost : IWorkflowController + public interface IWorkflowHost : IWorkflowController, IActivityController, IHostedService { /// /// Start the workflow host, this enable execution of workflows @@ -19,6 +20,7 @@ public interface IWorkflowHost : IWorkflowController event StepErrorEventHandler OnStepError; + event LifeCycleEventHandler OnLifeCycleEvent; void ReportStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception); //public dependencies to allow for extension method access @@ -32,4 +34,5 @@ public interface IWorkflowHost : IWorkflowController } public delegate void StepErrorEventHandler(WorkflowInstance workflow, WorkflowStep step, Exception exception); -} + public delegate void LifeCycleEventHandler(LifeCycleEvent evt); +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowMiddleware.cs b/src/WorkflowCore/Interface/IWorkflowMiddleware.cs new file mode 100644 index 000000000..ede4ca8ec --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowMiddleware.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + /// + /// Determines at which point to run the middleware. + /// + public enum WorkflowMiddlewarePhase + { + /// + /// The middleware should run before a workflow starts. + /// + PreWorkflow, + + /// + /// The middleware should run after a workflow completes. + /// + PostWorkflow, + + /// + /// The middleware should run after each workflow execution. + /// + ExecuteWorkflow + } + + /// + /// Middleware that can run before a workflow starts or after a workflow completes. + /// + public interface IWorkflowMiddleware + { + /// + /// The phase in the workflow execution to run this middleware in + /// + WorkflowMiddlewarePhase Phase { get; } + + /// + /// Runs the middleware on the given . + /// + /// The . + /// The next middleware in the chain. + /// A that completes asynchronously once the + /// middleware chain finishes running. + Task HandleAsync(WorkflowInstance workflow, WorkflowDelegate next); + } +} diff --git a/src/WorkflowCore/Interface/IWorkflowMiddlewareErrorHandler.cs b/src/WorkflowCore/Interface/IWorkflowMiddlewareErrorHandler.cs new file mode 100644 index 000000000..5522bae39 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowMiddlewareErrorHandler.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace WorkflowCore.Interface +{ + /// + /// Handles exceptions within workflow middleware. + /// + public interface IWorkflowMiddlewareErrorHandler + { + /// + /// Asynchronously handle the given exception. + /// + /// The exception to handle + /// A task that completes when handling is done. + Task HandleAsync(Exception ex); + } +} diff --git a/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs b/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs new file mode 100644 index 000000000..6c47899f5 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowMiddlewareRunner.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + /// + /// Runs workflow pre/post and execute middleware. + /// + public interface IWorkflowMiddlewareRunner + { + /// + /// Runs workflow-level middleware that is set to run at the + /// phase. Middleware will be run in the + /// order in which they were registered with DI with middleware declared earlier starting earlier and + /// completing later. + /// + /// The to run for. + /// The definition. + /// A task that will complete when all middleware has run. + Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition def); + + /// + /// Runs workflow-level middleware that is set to run at the + /// phase. Middleware will be run in the + /// order in which they were registered with DI with middleware declared earlier starting earlier and + /// completing later. + /// + /// The to run for. + /// The definition. + /// A task that will complete when all middleware has run. + Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def); + + /// + /// Runs workflow-level middleware that is set to run at the + /// phase. Middleware will be run in the + /// order in which they were registered with DI with middleware declared earlier starting earlier and + /// completing later. + /// + /// The to run for. + /// The definition. + /// A task that will complete when all middleware has run. + Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def); + } +} diff --git a/src/WorkflowCore/Interface/IWorkflowModifier.cs b/src/WorkflowCore/Interface/IWorkflowModifier.cs new file mode 100644 index 000000000..835855e97 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowModifier.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections; +using System.Linq.Expressions; +using WorkflowCore.Models; +using WorkflowCore.Primitives; + +namespace WorkflowCore.Interface +{ + public interface IWorkflowModifier + where TStepBody : IStepBody + + { + /// + /// Specify the next step in the workflow + /// + /// The type of the step to execute + /// Configure additional parameters for this step + /// + IStepBuilder Then(Action> stepSetup = null) where TStep : IStepBody; + + /// + /// Specify the next step in the workflow + /// + /// + /// + /// + IStepBuilder Then(IStepBuilder newStep) where TStep : IStepBody; + + /// + /// Specify an inline next step in the workflow + /// + /// + /// + IStepBuilder Then(Func body); + + /// + /// Specify an inline next step in the workflow + /// + /// + /// + IStepBuilder Then(Action body); + + /// + /// Wait here until to specified event is published + /// + /// The name used to identify the kind of event to wait for + /// A specific key value within the context of the event to wait for + /// Listen for events as of this effective date + /// A conditon that when true will cancel this WaitFor + /// + IStepBuilder WaitFor(string eventName, Expression> eventKey, + Expression> effectiveDate = null, Expression> cancelCondition = null); + + /// + /// Wait here until to specified event is published + /// + /// The name used to identify the kind of event to wait for + /// A specific key value within the context of the event to wait for + /// Listen for events as of this effective date + /// A conditon that when true will cancel this WaitFor + /// + IStepBuilder WaitFor(string eventName, + Expression> eventKey, + Expression> effectiveDate = null, Expression> cancelCondition = null); + + /// + /// Wait for a specified period + /// + /// + /// + IStepBuilder Delay(Expression> period); + + /// + /// Evaluate an expression and take a different path depending on the value + /// + /// Expression to evaluate for decision + /// + IStepBuilder Decide(Expression> expression); + + /// + /// Execute a block of steps, once for each item in a collection in a parallel foreach + /// + /// Resolves a collection for iterate over + /// + IContainerStepBuilder ForEach(Expression> collection); + + /// + /// Execute a block of steps, once for each item in a collection in a RunParallel foreach + /// + /// Resolves a collection for iterate over + /// + IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel); + + /// + /// Execute a block of steps, once for each item in a collection in a RunParallel foreach + /// + /// Resolves a collection for iterate over + /// + IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel); + + /// + /// Repeat a block of steps until a condition becomes true + /// + /// Resolves a condition to break out of the while loop + /// + IContainerStepBuilder While(Expression> condition); + + /// + /// Repeat a block of steps until a condition becomes true + /// + /// Resolves a condition to break out of the while loop + /// + IContainerStepBuilder While(Expression> condition); + + /// + /// Execute a block of steps if a condition is true + /// + /// Resolves a condition to evaluate + /// + IContainerStepBuilder If(Expression> condition); + + /// + /// Execute a block of steps if a condition is true + /// + /// Resolves a condition to evaluate + /// + IContainerStepBuilder If(Expression> condition); + + /// + /// Configure an outcome for this step, then wire it to a sequence + /// + /// + /// + IContainerStepBuilder When(Expression> outcomeValue, + string label = null); + + /// + /// Execute multiple blocks of steps in parallel + /// + /// + IParallelStepBuilder Parallel(); + + /// + /// Execute a sequence of steps in a container + /// + /// + IStepBuilder Saga(Action> builder); + + /// + /// Schedule a block of steps to execute in parallel sometime in the future + /// + /// The time span to wait before executing the block + /// + IContainerStepBuilder Schedule(Expression> time); + + /// + /// Schedule a block of steps to execute in parallel sometime in the future at a recurring interval + /// + /// The time span to wait between recurring executions + /// Resolves a condition to stop the recurring task + /// + IContainerStepBuilder Recur(Expression> interval, + Expression> until); + + /// + /// Wait here until an external activity is complete + /// + /// The name used to identify the activity to wait for + /// The data to pass the external activity worker + /// Listen for events as of this effective date + /// A conditon that when true will cancel this WaitFor + /// + IStepBuilder Activity(string activityName, Expression> parameters = null, + Expression> effectiveDate = null, Expression> cancelCondition = null); + + /// + /// Wait here until an external activity is complete + /// + /// The name used to identify the activity to wait for + /// The data to pass the external activity worker + /// Listen for events as of this effective date + /// A conditon that when true will cancel this WaitFor + /// + IStepBuilder Activity(Expression> activityName, Expression> parameters = null, + Expression> effectiveDate = null, Expression> cancelCondition = null); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowPurger.cs b/src/WorkflowCore/Interface/IWorkflowPurger.cs new file mode 100644 index 000000000..42f6ba85f --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowPurger.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IWorkflowPurger + { + Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Interface/IWorkflowRegistry.cs b/src/WorkflowCore/Interface/IWorkflowRegistry.cs index 555b4e84f..40143e3c1 100644 --- a/src/WorkflowCore/Interface/IWorkflowRegistry.cs +++ b/src/WorkflowCore/Interface/IWorkflowRegistry.cs @@ -1,4 +1,5 @@ -using WorkflowCore.Models; +using System.Collections.Generic; +using WorkflowCore.Models; namespace WorkflowCore.Interface { @@ -8,5 +9,8 @@ public interface IWorkflowRegistry void RegisterWorkflow(WorkflowDefinition definition); void RegisterWorkflow(IWorkflow workflow) where TData : new(); WorkflowDefinition GetDefinition(string workflowId, int? version = null); + bool IsRegistered(string workflowId, int version); + void DeregisterWorkflow(string workflowId, int version); + IEnumerable GetAllDefinitions(); } } diff --git a/src/WorkflowCore/Interface/IWorkflowStepMiddleware.cs b/src/WorkflowCore/Interface/IWorkflowStepMiddleware.cs new file mode 100644 index 000000000..91f888589 --- /dev/null +++ b/src/WorkflowCore/Interface/IWorkflowStepMiddleware.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + /// + /// Middleware that runs around a workflow step and can enhance or alter + /// the steps behavior. + /// + public interface IWorkflowStepMiddleware + { + /// + /// Handle the workflow step and return an + /// asynchronously. It is important to invoke at some point + /// in the middleware. Not doing so will prevent the workflow step from ever + /// getting executed. + /// + /// The step's context. + /// An instance of the step body that is going to be run. + /// The next middleware in the chain. + /// A of the workflow result. + Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next + ); + } +} diff --git a/src/WorkflowCore/Interface/Persistence/IEventRepository.cs b/src/WorkflowCore/Interface/Persistence/IEventRepository.cs new file mode 100644 index 000000000..75651c24b --- /dev/null +++ b/src/WorkflowCore/Interface/Persistence/IEventRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IEventRepository + { + Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default); + + Task GetEvent(string id, CancellationToken cancellationToken = default); + + Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default); + + Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); + + Task MarkEventProcessed(string id, CancellationToken cancellationToken = default); + + Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default); + + } +} diff --git a/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs b/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs new file mode 100644 index 000000000..4b83f5920 --- /dev/null +++ b/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IPersistenceProvider : IWorkflowRepository, ISubscriptionRepository, IEventRepository, IScheduledCommandRepository + { + + Task PersistErrors(IEnumerable errors, CancellationToken cancellationToken = default); + + void EnsureStoreExists(); + + } +} diff --git a/src/WorkflowCore/Interface/Persistence/IScheduledCommandRepository.cs b/src/WorkflowCore/Interface/Persistence/IScheduledCommandRepository.cs new file mode 100644 index 000000000..a86bccc42 --- /dev/null +++ b/src/WorkflowCore/Interface/Persistence/IScheduledCommandRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IScheduledCommandRepository + { + bool SupportsScheduledCommands { get; } + + Task ScheduleCommand(ScheduledCommand command); + + Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default); + } +} diff --git a/src/WorkflowCore/Interface/Persistence/ISubscriptionRepository.cs b/src/WorkflowCore/Interface/Persistence/ISubscriptionRepository.cs new file mode 100644 index 000000000..2f22a45f4 --- /dev/null +++ b/src/WorkflowCore/Interface/Persistence/ISubscriptionRepository.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface ISubscriptionRepository + { + Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default); + + Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); + + Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default); + + Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default); + + Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); + + Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default); + + Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default); + + } +} diff --git a/src/WorkflowCore/Interface/Persistence/IWorkflowRepository.cs b/src/WorkflowCore/Interface/Persistence/IWorkflowRepository.cs new file mode 100644 index 000000000..09842af7a --- /dev/null +++ b/src/WorkflowCore/Interface/Persistence/IWorkflowRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Models; + +namespace WorkflowCore.Interface +{ + public interface IWorkflowRepository + { + Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default); + + Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default); + + Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default); + + Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default); + + [Obsolete] + Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take); + + Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default); + + Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default); + + } +} diff --git a/src/WorkflowCore/Models/ActionParameter.cs b/src/WorkflowCore/Models/ActionParameter.cs new file mode 100644 index 000000000..762a865e8 --- /dev/null +++ b/src/WorkflowCore/Models/ActionParameter.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; + +namespace WorkflowCore.Models +{ + public class ActionParameter : IStepParameter + { + private readonly Action _action; + + public ActionParameter(Action action) + { + _action = action; + } + + public ActionParameter(Action action) + { + _action = new Action((body, data, context) => + { + action(body, data); + }); + } + + private void Assign(object data, IStepBody step, IStepExecutionContext context) + { + _action.Invoke((TStepBody)step, (TData)data, context); + } + + public void AssignInput(object data, IStepBody body, IStepExecutionContext context) + { + Assign(data, body, context); + } + + public void AssignOutput(object data, IStepBody body, IStepExecutionContext context) + { + Assign(data, body, context); + } + } +} diff --git a/src/WorkflowCore/Models/ActivityResult.cs b/src/WorkflowCore/Models/ActivityResult.cs new file mode 100644 index 000000000..f3a828a08 --- /dev/null +++ b/src/WorkflowCore/Models/ActivityResult.cs @@ -0,0 +1,13 @@ +using System; + +namespace WorkflowCore.Models +{ + + public class ActivityResult + { + public enum StatusType { Success, Fail } + public StatusType Status { get; set; } + public string SubscriptionId { get; set; } + public object Data { get; set; } + } +} diff --git a/src/WorkflowCore/Models/DataMapping.cs b/src/WorkflowCore/Models/DataMapping.cs deleted file mode 100644 index b67daf23f..000000000 --- a/src/WorkflowCore/Models/DataMapping.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Linq.Expressions; - -namespace WorkflowCore.Models -{ - public class DataMapping - { - public LambdaExpression Source { get; set; } - - public LambdaExpression Target { get; set; } - } -} diff --git a/src/WorkflowCore/Models/Event.cs b/src/WorkflowCore/Models/Event.cs index 42dff60d6..5db6b4987 100644 --- a/src/WorkflowCore/Models/Event.cs +++ b/src/WorkflowCore/Models/Event.cs @@ -15,5 +15,7 @@ public class Event public DateTime EventTime { get; set; } public bool IsProcessed { get; set; } + + public const string EventTypeActivity = "WorkflowCore.Activity"; } } diff --git a/src/WorkflowCore/Models/EventSubscription.cs b/src/WorkflowCore/Models/EventSubscription.cs index 593f1e3a5..a5661987b 100644 --- a/src/WorkflowCore/Models/EventSubscription.cs +++ b/src/WorkflowCore/Models/EventSubscription.cs @@ -10,10 +10,20 @@ public class EventSubscription public int StepId { get; set; } + public string ExecutionPointerId { get; set; } + public string EventName { get; set; } public string EventKey { get; set; } public DateTime SubscribeAsOf { get; set; } + + public object SubscriptionData { get; set; } + + public string ExternalToken { get; set; } + + public string ExternalWorkerId { get; set; } + + public DateTime? ExternalTokenExpiry { get; set; } } } diff --git a/src/WorkflowCore/Models/ExecutionPointer.cs b/src/WorkflowCore/Models/ExecutionPointer.cs index 0bb15fbee..6c12afda7 100644 --- a/src/WorkflowCore/Models/ExecutionPointer.cs +++ b/src/WorkflowCore/Models/ExecutionPointer.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; namespace WorkflowCore.Models { public class ExecutionPointer { + private IReadOnlyCollection _scope = new List(); + public string Id { get; set; } public int StepId { get; set; } @@ -25,8 +28,8 @@ public class ExecutionPointer public bool EventPublished { get; set; } - public object EventData { get; set; } - + public object EventData { get; set; } + public Dictionary ExtensionAttributes { get; set; } = new Dictionary(); public string StepName { get; set; } @@ -42,8 +45,12 @@ public class ExecutionPointer public object Outcome { get; set; } public PointerStatus Status { get; set; } = PointerStatus.Legacy; - - public Stack Scope { get; set; } = new Stack(); + + public IReadOnlyCollection Scope + { + get => _scope; + set => _scope = new List(value); + } } public enum PointerStatus @@ -55,6 +62,8 @@ public enum PointerStatus Sleeping = 4, WaitingForEvent = 5, Failed = 6, - Compensated = 7 + Compensated = 7, + Cancelled = 8, + PendingPredecessor = 9 } } diff --git a/src/WorkflowCore/Models/ExecutionPointerCollection.cs b/src/WorkflowCore/Models/ExecutionPointerCollection.cs new file mode 100644 index 000000000..f125db5a8 --- /dev/null +++ b/src/WorkflowCore/Models/ExecutionPointerCollection.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace WorkflowCore.Models +{ + public class ExecutionPointerCollection : ICollection + { + private readonly Dictionary _dictionary = new Dictionary(); + private readonly Dictionary> _scopeMap = new Dictionary>(); + + public ExecutionPointerCollection() + { + } + + public ExecutionPointerCollection(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + public ExecutionPointerCollection(ICollection pointers) + { + foreach (var ptr in pointers) + { + Add(ptr); + } + } + + public IEnumerator GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public ExecutionPointer FindById(string id) + { + if (!_dictionary.ContainsKey(id)) + return null; + + return _dictionary[id]; + } + + public ICollection FindByScope(string stackFrame) + { + if (!_scopeMap.ContainsKey(stackFrame)) + return new List(); + + return _scopeMap[stackFrame]; + } + + public void Add(ExecutionPointer item) + { + _dictionary.Add(item.Id, item); + + foreach (var stackFrame in item.Scope) + { + if (!_scopeMap.ContainsKey(stackFrame)) + _scopeMap.Add(stackFrame, new List()); + _scopeMap[stackFrame].Add(item); + } + } + + public void Clear() + { + _dictionary.Clear(); + _scopeMap.Clear(); + } + + public bool Contains(ExecutionPointer item) + { + return _dictionary.ContainsValue(item); + } + + public void CopyTo(ExecutionPointer[] array, int arrayIndex) + { + _dictionary.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(ExecutionPointer item) + { + foreach (var stackFrame in item.Scope) + { + _scopeMap[stackFrame].Remove(item); + } + + return _dictionary.Remove(item.Id); + } + + public ExecutionPointer Find(Predicate match) + { + return _dictionary.Values.FirstOrDefault(x => match(x)); + } + + public ICollection FindByStatus(PointerStatus status) + { + //TODO: track states in hash table + return _dictionary.Values.Where(x => x.Status == status).ToList(); + } + + public int Count => _dictionary.Count; + public bool IsReadOnly => false; + } +} diff --git a/src/WorkflowCore/Models/ExecutionResult.cs b/src/WorkflowCore/Models/ExecutionResult.cs index 9d6e791f6..14c562802 100644 --- a/src/WorkflowCore/Models/ExecutionResult.cs +++ b/src/WorkflowCore/Models/ExecutionResult.cs @@ -18,6 +18,8 @@ public class ExecutionResult public string EventKey { get; set; } public DateTime EventAsOf { get; set; } + + public object SubscriptionData { get; set; } public List BranchValues { get; set; } = new List(); @@ -33,7 +35,7 @@ public ExecutionResult(object outcome) public static ExecutionResult Outcome(object value) { - return new ExecutionResult() + return new ExecutionResult { Proceed = true, OutcomeValue = value @@ -42,7 +44,7 @@ public static ExecutionResult Outcome(object value) public static ExecutionResult Next() { - return new ExecutionResult() + return new ExecutionResult { Proceed = true, OutcomeValue = null @@ -51,7 +53,7 @@ public static ExecutionResult Next() public static ExecutionResult Persist(object persistenceData) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, PersistenceData = persistenceData @@ -60,7 +62,7 @@ public static ExecutionResult Persist(object persistenceData) public static ExecutionResult Branch(List branches, object persistenceData) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, PersistenceData = persistenceData, @@ -70,7 +72,7 @@ public static ExecutionResult Branch(List branches, object persistenceDa public static ExecutionResult Sleep(TimeSpan duration, object persistenceData) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, SleepFor = duration, @@ -80,7 +82,7 @@ public static ExecutionResult Sleep(TimeSpan duration, object persistenceData) public static ExecutionResult WaitForEvent(string eventName, string eventKey, DateTime effectiveDate) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, EventName = eventName, @@ -88,5 +90,17 @@ public static ExecutionResult WaitForEvent(string eventName, string eventKey, Da EventAsOf = effectiveDate.ToUniversalTime() }; } + + public static ExecutionResult WaitForActivity(string activityName, object subscriptionData, DateTime effectiveDate) + { + return new ExecutionResult + { + Proceed = false, + EventName = Event.EventTypeActivity, + EventKey = activityName, + SubscriptionData = subscriptionData, + EventAsOf = effectiveDate.ToUniversalTime() + }; + } } } diff --git a/src/WorkflowCore/Models/ExpressionOutcome.cs b/src/WorkflowCore/Models/ExpressionOutcome.cs new file mode 100644 index 000000000..6d30998a0 --- /dev/null +++ b/src/WorkflowCore/Models/ExpressionOutcome.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.Interface; + +namespace WorkflowCore.Models +{ + public class ExpressionOutcome : IStepOutcome + { + private readonly Func _func; + + + public int NextStep { get; set; } + + public string Label { get; set; } + + public string ExternalNextStepId { get; set; } + + public ExpressionOutcome(Expression> expression) + { + _func = expression.Compile(); + } + + public bool Matches(ExecutionResult executionResult, object data) + { + return _func((TData)data, executionResult.OutcomeValue); + } + + public bool Matches(object data) + { + return _func((TData)data, null); + } + + } +} diff --git a/src/WorkflowCore/Models/IteratorPersistenceData.cs b/src/WorkflowCore/Models/IteratorPersistenceData.cs new file mode 100644 index 000000000..1d5ee1b8b --- /dev/null +++ b/src/WorkflowCore/Models/IteratorPersistenceData.cs @@ -0,0 +1,7 @@ +namespace WorkflowCore.Models +{ + public class IteratorPersistenceData : ControlPersistenceData + { + public int Index { get; set; } = 0; + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/LifeCycleEvent.cs b/src/WorkflowCore/Models/LifeCycleEvents/LifeCycleEvent.cs new file mode 100755 index 000000000..f72283e9a --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/LifeCycleEvent.cs @@ -0,0 +1,17 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public abstract class LifeCycleEvent + { + public DateTime EventTimeUtc { get; set; } + + public string WorkflowInstanceId { get; set; } + + public string WorkflowDefinitionId { get; set; } + + public int Version { get; set; } + + public string Reference { get; set; } + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs b/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs new file mode 100644 index 000000000..3d44be288 --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs @@ -0,0 +1,11 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class StepCompleted : LifeCycleEvent + { + public string ExecutionPointerId { get; set; } + + public int StepId { get; set; } + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs b/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs new file mode 100644 index 000000000..183dd1a7b --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs @@ -0,0 +1,11 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class StepStarted : LifeCycleEvent + { + public string ExecutionPointerId { get; set; } + + public int StepId { get; set; } + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs new file mode 100644 index 000000000..e83da1827 --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs @@ -0,0 +1,8 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowCompleted : LifeCycleEvent + { + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs new file mode 100644 index 000000000..e24849fcf --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs @@ -0,0 +1,13 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowError : LifeCycleEvent + { + public string Message { get; set; } + + public string ExecutionPointerId { get; set; } + + public int StepId { get; set; } + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs new file mode 100644 index 000000000..1c400b84d --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs @@ -0,0 +1,8 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowResumed : LifeCycleEvent + { + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs new file mode 100644 index 000000000..60b906de8 --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs @@ -0,0 +1,8 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowStarted : LifeCycleEvent + { + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs new file mode 100644 index 000000000..ccadff82d --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs @@ -0,0 +1,8 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowSuspended : LifeCycleEvent + { + } +} diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs new file mode 100644 index 000000000..778c23493 --- /dev/null +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs @@ -0,0 +1,8 @@ +using System; + +namespace WorkflowCore.Models.LifeCycleEvents +{ + public class WorkflowTerminated : LifeCycleEvent + { + } +} diff --git a/src/WorkflowCore/Models/MemberMapParameter.cs b/src/WorkflowCore/Models/MemberMapParameter.cs new file mode 100644 index 000000000..e5273986d --- /dev/null +++ b/src/WorkflowCore/Models/MemberMapParameter.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using WorkflowCore.Interface; + +namespace WorkflowCore.Models +{ + public class MemberMapParameter : IStepParameter + { + private readonly LambdaExpression _source; + private readonly LambdaExpression _target; + + public MemberMapParameter(LambdaExpression source, LambdaExpression target) + { + if (target.Body.NodeType != ExpressionType.MemberAccess) + throw new NotSupportedException(); + + _source = source; + _target = target; + } + + private void Assign(object sourceObject, LambdaExpression sourceExpr, object targetObject, LambdaExpression targetExpr, IStepExecutionContext context) + { + object resolvedValue = null; + + switch (sourceExpr.Parameters.Count) + { + case 1: + resolvedValue = sourceExpr.Compile().DynamicInvoke(sourceObject); + break; + case 2: + resolvedValue = sourceExpr.Compile().DynamicInvoke(sourceObject, context); + break; + default: + throw new ArgumentException(); + } + + if (resolvedValue == null) + { + var defaultAssign = Expression.Lambda(Expression.Assign(targetExpr.Body, Expression.Default(targetExpr.ReturnType)), targetExpr.Parameters.Single()); + defaultAssign.Compile().DynamicInvoke(targetObject); + return; + } + + var valueExpr = Expression.Convert(Expression.Constant(resolvedValue), targetExpr.ReturnType); + var assign = Expression.Lambda(Expression.Assign(targetExpr.Body, valueExpr), targetExpr.Parameters.Single()); + assign.Compile().DynamicInvoke(targetObject); + } + + public void AssignInput(object data, IStepBody body, IStepExecutionContext context) + { + Assign(data, _source, body, _target, context); + } + + public void AssignOutput(object data, IStepBody body, IStepExecutionContext context) + { + Assign(body, _source, data, _target, context); + } + } +} diff --git a/src/WorkflowCore/Models/ScheduledCommand.cs b/src/WorkflowCore/Models/ScheduledCommand.cs new file mode 100644 index 000000000..de3dcc5dd --- /dev/null +++ b/src/WorkflowCore/Models/ScheduledCommand.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WorkflowCore.Models +{ + public class ScheduledCommand + { + public const string ProcessWorkflow = "ProcessWorkflow"; + public const string ProcessEvent = "ProcessEvent"; + + public string CommandName { get; set; } + public string Data { get; set; } + public long ExecuteTime { get; set; } + } +} diff --git a/src/WorkflowCore/Models/Search/Page.cs b/src/WorkflowCore/Models/Search/Page.cs new file mode 100644 index 000000000..412688a28 --- /dev/null +++ b/src/WorkflowCore/Models/Search/Page.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.Models.Search +{ + public class Page + { + public ICollection Data { get; set; } + public long Total { get; set; } + } +} diff --git a/src/WorkflowCore/Models/Search/SearchFilter.cs b/src/WorkflowCore/Models/Search/SearchFilter.cs new file mode 100644 index 000000000..e7491806c --- /dev/null +++ b/src/WorkflowCore/Models/Search/SearchFilter.cs @@ -0,0 +1,151 @@ +using System; +using System.Linq.Expressions; + +namespace WorkflowCore.Models.Search +{ + public abstract class SearchFilter + { + public bool IsData { get; set; } + public Type DataType { get; set; } + public Expression Property { get; set; } + + static Func Lambda(Func del) + { + return del; + } + } + + public class ScalarFilter : SearchFilter + { + public object Value { get; set; } + + public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter + { + Property = property, + Value = value + }; + + public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + Value = value + }; + } + + public class DateRangeFilter : SearchFilter + { + public DateTime? BeforeValue { get; set; } + public DateTime? AfterValue { get; set; } + + public static DateRangeFilter Before(Expression> property, DateTime value) => new DateRangeFilter + { + Property = property, + BeforeValue = value + }; + + public static DateRangeFilter After(Expression> property, DateTime value) => new DateRangeFilter + { + Property = property, + AfterValue = value + }; + + public static DateRangeFilter Between(Expression> property, DateTime start, DateTime end) => new DateRangeFilter + { + Property = property, + BeforeValue = end, + AfterValue = start + }; + + public static DateRangeFilter Before(Expression> property, DateTime value) => new DateRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + BeforeValue = value + }; + + public static DateRangeFilter After(Expression> property, DateTime value) => new DateRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + AfterValue = value + }; + + public static DateRangeFilter Between(Expression> property, DateTime start, DateTime end) => new DateRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + BeforeValue = end, + AfterValue = start + }; + } + + public class NumericRangeFilter : SearchFilter + { + public double? LessValue { get; set; } + public double? GreaterValue { get; set; } + + public static NumericRangeFilter LessThan(Expression> property, double value) => new NumericRangeFilter + { + Property = property, + LessValue = value + }; + + public static NumericRangeFilter GreaterThan(Expression> property, double value) => new NumericRangeFilter + { + Property = property, + GreaterValue = value + }; + + public static NumericRangeFilter Between(Expression> property, double start, double end) => new NumericRangeFilter + { + Property = property, + LessValue = end, + GreaterValue = start + }; + + public static NumericRangeFilter LessThan(Expression> property, double value) => new NumericRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + LessValue = value + }; + + public static NumericRangeFilter GreaterThan(Expression> property, double value) => new NumericRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + GreaterValue = value + }; + + public static NumericRangeFilter Between(Expression> property, double start, double end) => new NumericRangeFilter + { + IsData = true, + DataType = typeof(T), + Property = property, + LessValue = end, + GreaterValue = start + }; + } + + public class StatusFilter : ScalarFilter + { + protected StatusFilter() + { + Expression> lambda = (WorkflowSearchResult x) => x.Status; + Property = lambda; + } + + public static StatusFilter Equals(WorkflowStatus value) => new StatusFilter + { + Value = value.ToString() + }; + } + +} diff --git a/src/WorkflowCore/Models/Search/StepInfo.cs b/src/WorkflowCore/Models/Search/StepInfo.cs new file mode 100644 index 000000000..6b9a97d8f --- /dev/null +++ b/src/WorkflowCore/Models/Search/StepInfo.cs @@ -0,0 +1,11 @@ +using System; + +namespace WorkflowCore.Models.Search +{ + public class StepInfo + { + public int StepId { get; set; } + + public string Name { get; set; } + } +} diff --git a/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs b/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs new file mode 100644 index 000000000..1cb6a3c9a --- /dev/null +++ b/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace WorkflowCore.Models.Search +{ + public class WorkflowSearchResult + { + public string Id { get; set; } + + public string WorkflowDefinitionId { get; set; } + + public int Version { get; set; } + + public string Description { get; set; } + + public string Reference { get; set; } + + public DateTime? NextExecutionUtc { get; set; } + + public WorkflowStatus Status { get; set; } + + public object Data { get; set; } + + public DateTime CreateTime { get; set; } + + public DateTime? CompleteTime { get; set; } + + public ICollection WaitingSteps { get; set; } = new HashSet(); + + public ICollection SleepingSteps { get; set; } = new HashSet(); + + public ICollection FailedSteps { get; set; } = new HashSet(); + + + } + + public class WorkflowSearchResult : WorkflowSearchResult + { + public new TData Data { get; set; } + } + +} diff --git a/src/WorkflowCore/Models/StepBody.cs b/src/WorkflowCore/Models/StepBody.cs index 5ec927528..bb495c7a5 100644 --- a/src/WorkflowCore/Models/StepBody.cs +++ b/src/WorkflowCore/Models/StepBody.cs @@ -15,7 +15,7 @@ public Task RunAsync(IStepExecutionContext context) protected ExecutionResult OutcomeResult(object value) { - return new ExecutionResult() + return new ExecutionResult { Proceed = true, OutcomeValue = value @@ -24,7 +24,7 @@ protected ExecutionResult OutcomeResult(object value) protected ExecutionResult PersistResult(object persistenceData) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, PersistenceData = persistenceData @@ -33,7 +33,7 @@ protected ExecutionResult PersistResult(object persistenceData) protected ExecutionResult SleepResult(object persistenceData, TimeSpan sleep) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, PersistenceData = persistenceData, diff --git a/src/WorkflowCore/Models/StepExecutionContext.cs b/src/WorkflowCore/Models/StepExecutionContext.cs index b48bcd35c..096d719f3 100644 --- a/src/WorkflowCore/Models/StepExecutionContext.cs +++ b/src/WorkflowCore/Models/StepExecutionContext.cs @@ -1,4 +1,5 @@ using WorkflowCore.Interface; +using System.Threading; namespace WorkflowCore.Models { @@ -13,5 +14,7 @@ public class StepExecutionContext : IStepExecutionContext public object PersistenceData { get; set; } public object Item { get; set; } + + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; } } diff --git a/src/WorkflowCore/Models/StepOutcome.cs b/src/WorkflowCore/Models/StepOutcome.cs deleted file mode 100644 index 3c5b32dcb..000000000 --- a/src/WorkflowCore/Models/StepOutcome.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Linq.Expressions; - -namespace WorkflowCore.Models -{ - public class StepOutcome - { - private Expression> _value; - - public Expression> Value - { - set { _value = value; } - } - - public int NextStep { get; set; } - - public string Label { get; set; } - - public string Tag { get; set; } - - public object GetValue(object data) - { - if (_value == null) - return null; - - return _value.Compile().Invoke(data); - } - } -} diff --git a/src/WorkflowCore/Models/ValueOutcome.cs b/src/WorkflowCore/Models/ValueOutcome.cs new file mode 100644 index 000000000..a6e4f2fb2 --- /dev/null +++ b/src/WorkflowCore/Models/ValueOutcome.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq.Expressions; +using WorkflowCore.Interface; + +namespace WorkflowCore.Models +{ + public class ValueOutcome : IStepOutcome + { + private LambdaExpression _value; + + public LambdaExpression Value + { + set { _value = value; } + } + + public int NextStep { get; set; } + + public string Label { get; set; } + + public string ExternalNextStepId { get; set; } + + public bool Matches(ExecutionResult executionResult, object data) + { + return object.Equals(GetValue(data), executionResult.OutcomeValue) || GetValue(data) == null; + } + + public bool Matches(object data) + { + return GetValue(data) == null; + } + + public object GetValue(object data) + { + if (_value == null) + return null; + + return _value.Compile().DynamicInvoke(data); + } + + } +} diff --git a/src/WorkflowCore/Models/WorkflowDefinition.cs b/src/WorkflowCore/Models/WorkflowDefinition.cs index df0695087..d40fc5a2e 100644 --- a/src/WorkflowCore/Models/WorkflowDefinition.cs +++ b/src/WorkflowCore/Models/WorkflowDefinition.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace WorkflowCore.Models @@ -12,20 +11,23 @@ public class WorkflowDefinition public string Description { get; set; } - public List Steps { get; set; } + public WorkflowStepCollection Steps { get; set; } = new WorkflowStepCollection(); public Type DataType { get; set; } public WorkflowErrorHandling DefaultErrorBehavior { get; set; } - public TimeSpan? DefaultErrorRetryInterval { get; set; } + public Type OnPostMiddlewareError { get; set; } + public Type OnExecuteMiddlewareError { get; set; } + + public TimeSpan? DefaultErrorRetryInterval { get; set; } } - public enum WorkflowErrorHandling - { - Retry = 0, - Suspend = 1, + public enum WorkflowErrorHandling + { + Retry = 0, + Suspend = 1, Terminate = 2, Compensate = 3 } diff --git a/src/WorkflowCore/Models/WorkflowDelegate.cs b/src/WorkflowCore/Models/WorkflowDelegate.cs new file mode 100644 index 000000000..2d8876163 --- /dev/null +++ b/src/WorkflowCore/Models/WorkflowDelegate.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace WorkflowCore.Models +{ + /// + /// Represents a function that executes before or after a workflow starts or completes. + /// + public delegate Task WorkflowDelegate(); +} diff --git a/src/WorkflowCore/Models/WorkflowInstance.cs b/src/WorkflowCore/Models/WorkflowInstance.cs index 402dbc317..71f1e3c28 100644 --- a/src/WorkflowCore/Models/WorkflowInstance.cs +++ b/src/WorkflowCore/Models/WorkflowInstance.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Linq; namespace WorkflowCore.Models { @@ -15,7 +15,7 @@ public class WorkflowInstance public string Reference { get; set; } - public List ExecutionPointers { get; set; } = new List(); + public ExecutionPointerCollection ExecutionPointers { get; set; } = new ExecutionPointerCollection(); public long? NextExecution { get; set; } @@ -25,15 +25,21 @@ public class WorkflowInstance public DateTime CreateTime { get; set; } - public DateTime? CompleteTime { get; set; } + public DateTime? CompleteTime { get; set; } + public bool IsBranchComplete(string parentId) + { + return ExecutionPointers + .FindByScope(parentId) + .All(x => x.EndTime != null); + } } - public enum WorkflowStatus + public enum WorkflowStatus { - Runnable = 0, - Suspended = 1, - Complete = 2, - Terminated = 3 + Runnable = 0, + Suspended = 1, + Complete = 2, + Terminated = 3, } } diff --git a/src/WorkflowCore/Models/WorkflowOptions.cs b/src/WorkflowCore/Models/WorkflowOptions.cs index 71dcb8668..8913c32c2 100644 --- a/src/WorkflowCore/Models/WorkflowOptions.cs +++ b/src/WorkflowCore/Models/WorkflowOptions.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using System; +using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Services; @@ -9,21 +11,35 @@ public class WorkflowOptions internal Func PersistanceFactory; internal Func QueueFactory; internal Func LockFactory; + internal Func EventHubFactory; + internal Func SearchIndexFactory; internal TimeSpan PollInterval; internal TimeSpan IdleTime; - internal TimeSpan ErrorRetryInterval; + internal TimeSpan ErrorRetryInterval; + internal int MaxConcurrentWorkflows = Math.Max(Environment.ProcessorCount, 4); - public WorkflowOptions() + public IServiceCollection Services { get; private set; } + + public WorkflowOptions(IServiceCollection services) { + Services = services; PollInterval = TimeSpan.FromSeconds(10); IdleTime = TimeSpan.FromMilliseconds(100); - ErrorRetryInterval = TimeSpan.FromSeconds(60); + ErrorRetryInterval = TimeSpan.FromSeconds(60); QueueFactory = new Func(sp => new SingleNodeQueueProvider()); LockFactory = new Func(sp => new SingleNodeLockProvider()); - PersistanceFactory = new Func(sp => new MemoryPersistenceProvider()); + PersistanceFactory = new Func(sp => new TransientMemoryPersistenceProvider(sp.GetService())); + SearchIndexFactory = new Func(sp => new NullSearchIndex()); + EventHubFactory = new Func(sp => new SingleNodeEventHub(sp.GetService())); } + public bool EnableWorkflows { get; set; } = true; + public bool EnableEvents { get; set; } = true; + public bool EnableIndexes { get; set; } = true; + public bool EnablePolling { get; set; } = true; + public bool EnableLifeCycleEventsPublisher { get; set; } = true; + public void UsePersistence(Func factory) { PersistanceFactory = factory; @@ -39,6 +55,16 @@ public void UseQueueProvider(Func factory) QueueFactory = factory; } + public void UseEventHub(Func factory) + { + EventHubFactory = factory; + } + + public void UseSearchIndex(Func factory) + { + SearchIndexFactory = factory; + } + public void UsePollInterval(TimeSpan interval) { PollInterval = interval; @@ -48,6 +74,16 @@ public void UseErrorRetryInterval(TimeSpan interval) { ErrorRetryInterval = interval; } + + public void UseIdleTime(TimeSpan interval) + { + IdleTime = interval; + } + + public void UseMaxConcurrentWorkflows(int maxConcurrentWorkflows) + { + MaxConcurrentWorkflows = maxConcurrentWorkflows; + } } } diff --git a/src/WorkflowCore/Models/WorkflowStep.cs b/src/WorkflowCore/Models/WorkflowStep.cs index 6d9bb012a..814219916 100644 --- a/src/WorkflowCore/Models/WorkflowStep.cs +++ b/src/WorkflowCore/Models/WorkflowStep.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Reflection; +using System.Linq.Expressions; using WorkflowCore.Interface; namespace WorkflowCore.Models @@ -9,30 +9,34 @@ public abstract class WorkflowStep { public abstract Type BodyType { get; } - public int Id { get; set; } + public virtual int Id { get; set; } - public string Name { get; set; } + public virtual string Name { get; set; } - public string Tag { get; set; } + public virtual string ExternalId { get; set; } - public List Children { get; set; } = new List(); + public virtual List Children { get; set; } = new List(); - public List Outcomes { get; set; } = new List(); + public virtual List Outcomes { get; set; } = new List(); - public List Inputs { get; set; } = new List(); + public virtual List Inputs { get; set; } = new List(); - public List Outputs { get; set; } = new List(); + public virtual List Outputs { get; set; } = new List(); - public WorkflowErrorHandling? ErrorBehavior { get; set; } + public virtual WorkflowErrorHandling? ErrorBehavior { get; set; } - public TimeSpan? RetryInterval { get; set; } + public virtual TimeSpan? RetryInterval { get; set; } - public int? CompensationStepId { get; set; } + public virtual int? CompensationStepId { get; set; } public virtual bool ResumeChildrenAfterCompensation => true; public virtual bool RevertChildrenAfterCompensation => false; + public virtual LambdaExpression CancelCondition { get; set; } + + public bool ProceedOnCancel { get; set; } = false; + public virtual ExecutionPipelineDirective InitForExecution(WorkflowExecutorResult executorResult, WorkflowDefinition defintion, WorkflowInstance workflow, ExecutionPointer executionPointer) { return ExecutionPipelineDirective.Next; @@ -61,6 +65,7 @@ public virtual void PrimeForRetry(ExecutionPointer pointer) /// public virtual void AfterWorkflowIteration(WorkflowExecutorResult executorResult, WorkflowDefinition defintion, WorkflowInstance workflow, ExecutionPointer executionPointer) { + } public virtual IStepBody ConstructBody(IServiceProvider serviceProvider) diff --git a/src/WorkflowCore/Models/WorkflowStepCollection.cs b/src/WorkflowCore/Models/WorkflowStepCollection.cs new file mode 100644 index 000000000..68346620e --- /dev/null +++ b/src/WorkflowCore/Models/WorkflowStepCollection.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace WorkflowCore.Models +{ + public class WorkflowStepCollection : ICollection + { + private readonly Dictionary _dictionary = new Dictionary(); + + public WorkflowStepCollection() + { + } + + public WorkflowStepCollection(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + public WorkflowStepCollection(ICollection steps) + { + foreach (var step in steps) + { + Add(step); + } + } + + public IEnumerator GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public WorkflowStep FindById(int id) + { + if (!_dictionary.ContainsKey(id)) + return null; + + return _dictionary[id]; + } + + public void Add(WorkflowStep item) + { + _dictionary.Add(item.Id, item); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(WorkflowStep item) + { + return _dictionary.ContainsValue(item); + } + + public void CopyTo(WorkflowStep[] array, int arrayIndex) + { + _dictionary.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(WorkflowStep item) + { + return _dictionary.Remove(item.Id); + } + + public WorkflowStep Find(Predicate match) + { + return _dictionary.Values.FirstOrDefault(x => match(x)); + } + + public int Count => _dictionary.Count; + public bool IsReadOnly => false; + } +} diff --git a/src/WorkflowCore/Models/WorkflowStepDelegate.cs b/src/WorkflowCore/Models/WorkflowStepDelegate.cs new file mode 100644 index 000000000..21befb575 --- /dev/null +++ b/src/WorkflowCore/Models/WorkflowStepDelegate.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace WorkflowCore.Models +{ + /// + /// Represents a function that executes a workflow step and returns a result. + /// + public delegate Task WorkflowStepDelegate(); +} diff --git a/src/WorkflowCore/Primitives/Activity.cs b/src/WorkflowCore/Primitives/Activity.cs new file mode 100644 index 000000000..4f0ebb0c1 --- /dev/null +++ b/src/WorkflowCore/Primitives/Activity.cs @@ -0,0 +1,52 @@ +using System; +using WorkflowCore.Exceptions; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Primitives +{ + public class Activity : StepBody + { + public string ActivityName { get; set; } + + public DateTime EffectiveDate { get; set; } + + public object Parameters { get; set; } + + public object Result { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + if (!context.ExecutionPointer.EventPublished) + { + DateTime effectiveDate = DateTime.MinValue; + + if (EffectiveDate != null) + { + effectiveDate = EffectiveDate; + } + + return ExecutionResult.WaitForActivity(ActivityName, Parameters, effectiveDate); + } + + if (context.ExecutionPointer.EventData is ActivityResult) + { + var actResult = (context.ExecutionPointer.EventData as ActivityResult); + if (actResult.Status == ActivityResult.StatusType.Success) + { + Result = actResult.Data; + } + else + { + throw new ActivityFailedException(actResult.Data); + } + } + else + { + Result = context.ExecutionPointer.EventData; + } + + return ExecutionResult.Next(); + } + } +} diff --git a/src/WorkflowCore/Primitives/CancellableStep.cs b/src/WorkflowCore/Primitives/CancellableStep.cs deleted file mode 100644 index 8c4a960eb..000000000 --- a/src/WorkflowCore/Primitives/CancellableStep.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Linq.Expressions; -using WorkflowCore.Interface; -using WorkflowCore.Models; - -namespace WorkflowCore.Primitives -{ - public class CancellableStep : WorkflowStep - where TStepBody : IStepBody - { - private readonly Expression> _cancelCondition; - - public CancellableStep(Expression> cancelCondition) - { - _cancelCondition = cancelCondition; - } - - public override void AfterWorkflowIteration(WorkflowExecutorResult executorResult, WorkflowDefinition defintion, WorkflowInstance workflow, ExecutionPointer executionPointer) - { - base.AfterWorkflowIteration(executorResult, defintion, workflow, executionPointer); - var func = _cancelCondition.Compile(); - if (func((TData) workflow.Data)) - { - executionPointer.EndTime = DateTime.Now.ToUniversalTime(); - executionPointer.Active = false; - } - } - } -} diff --git a/src/WorkflowCore/Primitives/ContainerStepBody.cs b/src/WorkflowCore/Primitives/ContainerStepBody.cs index 68da80ac9..46da6a0b0 100644 --- a/src/WorkflowCore/Primitives/ContainerStepBody.cs +++ b/src/WorkflowCore/Primitives/ContainerStepBody.cs @@ -1,31 +1,10 @@ using System.Linq; -using System.Collections.Generic; using WorkflowCore.Models; namespace WorkflowCore.Primitives { public abstract class ContainerStepBody : StepBody - { - protected bool IsBranchComplete(IEnumerable pointers, string rootId) - { - //TODO: move to own class - var root = pointers.First(x => x.Id == rootId); - - if (root.EndTime == null) - { - return false; - } - - var list = pointers.Where(x => x.PredecessorId == rootId).ToList(); - - bool result = true; - - foreach (var item in list) - { - result = result && IsBranchComplete(pointers, item.Id); - } - - return result; - } + { + } } diff --git a/src/WorkflowCore/Primitives/Decide.cs b/src/WorkflowCore/Primitives/Decide.cs new file mode 100644 index 000000000..ec33ccaf9 --- /dev/null +++ b/src/WorkflowCore/Primitives/Decide.cs @@ -0,0 +1,16 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Primitives +{ + public class Decide : StepBody + { + public object Expression { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + return ExecutionResult.Outcome(Expression); + } + } +} diff --git a/src/WorkflowCore/Primitives/Foreach.cs b/src/WorkflowCore/Primitives/Foreach.cs index 9551ef871..ef4c39fe0 100644 --- a/src/WorkflowCore/Primitives/Foreach.cs +++ b/src/WorkflowCore/Primitives/Foreach.cs @@ -8,34 +8,58 @@ namespace WorkflowCore.Primitives { public class Foreach : ContainerStepBody { - public IEnumerable Collection { get; set; } + public IEnumerable Collection { get; set; } + public bool RunParallel { get; set; } = true; public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData == null) { - var values = Collection.Cast(); - return ExecutionResult.Branch(new List(values), new ControlPersistenceData() { ChildrenActive = true }); + var values = Collection.Cast().ToList(); + if (!values.Any()) + { + return ExecutionResult.Next(); + } + + if (RunParallel) + { + return ExecutionResult.Branch(new List(values), new IteratorPersistenceData { ChildrenActive = true }); + } + else + { + return ExecutionResult.Branch(new List(new object[] { values.ElementAt(0) }), new IteratorPersistenceData { ChildrenActive = true }); + } } - if (context.PersistenceData is ControlPersistenceData) + if (context.PersistenceData is IteratorPersistenceData persistenceData && persistenceData?.ChildrenActive == true) { - if ((context.PersistenceData as ControlPersistenceData).ChildrenActive) + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) + if (!RunParallel) { - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); + var values = Collection.Cast(); + persistenceData.Index++; + if (persistenceData.Index < values.Count()) + { + return ExecutionResult.Branch(new List(new object[] { values.ElementAt(persistenceData.Index) }), persistenceData); + } } - if (complete) - { - return ExecutionResult.Next(); - } + return ExecutionResult.Next(); + } + + return ExecutionResult.Persist(persistenceData); + } + + if (context.PersistenceData is ControlPersistenceData controlPersistenceData && controlPersistenceData?.ChildrenActive == true) + { + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) + { + return ExecutionResult.Next(); } } return ExecutionResult.Persist(context.PersistenceData); - } + } } } diff --git a/src/WorkflowCore/Primitives/If.cs b/src/WorkflowCore/Primitives/If.cs index ba7f27820..b3950bf7c 100644 --- a/src/WorkflowCore/Primitives/If.cs +++ b/src/WorkflowCore/Primitives/If.cs @@ -15,19 +15,15 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (Condition) { - return ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + return ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); } return ExecutionResult.Next(); } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) - { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - - if (complete) + { + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { return ExecutionResult.Next(); } diff --git a/src/WorkflowCore/Primitives/OutcomeSwitch.cs b/src/WorkflowCore/Primitives/OutcomeSwitch.cs index 0ee1d5bda..53cdc4729 100644 --- a/src/WorkflowCore/Primitives/OutcomeSwitch.cs +++ b/src/WorkflowCore/Primitives/OutcomeSwitch.cs @@ -12,20 +12,14 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData == null) { - var result = ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + var result = ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); result.OutcomeValue = GetPreviousOutcome(context); return result; } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) - { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - { - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - } - - if (complete) + { + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { return ExecutionResult.Next(); } @@ -42,7 +36,7 @@ public override ExecutionResult Run(IStepExecutionContext context) private object GetPreviousOutcome(IStepExecutionContext context) { - var prevPointer = context.Workflow.ExecutionPointers.First(x => x.Id == context.ExecutionPointer.PredecessorId); + var prevPointer = context.Workflow.ExecutionPointers.FindById(context.ExecutionPointer.PredecessorId); return prevPointer.Outcome; } } diff --git a/src/WorkflowCore/Primitives/Recur.cs b/src/WorkflowCore/Primitives/Recur.cs index ccc41e7d0..4b6189fb5 100644 --- a/src/WorkflowCore/Primitives/Recur.cs +++ b/src/WorkflowCore/Primitives/Recur.cs @@ -18,10 +18,10 @@ public override ExecutionResult Run(IStepExecutionContext context) return ExecutionResult.Next(); } - return new ExecutionResult() + return new ExecutionResult { Proceed = false, - BranchValues = new List() { null }, + BranchValues = new List { null }, SleepFor = Interval }; } diff --git a/src/WorkflowCore/Primitives/SagaContainer.cs b/src/WorkflowCore/Primitives/SagaContainer.cs index 2034f6bd4..93470cdbb 100644 --- a/src/WorkflowCore/Primitives/SagaContainer.cs +++ b/src/WorkflowCore/Primitives/SagaContainer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using WorkflowCore.Exceptions; -using WorkflowCore.Interface; +using WorkflowCore.Interface; using WorkflowCore.Models; namespace WorkflowCore.Primitives diff --git a/src/WorkflowCore/Primitives/Schedule.cs b/src/WorkflowCore/Primitives/Schedule.cs index 856187a53..7eb63c8c1 100644 --- a/src/WorkflowCore/Primitives/Schedule.cs +++ b/src/WorkflowCore/Primitives/Schedule.cs @@ -13,24 +13,17 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData == null) { - return ExecutionResult.Sleep(Interval, new SchedulePersistenceData() { Elapsed = false }); + return ExecutionResult.Sleep(Interval, new SchedulePersistenceData { Elapsed = false }); } if (context.PersistenceData is SchedulePersistenceData) { if (!((SchedulePersistenceData)context.PersistenceData).Elapsed) { - return ExecutionResult.Branch(new List() { null }, new SchedulePersistenceData() { Elapsed = true }); + return ExecutionResult.Branch(new List { context.Item }, new SchedulePersistenceData { Elapsed = true }); } - - var complete = true; - - foreach (var childId in context.ExecutionPointer.Children) - { - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - } - - if (complete) + + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { return ExecutionResult.Next(); } diff --git a/src/WorkflowCore/Primitives/Sequence.cs b/src/WorkflowCore/Primitives/Sequence.cs index 66d543333..d3e0b827d 100644 --- a/src/WorkflowCore/Primitives/Sequence.cs +++ b/src/WorkflowCore/Primitives/Sequence.cs @@ -11,16 +11,12 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData == null) { - return ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + return ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - - if (complete) + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { return ExecutionResult.Next(); } diff --git a/src/WorkflowCore/Primitives/WaitFor.cs b/src/WorkflowCore/Primitives/WaitFor.cs index e2a8a673f..7f84be2cd 100644 --- a/src/WorkflowCore/Primitives/WaitFor.cs +++ b/src/WorkflowCore/Primitives/WaitFor.cs @@ -20,7 +20,6 @@ public override ExecutionResult Run(IStepExecutionContext context) { DateTime effectiveDate = DateTime.MinValue; - // TODO: This will always execute. if (EffectiveDate != null) { effectiveDate = EffectiveDate; diff --git a/src/WorkflowCore/Primitives/When.cs b/src/WorkflowCore/Primitives/When.cs index f70c113b0..a2ab32252 100644 --- a/src/WorkflowCore/Primitives/When.cs +++ b/src/WorkflowCore/Primitives/When.cs @@ -25,18 +25,12 @@ public override ExecutionResult Run(IStepExecutionContext context) if (context.PersistenceData == null) { - return ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + return ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) - { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - { - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - } - - if (complete) + { + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) { return ExecutionResult.Next(); } diff --git a/src/WorkflowCore/Primitives/While.cs b/src/WorkflowCore/Primitives/While.cs index fc2b5c906..7716ed0b0 100644 --- a/src/WorkflowCore/Primitives/While.cs +++ b/src/WorkflowCore/Primitives/While.cs @@ -15,26 +15,18 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (Condition) { - return ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + return ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); } return ExecutionResult.Next(); } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) - { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - { - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - } - - if (complete) - { - return ExecutionResult.Persist(null); - } - - return ExecutionResult.Persist(context.PersistenceData); + { + if (!context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) + return ExecutionResult.Persist(context.PersistenceData); + + return ExecutionResult.Persist(null); //re-evaluate condition on next pass } throw new CorruptPersistenceDataException(); diff --git a/src/WorkflowCore/Properties/AssemblyInfo.cs b/src/WorkflowCore/Properties/AssemblyInfo.cs index d692f8bbb..2d7f37b38 100644 --- a/src/WorkflowCore/Properties/AssemblyInfo.cs +++ b/src/WorkflowCore/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/WorkflowCore/ServiceCollectionExtensions.cs b/src/WorkflowCore/ServiceCollectionExtensions.cs index 203ad535d..760a89d41 100644 --- a/src/WorkflowCore/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore/ServiceCollectionExtensions.cs @@ -1,41 +1,75 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Services; using WorkflowCore.Models; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using WorkflowCore.Primitives; using WorkflowCore.Services.BackgroundTasks; -using WorkflowCore.Services.DefinitionStorage; +using WorkflowCore.Services.ErrorHandlers; namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static void AddWorkflow(this IServiceCollection services, Action setupAction = null) + public static IServiceCollection AddWorkflow(this IServiceCollection services, Action setupAction = null) { if (services.Any(x => x.ServiceType == typeof(WorkflowOptions))) throw new InvalidOperationException("Workflow services already registered"); - var options = new WorkflowOptions(); + var options = new WorkflowOptions(services); setupAction?.Invoke(options); + services.AddSingleton(); services.AddTransient(options.PersistanceFactory); + services.AddTransient(options.PersistanceFactory); + services.AddTransient(options.PersistanceFactory); + services.AddTransient(options.PersistanceFactory); services.AddSingleton(options.QueueFactory); services.AddSingleton(options.LockFactory); + services.AddSingleton(options.EventHubFactory); + services.AddSingleton(options.SearchIndexFactory); + services.AddSingleton(); services.AddSingleton(options); + services.AddSingleton(); + + if (options.EnableWorkflows) + { + services.AddTransient(); + } + + if (options.EnableEvents) + { + services.AddTransient(); + } + + if (options.EnableIndexes) + { + services.AddTransient(); + } + + if (options.EnablePolling) + { + services.AddTransient(); + } - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(sp => sp.GetService()); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -44,10 +78,46 @@ public static void AddWorkflow(this IServiceCollection services, Action, InjectedObjectPoolPolicy>(); services.AddTransient, InjectedObjectPoolPolicy>(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); + + return services; } + + /// + /// Adds a middleware that will run around the execution of a workflow step. + /// + /// The services collection. + /// Optionally configure using your own factory. + /// The type of middleware. + /// It must implement . + /// The services collection for chaining. + public static IServiceCollection AddWorkflowStepMiddleware( + this IServiceCollection services, + Func factory = null) + where TMiddleware : class, IWorkflowStepMiddleware => + factory == null + ? services.AddTransient() + : services.AddTransient(factory); + + /// + /// Adds a middleware that will run either before a workflow is kicked off or after + /// a workflow completes. Specify the phase of the workflow execution process that + /// you want to execute this middleware using . + /// + /// The services collection. + /// Optionally configure using your own factory. + /// The type of middleware. + /// It must implement . + /// The services collection for chaining. + public static IServiceCollection AddWorkflowMiddleware( + this IServiceCollection services, + Func factory = null) + where TMiddleware : class, IWorkflowMiddleware => + factory == null + ? services.AddTransient() + : services.AddTransient(factory); } } diff --git a/src/WorkflowCore/Services/ActivityController.cs b/src/WorkflowCore/Services/ActivityController.cs new file mode 100644 index 000000000..e37481521 --- /dev/null +++ b/src/WorkflowCore/Services/ActivityController.cs @@ -0,0 +1,138 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using WorkflowCore.Exceptions; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + public class ActivityController : IActivityController + { + private readonly ISubscriptionRepository _subscriptionRepository; + private readonly IDistributedLockProvider _lockProvider; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IWorkflowController _workflowController; + + public ActivityController(ISubscriptionRepository subscriptionRepository, IWorkflowController workflowController, IDateTimeProvider dateTimeProvider, IDistributedLockProvider lockProvider) + { + _subscriptionRepository = subscriptionRepository; + _dateTimeProvider = dateTimeProvider; + _lockProvider = lockProvider; + _workflowController = workflowController; + } + + public async Task GetPendingActivity(string activityName, string workerId, TimeSpan? timeout = null) + { + var endTime = _dateTimeProvider.UtcNow.Add(timeout ?? TimeSpan.Zero); + var firstPass = true; + EventSubscription subscription = null; + while ((subscription == null && _dateTimeProvider.UtcNow < endTime) || firstPass) + { + if (!firstPass) + await Task.Delay(100); + subscription = await _subscriptionRepository.GetFirstOpenSubscription(Event.EventTypeActivity, activityName, _dateTimeProvider.Now); + if (subscription != null) + if (!await _lockProvider.AcquireLock($"sub:{subscription.Id}", CancellationToken.None)) + subscription = null; + firstPass = false; + } + if (subscription == null) + return null; + + try + { + var token = Token.Create(subscription.Id, subscription.EventKey); + var result = new PendingActivity + { + Token = token.Encode(), + ActivityName = subscription.EventKey, + Parameters = subscription.SubscriptionData, + TokenExpiry = DateTime.MaxValue + }; + + if (!await _subscriptionRepository.SetSubscriptionToken(subscription.Id, result.Token, workerId, result.TokenExpiry)) + return null; + + return result; + } + finally + { + await _lockProvider.ReleaseLock($"sub:{subscription.Id}"); + } + + } + + public async Task ReleaseActivityToken(string token) + { + var tokenObj = Token.Decode(token); + await _subscriptionRepository.ClearSubscriptionToken(tokenObj.SubscriptionId, token); + } + + public async Task SubmitActivitySuccess(string token, object result) + { + await SubmitActivityResult(token, new ActivityResult + { + Data = result, + Status = ActivityResult.StatusType.Success + }); + } + + public async Task SubmitActivityFailure(string token, object result) + { + await SubmitActivityResult(token, new ActivityResult + { + Data = result, + Status = ActivityResult.StatusType.Fail + }); + } + + private async Task SubmitActivityResult(string token, ActivityResult result) + { + var tokenObj = Token.Decode(token); + var sub = await _subscriptionRepository.GetSubscription(tokenObj.SubscriptionId); + if (sub == null) + throw new NotFoundException(); + + if (sub.ExternalToken != token) + throw new NotFoundException("Token mismatch"); + + result.SubscriptionId = sub.Id; + + await _workflowController.PublishEvent(sub.EventName, sub.EventKey, result); + } + + class Token + { + public string SubscriptionId { get; set; } + public string ActivityName { get; set; } + public string Nonce { get; set; } + + public string Encode() + { + var json = JsonConvert.SerializeObject(this); + return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); + } + + public static Token Create(string subscriptionId, string activityName) + { + return new Token + { + SubscriptionId = subscriptionId, + ActivityName = activityName, + Nonce = Guid.NewGuid().ToString() + }; + } + + public static Token Decode(string encodedToken) + { + var raw = Convert.FromBase64String(encodedToken); + var json = Encoding.UTF8.GetString(raw); + return JsonConvert.DeserializeObject(json); + } + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs index 24b700267..26e630ea5 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,90 +11,144 @@ namespace WorkflowCore.Services.BackgroundTasks { internal class EventConsumer : QueueConsumer, IBackgroundTask { - private readonly IPersistenceProvider _persistenceStore; + private readonly IWorkflowRepository _workflowRepository; + private readonly ISubscriptionRepository _subscriptionRepository; + private readonly IEventRepository _eventRepository; private readonly IDistributedLockProvider _lockProvider; private readonly IDateTimeProvider _datetimeProvider; - + private readonly IGreyList _greylist; + protected override int MaxConcurrentItems => 2; protected override QueueType Queue => QueueType.Event; - public EventConsumer(IPersistenceProvider persistenceStore, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, WorkflowOptions options, IDateTimeProvider datetimeProvider) + public EventConsumer(IWorkflowRepository workflowRepository, ISubscriptionRepository subscriptionRepository, IEventRepository eventRepository, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, WorkflowOptions options, IDateTimeProvider datetimeProvider, IGreyList greylist) : base(queueProvider, loggerFactory, options) { - _persistenceStore = persistenceStore; + _workflowRepository = workflowRepository; + _greylist = greylist; + _subscriptionRepository = subscriptionRepository; + _eventRepository = eventRepository; _lockProvider = lockProvider; _datetimeProvider = datetimeProvider; } protected override async Task ProcessItem(string itemId, CancellationToken cancellationToken) { - if (await _lockProvider.AcquireLock($"evt:{itemId}", cancellationToken)) + if (!await _lockProvider.AcquireLock($"evt:{itemId}", cancellationToken)) { - try + Logger.LogInformation($"Event locked {itemId}"); + return; + } + + try + { + cancellationToken.ThrowIfCancellationRequested(); + var evt = await _eventRepository.GetEvent(itemId, cancellationToken); + + WorkflowActivity.Enrich(evt); + if (evt.IsProcessed) { - cancellationToken.ThrowIfCancellationRequested(); - var evt = await _persistenceStore.GetEvent(itemId); - if (evt.EventTime <= _datetimeProvider.Now.ToUniversalTime()) + _greylist.Add($"evt:{evt.Id}"); + return; + } + if (evt.EventTime <= _datetimeProvider.UtcNow) + { + IEnumerable subs = null; + if (evt.EventData is ActivityResult) { - var subs = await _persistenceStore.GetSubcriptions(evt.EventName, evt.EventKey, evt.EventTime); - var success = true; - - foreach (var sub in subs.ToList()) + var activity = await _subscriptionRepository.GetSubscription((evt.EventData as ActivityResult).SubscriptionId, cancellationToken); + if (activity == null) { - success = success && await SeedSubscription(evt, sub, cancellationToken); + Logger.LogWarning($"Activity already processed - {(evt.EventData as ActivityResult).SubscriptionId}"); + await _eventRepository.MarkEventProcessed(itemId, cancellationToken); + return; } + subs = new List { activity }; + } + else + { + subs = await _subscriptionRepository.GetSubscriptions(evt.EventName, evt.EventKey, evt.EventTime, cancellationToken); + } - if (success) - { - await _persistenceStore.MarkEventProcessed(itemId); - } + var toQueue = new HashSet(); + var complete = true; + + foreach (var sub in subs.ToList()) + complete = complete && await SeedSubscription(evt, sub, toQueue, cancellationToken); + + if (complete) + { + await _eventRepository.MarkEventProcessed(itemId, cancellationToken); } - } - finally - { - await _lockProvider.ReleaseLock($"evt:{itemId}"); + else + { + _greylist.Remove($"evt:{evt.Id}"); + } + + foreach (var eventId in toQueue) + await QueueProvider.QueueWork(eventId, QueueType.Event); } } - else + finally { - Logger.LogInformation($"Event locked {itemId}"); + await _lockProvider.ReleaseLock($"evt:{itemId}"); } } - private async Task SeedSubscription(Event evt, EventSubscription sub, CancellationToken cancellationToken) + private async Task SeedSubscription(Event evt, EventSubscription sub, HashSet toQueue, CancellationToken cancellationToken) { - if (await _lockProvider.AcquireLock(sub.WorkflowId, cancellationToken)) + foreach (var eventId in await _eventRepository.GetEvents(sub.EventName, sub.EventKey, sub.SubscribeAsOf, cancellationToken)) { - try - { - var workflow = await _persistenceStore.GetWorkflowInstance(sub.WorkflowId); - var pointers = workflow.ExecutionPointers.Where(p => p.EventName == sub.EventName && p.EventKey == sub.EventKey && !p.EventPublished && p.EndTime == null); - foreach (var p in pointers) - { - p.EventData = evt.EventData; - p.EventPublished = true; - p.Active = true; - } - workflow.NextExecution = 0; - await _persistenceStore.PersistWorkflow(workflow); - await _persistenceStore.TerminateSubscription(sub.Id); - return true; - } - catch (Exception ex) + if (eventId == evt.Id) + continue; + + var siblingEvent = await _eventRepository.GetEvent(eventId, cancellationToken); + if ((!siblingEvent.IsProcessed) && (siblingEvent.EventTime < evt.EventTime)) { - Logger.LogError(ex.Message); + await QueueProvider.QueueWork(eventId, QueueType.Event); return false; } - finally + + if (!siblingEvent.IsProcessed) + toQueue.Add(siblingEvent.Id); + } + + if (!await _lockProvider.AcquireLock(sub.WorkflowId, cancellationToken)) + { + Logger.LogInformation("Workflow locked {WorkflowId}", sub.WorkflowId); + return false; + } + + try + { + var workflow = await _workflowRepository.GetWorkflowInstance(sub.WorkflowId, cancellationToken); + IEnumerable pointers = null; + + if (!string.IsNullOrEmpty(sub.ExecutionPointerId)) + pointers = workflow.ExecutionPointers.Where(p => p.Id == sub.ExecutionPointerId && !p.EventPublished && p.EndTime == null); + else + pointers = workflow.ExecutionPointers.Where(p => p.EventName == sub.EventName && p.EventKey == sub.EventKey && !p.EventPublished && p.EndTime == null); + + foreach (var p in pointers) { - await _lockProvider.ReleaseLock(sub.WorkflowId); - await QueueProvider.QueueWork(sub.WorkflowId, QueueType.Workflow); + p.EventData = evt.EventData; + p.EventPublished = true; + p.Active = true; } + workflow.NextExecution = 0; + await _workflowRepository.PersistWorkflow(workflow, cancellationToken); + await _subscriptionRepository.TerminateSubscription(sub.Id, cancellationToken); + return true; } - else + catch (Exception ex) { - Logger.LogInformation("Workflow locked {0}", sub.WorkflowId); + Logger.LogError(ex, ex.Message); return false; } + finally + { + await _lockProvider.ReleaseLock(sub.WorkflowId); + await QueueProvider.QueueWork(sub.WorkflowId, QueueType.Workflow); + } } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs new file mode 100644 index 000000000..29565e647 --- /dev/null +++ b/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services.BackgroundTasks +{ + internal class IndexConsumer : QueueConsumer, IBackgroundTask + { + private readonly ISearchIndex _searchIndex; + private readonly ObjectPool _persistenceStorePool; + private readonly ILogger _logger; + private readonly Dictionary _errorCounts = new Dictionary(); + + protected override QueueType Queue => QueueType.Index; + protected override bool EnableSecondPasses => true; + + public IndexConsumer(IPooledObjectPolicy persistencePoolPolicy, IQueueProvider queueProvider, ILoggerFactory loggerFactory, ISearchIndex searchIndex, WorkflowOptions options) + : base(queueProvider, loggerFactory, options) + { + _persistenceStorePool = new DefaultObjectPool(persistencePoolPolicy); + _searchIndex = searchIndex; + _logger = loggerFactory.CreateLogger(GetType()); + } + + protected override async Task ProcessItem(string itemId, CancellationToken cancellationToken) + { + try + { + var workflow = await FetchWorkflow(itemId); + + WorkflowActivity.Enrich(workflow, "index"); + await _searchIndex.IndexWorkflow(workflow); + lock (_errorCounts) + { + _errorCounts.Remove(itemId); + } + } + catch (Exception e) + { + Logger.LogWarning(default(EventId), $"Error indexing workfow - {itemId} - {e.Message}"); + var errCount = 0; + lock (_errorCounts) + { + if (!_errorCounts.ContainsKey(itemId)) + _errorCounts.Add(itemId, 0); + + _errorCounts[itemId]++; + errCount = _errorCounts[itemId]; + } + + if (errCount < 5) + { + await QueueProvider.QueueWork(itemId, Queue); + return; + } + if (errCount < 20) + { + await Task.Delay(TimeSpan.FromSeconds(10)); + await QueueProvider.QueueWork(itemId, Queue); + return; + } + + lock (_errorCounts) + { + _errorCounts.Remove(itemId); + } + + Logger.LogError(default(EventId), e, $"Unable to index workfow - {itemId} - {e.Message}"); + } + } + + private async Task FetchWorkflow(string id) + { + var store = _persistenceStorePool.Get(); + try + { + return await store.GetWorkflowInstance(id); + } + finally + { + _persistenceStorePool.Return(store); + } + } + + } +} diff --git a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs index 1c23bebf6..a5392ecbb 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; +using ConcurrentCollections; using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -12,18 +16,24 @@ internal abstract class QueueConsumer : IBackgroundTask { protected abstract QueueType Queue { get; } protected virtual int MaxConcurrentItems => Math.Max(Environment.ProcessorCount, 2); + protected virtual bool EnableSecondPasses => false; protected readonly IQueueProvider QueueProvider; protected readonly ILogger Logger; protected readonly WorkflowOptions Options; protected Task DispatchTask; private CancellationTokenSource _cancellationTokenSource; + private Dictionary _activeTasks; + private ConcurrentHashSet _secondPasses; protected QueueConsumer(IQueueProvider queueProvider, ILoggerFactory loggerFactory, WorkflowOptions options) { QueueProvider = queueProvider; Options = options; Logger = loggerFactory.CreateLogger(GetType()); + + _activeTasks = new Dictionary(); + _secondPasses = new ConcurrentHashSet(); } protected abstract Task ProcessItem(string itemId, CancellationToken cancellationToken); @@ -36,70 +46,110 @@ public virtual void Start() } _cancellationTokenSource = new CancellationTokenSource(); - - DispatchTask = new Task(Execute); - DispatchTask.Start(); + + DispatchTask = Task.Factory.StartNew(Execute, TaskCreationOptions.LongRunning); } public virtual void Stop() { _cancellationTokenSource.Cancel(); - DispatchTask.Wait(); - DispatchTask = null; + if (DispatchTask != null) + { + DispatchTask.Wait(); + DispatchTask = null; + } } - private async void Execute() + private async Task Execute() { - var cancelToken = _cancellationTokenSource.Token; - var opts = new ExecutionDataflowBlockOptions() - { - MaxDegreeOfParallelism = MaxConcurrentItems, - BoundedCapacity = MaxConcurrentItems + 1 - }; - - var actionBlock = new ActionBlock(ExecuteItem, opts); + var cancelToken = _cancellationTokenSource.Token; while (!cancelToken.IsCancellationRequested) { + Activity activity = default; try { - if (!SpinWait.SpinUntil(() => actionBlock.InputCount == 0, Options.IdleTime)) + var activeCount = 0; + lock (_activeTasks) { + activeCount = _activeTasks.Count; + } + if (activeCount >= MaxConcurrentItems) + { + await Task.Delay(Options.IdleTime); continue; } + activity = WorkflowActivity.StartConsume(Queue); var item = await QueueProvider.DequeueWork(Queue, cancelToken); if (item == null) { + activity?.Dispose(); if (!QueueProvider.IsDequeueBlocking) await Task.Delay(Options.IdleTime, cancelToken); continue; } - if (!actionBlock.Post(item)) + activity?.EnrichWithDequeuedItem(item); + + var hasTask = false; + lock (_activeTasks) + { + hasTask = _activeTasks.ContainsKey(item); + } + if (hasTask) + { + _secondPasses.Add(item); + if (!EnableSecondPasses) + await QueueProvider.QueueWork(item, Queue); + activity?.Dispose(); + continue; + } + + _secondPasses.TryRemove(item); + + var waitHandle = new ManualResetEvent(false); + lock (_activeTasks) { - await QueueProvider.QueueWork(item, Queue); + _activeTasks.Add(item, waitHandle); } + var task = ExecuteItem(item, waitHandle, activity); } catch (OperationCanceledException) { } catch (Exception ex) { - Logger.LogError(ex.Message); + Logger.LogError(ex, ex.Message); + activity?.RecordException(ex); } + finally + { + activity?.Dispose(); + } + } + + List toComplete; + lock (_activeTasks) + { + toComplete = _activeTasks.Values.ToList(); } - actionBlock.Complete(); - await actionBlock.Completion; + foreach (var handle in toComplete) + handle.WaitOne(); } - private async Task ExecuteItem(string itemId) + private async Task ExecuteItem(string itemId, EventWaitHandle waitHandle, Activity activity) { try { await ProcessItem(itemId, _cancellationTokenSource.Token); + while (EnableSecondPasses && _secondPasses.Contains(itemId)) + { + _secondPasses.TryRemove(itemId); + await ProcessItem(itemId, _cancellationTokenSource.Token); + } } catch (OperationCanceledException) { @@ -107,7 +157,16 @@ private async Task ExecuteItem(string itemId) } catch (Exception ex) { - Logger.LogError($"Error executing item {itemId} - {ex.Message}"); + Logger.LogError(default(EventId), ex, $"Error executing item {itemId} - {ex.Message}"); + activity?.RecordException(ex); + } + finally + { + waitHandle.Set(); + lock (_activeTasks) + { + _activeTasks.Remove(itemId); + } } } } diff --git a/src/WorkflowCore/Services/BackgroundTasks/RunnablePoller.cs b/src/WorkflowCore/Services/BackgroundTasks/RunnablePoller.cs index 22c136ad1..29b76837c 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/RunnablePoller.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/RunnablePoller.cs @@ -1,7 +1,10 @@ using System; +using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -13,15 +16,19 @@ internal class RunnablePoller : IBackgroundTask private readonly IDistributedLockProvider _lockProvider; private readonly IQueueProvider _queueProvider; private readonly ILogger _logger; + private readonly IGreyList _greylist; private readonly WorkflowOptions _options; + private readonly IDateTimeProvider _dateTimeProvider; private Timer _pollTimer; - public RunnablePoller(IPersistenceProvider persistenceStore, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, WorkflowOptions options) + public RunnablePoller(IPersistenceProvider persistenceStore, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, IGreyList greylist, IDateTimeProvider dateTimeProvider, WorkflowOptions options) { _persistenceStore = persistenceStore; + _greylist = greylist; _queueProvider = queueProvider; _logger = loggerFactory.CreateLogger(); _lockProvider = lockProvider; + _dateTimeProvider = dateTimeProvider; _options = options; } @@ -45,17 +52,50 @@ public void Stop() /// private async void PollRunnables(object target) { + await PollWorkflows(); + await PollEvents(); + await PollCommands(); + } + + private async Task PollWorkflows() + { + var activity = WorkflowActivity.StartPoll("workflows"); try { if (await _lockProvider.AcquireLock("poll runnables", new CancellationToken())) { try { - _logger.LogInformation("Polling for runnable workflows"); - var runnables = await _persistenceStore.GetRunnableInstances(DateTime.Now); + _logger.LogDebug("Polling for runnable workflows"); + + var runnables = await _persistenceStore.GetRunnableInstances(_dateTimeProvider.Now); foreach (var item in runnables) { - _logger.LogDebug("Got runnable instance {0}", item); + if (_persistenceStore.SupportsScheduledCommands) + { + try + { + await _persistenceStore.ScheduleCommand(new ScheduledCommand() + { + CommandName = ScheduledCommand.ProcessWorkflow, + Data = item, + ExecuteTime = _dateTimeProvider.UtcNow.Ticks + }); + continue; + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + activity?.RecordException(ex); + } + } + if (_greylist.Contains($"wf:{item}")) + { + _logger.LogDebug($"Got greylisted workflow {item}"); + continue; + } + _logger.LogDebug("Got runnable instance {Item}", item); + _greylist.Add($"wf:{item}"); await _queueProvider.QueueWork(item, QueueType.Workflow); } } @@ -67,21 +107,55 @@ private async void PollRunnables(object target) } catch (Exception ex) { - _logger.LogError(ex.Message); + _logger.LogError(ex, ex.Message); + activity?.RecordException(ex); } + finally + { + activity?.Dispose(); + } + } + private async Task PollEvents() + { + var activity = WorkflowActivity.StartPoll("events"); try { if (await _lockProvider.AcquireLock("unprocessed events", new CancellationToken())) { try { - _logger.LogInformation("Polling for unprocessed events"); - var events = await _persistenceStore.GetRunnableEvents(DateTime.Now); + _logger.LogDebug("Polling for unprocessed events"); + + var events = await _persistenceStore.GetRunnableEvents(_dateTimeProvider.Now); foreach (var item in events.ToList()) { + if (_persistenceStore.SupportsScheduledCommands) + { + try + { + await _persistenceStore.ScheduleCommand(new ScheduledCommand() + { + CommandName = ScheduledCommand.ProcessEvent, + Data = item, + ExecuteTime = _dateTimeProvider.UtcNow.Ticks + }); + continue; + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + activity?.RecordException(ex); + } + } + if (_greylist.Contains($"evt:{item}")) + { + _logger.LogDebug($"Got greylisted event {item}"); + continue; + } _logger.LogDebug($"Got unprocessed event {item}"); - await _queueProvider.QueueWork(item, QueueType.Event); + _greylist.Add($"evt:{item}"); + await _queueProvider.QueueWork(item, QueueType.Event); } } finally @@ -92,8 +166,56 @@ private async void PollRunnables(object target) } catch (Exception ex) { - _logger.LogError(ex.Message); + _logger.LogError(ex, ex.Message); + activity?.RecordException(ex); + } + finally + { + activity?.Dispose(); + } + } + + private async Task PollCommands() + { + var activity = WorkflowActivity.StartPoll("commands"); + try + { + if (!_persistenceStore.SupportsScheduledCommands) + return; + + if (await _lockProvider.AcquireLock("poll-commands", new CancellationToken())) + { + try + { + _logger.LogDebug("Polling for scheduled commands"); + await _persistenceStore.ProcessCommands(new DateTimeOffset(_dateTimeProvider.UtcNow), async (command) => + { + switch (command.CommandName) + { + case ScheduledCommand.ProcessWorkflow: + await _queueProvider.QueueWork(command.Data, QueueType.Workflow); + break; + case ScheduledCommand.ProcessEvent: + await _queueProvider.QueueWork(command.Data, QueueType.Event); + break; + } + }); + } + finally + { + await _lockProvider.ReleaseLock("poll-commands"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + activity?.RecordException(ex); + } + finally + { + activity?.Dispose(); } } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs index 296f8deb3..ad9c9e6f6 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs @@ -1,8 +1,8 @@ using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ObjectPool; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -12,90 +12,133 @@ internal class WorkflowConsumer : QueueConsumer, IBackgroundTask { private readonly IDistributedLockProvider _lockProvider; private readonly IDateTimeProvider _datetimeProvider; - private readonly ObjectPool _persistenceStorePool; - private readonly ObjectPool _executorPool; + private readonly IPersistenceProvider _persistenceStore; + private readonly IWorkflowExecutor _executor; + private readonly IGreyList _greylist; + protected override int MaxConcurrentItems => Options.MaxConcurrentWorkflows; protected override QueueType Queue => QueueType.Workflow; - public WorkflowConsumer(IPooledObjectPolicy persistencePoolPolicy, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, IPooledObjectPolicy executorPoolPolicy, IDateTimeProvider datetimeProvider, WorkflowOptions options) + public WorkflowConsumer(IPersistenceProvider persistenceProvider, IQueueProvider queueProvider, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, IWorkflowExecutor executor, IDateTimeProvider datetimeProvider, IGreyList greylist, WorkflowOptions options) : base(queueProvider, loggerFactory, options) { - _persistenceStorePool = new DefaultObjectPool(persistencePoolPolicy); - _executorPool = new DefaultObjectPool(executorPoolPolicy); + _persistenceStore = persistenceProvider; + _greylist = greylist; + _executor = executor; _lockProvider = lockProvider; _datetimeProvider = datetimeProvider; } protected override async Task ProcessItem(string itemId, CancellationToken cancellationToken) { - if (await _lockProvider.AcquireLock(itemId, cancellationToken)) + if (!await _lockProvider.AcquireLock(itemId, cancellationToken)) { - WorkflowInstance workflow = null; - WorkflowExecutorResult result = null; - var persistenceStore = _persistenceStorePool.Get(); - try + Logger.LogInformation("Workflow locked {ItemId}", itemId); + return; + } + + WorkflowInstance workflow = null; + WorkflowExecutorResult result = null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + workflow = await _persistenceStore.GetWorkflowInstance(itemId, cancellationToken); + + WorkflowActivity.Enrich(workflow, "process"); + if (workflow.Status == WorkflowStatus.Runnable) { try { - cancellationToken.ThrowIfCancellationRequested(); - workflow = await persistenceStore.GetWorkflowInstance(itemId); - if (workflow.Status == WorkflowStatus.Runnable) - { - var executor = _executorPool.Get(); - try - { - result = await executor.Execute(workflow); - } - finally - { - _executorPool.Return(executor); - await persistenceStore.PersistWorkflow(workflow); - } - } + result = await _executor.Execute(workflow, cancellationToken); } finally { - await _lockProvider.ReleaseLock(itemId); - if ((workflow != null) && (result != null)) - { - foreach (var sub in result.Subscriptions) - { - await SubscribeEvent(sub, persistenceStore); - } - - await persistenceStore.PersistErrors(result.Errors); + WorkflowActivity.Enrich(result); + await _persistenceStore.PersistWorkflow(workflow, result?.Subscriptions, cancellationToken); + await QueueProvider.QueueWork(itemId, QueueType.Index); + _greylist.Remove($"wf:{itemId}"); + } + } + } + finally + { + await _lockProvider.ReleaseLock(itemId); + if ((workflow != null) && (result != null)) + { + foreach (var sub in result.Subscriptions) + { + await TryProcessSubscription(sub, _persistenceStore, cancellationToken); + } - var readAheadTicks = _datetimeProvider.Now.Add(Options.PollInterval).ToUniversalTime().Ticks; + await _persistenceStore.PersistErrors(result.Errors, cancellationToken); - if ((workflow.Status == WorkflowStatus.Runnable) && workflow.NextExecution.HasValue && workflow.NextExecution.Value < readAheadTicks) + if ((workflow.Status == WorkflowStatus.Runnable) && workflow.NextExecution.HasValue) + { + var readAheadTicks = _datetimeProvider.UtcNow.Add(Options.PollInterval).Ticks; + if (workflow.NextExecution.Value < readAheadTicks) + { + new Task(() => FutureQueue(workflow, cancellationToken)).Start(); + } + else + { + if (_persistenceStore.SupportsScheduledCommands) { - new Task(() => FutureQueue(workflow, cancellationToken)).Start(); + await _persistenceStore.ScheduleCommand(new ScheduledCommand() + { + CommandName = ScheduledCommand.ProcessWorkflow, + Data = workflow.Id, + ExecuteTime = workflow.NextExecution.Value + }); } } } } - finally - { - _persistenceStorePool.Return(persistenceStore); - } - } - else - { - Logger.LogInformation("Workflow locked {0}", itemId); } + } - - private async Task SubscribeEvent(EventSubscription subscription, IPersistenceProvider persistenceStore) + + private async Task TryProcessSubscription(EventSubscription subscription, IPersistenceProvider persistenceStore, CancellationToken cancellationToken) { - //TODO: move to own class - Logger.LogDebug("Subscribing to event {0} {1} for workflow {2} step {3}", subscription.EventName, subscription.EventKey, subscription.WorkflowId, subscription.StepId); - - await persistenceStore.CreateEventSubscription(subscription); - var events = await persistenceStore.GetEvents(subscription.EventName, subscription.EventKey, subscription.SubscribeAsOf); - foreach (var evt in events) + if (subscription.EventName != Event.EventTypeActivity) { - await persistenceStore.MarkEventUnprocessed(evt); - await QueueProvider.QueueWork(evt, QueueType.Event); + var events = await persistenceStore.GetEvents(subscription.EventName, subscription.EventKey, subscription.SubscribeAsOf, cancellationToken); + + foreach (var evt in events) + { + var eventKey = $"evt:{evt}"; + bool acquiredLock = false; + try + { + acquiredLock = await _lockProvider.AcquireLock(eventKey, cancellationToken); + int attempt = 0; + while (!acquiredLock && attempt < 10) + { + await Task.Delay(Options.IdleTime, cancellationToken); + acquiredLock = await _lockProvider.AcquireLock(eventKey, cancellationToken); + + attempt++; + } + + if (!acquiredLock) + { + Logger.LogWarning($"Failed to lock {evt}"); + } + else + { + _greylist.Remove(eventKey); + await persistenceStore.MarkEventUnprocessed(evt, cancellationToken); + await QueueProvider.QueueWork(evt, QueueType.Event); + } + } + finally + { + if (acquiredLock) + { + await _lockProvider.ReleaseLock(eventKey); + } + } + } } } @@ -108,7 +151,7 @@ private async void FutureQueue(WorkflowInstance workflow, CancellationToken canc return; } - var target = (workflow.NextExecution.Value - _datetimeProvider.Now.ToUniversalTime().Ticks); + var target = (workflow.NextExecution.Value - _datetimeProvider.UtcNow.Ticks); if (target > 0) { await Task.Delay(TimeSpan.FromTicks(target), cancellationToken); @@ -118,8 +161,8 @@ private async void FutureQueue(WorkflowInstance workflow, CancellationToken canc } catch (Exception ex) { - Logger.LogError(ex.Message); + Logger.LogError(ex, ex.Message); } } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/CancellationProcessor.cs b/src/WorkflowCore/Services/CancellationProcessor.cs new file mode 100644 index 000000000..4e56de9ea --- /dev/null +++ b/src/WorkflowCore/Services/CancellationProcessor.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + public class CancellationProcessor : ICancellationProcessor + { + protected readonly ILogger _logger; + private readonly IExecutionResultProcessor _executionResultProcessor; + private readonly IDateTimeProvider _dateTimeProvider; + + public CancellationProcessor(IExecutionResultProcessor executionResultProcessor, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) + { + _executionResultProcessor = executionResultProcessor; + _logger = logFactory.CreateLogger(); + _dateTimeProvider = dateTimeProvider; + } + + public void ProcessCancellations(WorkflowInstance workflow, WorkflowDefinition workflowDef, WorkflowExecutorResult executionResult) + { + foreach (var step in workflowDef.Steps.Where(x => x.CancelCondition != null)) + { + var func = step.CancelCondition.Compile(); + var cancel = false; + try + { + cancel = (bool)(func.DynamicInvoke(workflow.Data)); + } + catch (Exception ex) + { + _logger.LogError(default(EventId), ex, ex.Message); + } + if (cancel) + { + var toCancel = workflow.ExecutionPointers.Where(x => x.StepId == step.Id && x.Status != PointerStatus.Complete && x.Status != PointerStatus.Cancelled).ToList(); + + foreach (var ptr in toCancel) + { + if (step.ProceedOnCancel) + { + _executionResultProcessor.ProcessExecutionResult(workflow, workflowDef, ptr, step, ExecutionResult.Next(), executionResult); + } + + ptr.EndTime = _dateTimeProvider.UtcNow; + ptr.Active = false; + ptr.Status = PointerStatus.Cancelled; + + foreach (var descendent in workflow.ExecutionPointers.FindByScope(ptr.Id).Where(x => x.Status != PointerStatus.Complete && x.Status != PointerStatus.Cancelled)) + { + descendent.EndTime = _dateTimeProvider.UtcNow; + descendent.Active = false; + descendent.Status = PointerStatus.Cancelled; + } + } + } + } + } + } +} diff --git a/src/WorkflowCore/Services/DateTimeProvider.cs b/src/WorkflowCore/Services/DateTimeProvider.cs index 467b08248..93defc48f 100644 --- a/src/WorkflowCore/Services/DateTimeProvider.cs +++ b/src/WorkflowCore/Services/DateTimeProvider.cs @@ -6,5 +6,6 @@ namespace WorkflowCore.Services public class DateTimeProvider : IDateTimeProvider { public DateTime Now => DateTime.Now; + public DateTime UtcNow => DateTime.UtcNow; } } diff --git a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs index 582562281..cb69c8791 100644 --- a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs @@ -1,26 +1,32 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; namespace WorkflowCore.Services { - #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + + public interface ISingletonMemoryProvider : IPersistenceProvider + { + } +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously /// /// In-memory implementation of IPersistenceProvider for demo and testing purposes /// - public class MemoryPersistenceProvider : IPersistenceProvider + public class MemoryPersistenceProvider : ISingletonMemoryProvider { + private readonly List _instances = new List(); + private readonly List _subscriptions = new List(); + private readonly List _events = new List(); + private readonly List _errors = new List(); - private static List _instances = new List(); - private static List _subscriptions = new List(); - private static List _events = new List(); - private static List _errors = new List(); + public bool SupportsScheduledCommands => false; - public async Task CreateNewWorkflow(WorkflowInstance workflow) + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken _ = default) { lock (_instances) { @@ -30,7 +36,7 @@ public async Task CreateNewWorkflow(WorkflowInstance workflow) } } - public async Task PersistWorkflow(WorkflowInstance workflow) + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken _ = default) { lock (_instances) { @@ -40,7 +46,26 @@ public async Task PersistWorkflow(WorkflowInstance workflow) } } - public async Task> GetRunnableInstances(DateTime asAt) + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + lock (_instances) + { + var existing = _instances.First(x => x.Id == workflow.Id); + _instances.Remove(existing); + _instances.Add(workflow); + + lock (_subscriptions) + { + foreach (var subscription in subscriptions) + { + subscription.Id = Guid.NewGuid().ToString(); + _subscriptions.Add(subscription); + } + } + } + } + + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken _ = default) { lock (_instances) { @@ -49,7 +74,7 @@ public async Task> GetRunnableInstances(DateTime asAt) } } - public async Task GetWorkflowInstance(string Id) + public async Task GetWorkflowInstance(string Id, CancellationToken _ = default) { lock (_instances) { @@ -57,6 +82,19 @@ public async Task GetWorkflowInstance(string Id) } } + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken _ = default) + { + if (ids == null) + { + return new List(); + } + + lock (_instances) + { + return _instances.Where(x => ids.Contains(x.Id)); + } + } + public async Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) { lock (_instances) @@ -88,7 +126,7 @@ public async Task> GetWorkflowInstances(WorkflowSt } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) { lock (_subscriptions) { @@ -98,7 +136,7 @@ public async Task CreateEventSubscription(EventSubscription subscription } } - public async Task> GetSubcriptions(string eventName, string eventKey, DateTime asOf) + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) { lock (_subscriptions) { @@ -107,7 +145,7 @@ public async Task> GetSubcriptions(string eventNa } } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) { lock (_subscriptions) { @@ -116,11 +154,58 @@ public async Task TerminateSubscription(string eventSubscriptionId) } } + public Task GetSubscription(string eventSubscriptionId, CancellationToken _ = default) + { + lock (_subscriptions) + { + var sub = _subscriptions.Single(x => x.Id == eventSubscriptionId); + return Task.FromResult(sub); + } + } + + public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) + { + lock (_subscriptions) + { + var result = _subscriptions + .FirstOrDefault(x => x.ExternalToken == null && x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf); + return Task.FromResult(result); + } + } + + public Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken _ = default) + { + lock (_subscriptions) + { + var sub = _subscriptions.Single(x => x.Id == eventSubscriptionId); + sub.ExternalToken = token; + sub.ExternalWorkerId = workerId; + sub.ExternalTokenExpiry = expiry; + + return Task.FromResult(true); + } + } + + public Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken _ = default) + { + lock (_subscriptions) + { + var sub = _subscriptions.Single(x => x.Id == eventSubscriptionId); + if (sub.ExternalToken != token) + throw new InvalidOperationException(); + sub.ExternalToken = null; + sub.ExternalWorkerId = null; + sub.ExternalTokenExpiry = null; + + return Task.CompletedTask; + } + } + public void EnsureStoreExists() - { + { } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken _ = default) { lock (_events) { @@ -129,8 +214,8 @@ public async Task CreateEvent(Event newEvent) return newEvent.Id; } } - - public async Task MarkEventProcessed(string id) + + public async Task MarkEventProcessed(string id, CancellationToken _ = default) { lock (_events) { @@ -140,7 +225,7 @@ public async Task MarkEventProcessed(string id) } } - public async Task> GetRunnableEvents(DateTime asAt) + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken _ = default) { lock (_events) { @@ -152,7 +237,7 @@ public async Task> GetRunnableEvents(DateTime asAt) } } - public async Task GetEvent(string id) + public async Task GetEvent(string id, CancellationToken _ = default) { lock (_events) { @@ -160,7 +245,7 @@ public async Task GetEvent(string id) } } - public async Task> GetEvents(string eventName, string eventKey, DateTime asOf) + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) { lock (_events) { @@ -172,7 +257,7 @@ public async Task> GetEvents(string eventName, string eventK } } - public async Task MarkEventUnprocessed(string id) + public async Task MarkEventUnprocessed(string id, CancellationToken _ = default) { lock (_events) { @@ -184,14 +269,24 @@ public async Task MarkEventUnprocessed(string id) } } - public async Task PersistErrors(IEnumerable errors) + public async Task PersistErrors(IEnumerable errors, CancellationToken _ = default) { lock (errors) { _errors.AddRange(errors); } } + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } } - #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } diff --git a/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs b/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs new file mode 100644 index 000000000..40d13fa9c --- /dev/null +++ b/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; + +namespace WorkflowCore.Services +{ + public class NullSearchIndex : ISearchIndex + { + public Task IndexWorkflow(WorkflowInstance workflow) + { + return Task.CompletedTask; + } + + public Task> Search(string terms, int skip, int take, params SearchFilter[] filters) + { + throw new NotImplementedException(); + } + + public Task Start() + { + return Task.CompletedTask; + } + + public Task Stop() + { + return Task.CompletedTask; + } + } +} diff --git a/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs b/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs new file mode 100644 index 000000000..007b51efb --- /dev/null +++ b/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Services +{ + public class SingleNodeEventHub : ILifeCycleEventHub + { + private ICollection> _subscribers = new HashSet>(); + private readonly ILogger _logger; + + public SingleNodeEventHub(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public Task PublishNotification(LifeCycleEvent evt) + { + Task.Run(() => + { + foreach (var subscriber in _subscribers) + { + try + { + subscriber(evt); + } + catch (Exception ex) + { + _logger.LogWarning(default(EventId), ex, $"Error on event subscriber: {ex.Message}"); + } + } + }); + return Task.CompletedTask; + } + + public void Subscribe(Action action) + { + _subscribers.Add(action); + } + + public Task Start() + { + return Task.CompletedTask; + } + + public Task Stop() + { + _subscribers.Clear(); + return Task.CompletedTask; + } + } +} diff --git a/src/WorkflowCore/Services/DefaultProviders/SingleNodeLockProvider.cs b/src/WorkflowCore/Services/DefaultProviders/SingleNodeLockProvider.cs index 3d6107d66..6e920b712 100644 --- a/src/WorkflowCore/Services/DefaultProviders/SingleNodeLockProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/SingleNodeLockProvider.cs @@ -12,7 +12,7 @@ namespace WorkflowCore.Services /// public class SingleNodeLockProvider : IDistributedLockProvider { - private List _locks = new List(); + private HashSet _locks = new HashSet(); public async Task AcquireLock(string Id, CancellationToken cancellationToken) { diff --git a/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs b/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs index 2f3b5bb45..56f63b550 100644 --- a/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; @@ -12,48 +13,43 @@ namespace WorkflowCore.Services /// public class SingleNodeQueueProvider : IQueueProvider { - - private readonly BlockingCollection _runQueue = new BlockingCollection(); - private readonly BlockingCollection _eventQueue = new BlockingCollection(); + + private readonly Dictionary> _queues = new Dictionary> + { + [QueueType.Workflow] = new BlockingCollection(), + [QueueType.Event] = new BlockingCollection(), + [QueueType.Index] = new BlockingCollection() + }; public bool IsDequeueBlocking => true; - public async Task QueueWork(string id, QueueType queue) + public Task QueueWork(string id, QueueType queue) { - SelectQueue(queue).Add(id); + _queues[queue].Add(id); + return Task.CompletedTask; } - public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) - { - if (SelectQueue(queue).TryTake(out string id, 100, cancellationToken)) - return id; + public Task DequeueWork(QueueType queue, CancellationToken cancellationToken) + { + if (_queues[queue].TryTake(out string id, 100, cancellationToken)) + return Task.FromResult(id); - return null; + return Task.FromResult(null); } - public async Task Start() + public Task Start() { + return Task.CompletedTask; } - public async Task Stop() + public Task Stop() { + return Task.CompletedTask; } public void Dispose() { } - - private BlockingCollection SelectQueue(QueueType queue) - { - switch (queue) - { - case QueueType.Workflow: - return _runQueue; - case QueueType.Event: - return _eventQueue; - } - return null; - } } diff --git a/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs new file mode 100644 index 000000000..31ec6dbe2 --- /dev/null +++ b/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + public class TransientMemoryPersistenceProvider : IPersistenceProvider + { + private readonly ISingletonMemoryProvider _innerService; + + public bool SupportsScheduledCommands => false; + + public TransientMemoryPersistenceProvider(ISingletonMemoryProvider innerService) + { + _innerService = innerService; + } + + public Task CreateEvent(Event newEvent, CancellationToken _ = default) => _innerService.CreateEvent(newEvent); + + public Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) => _innerService.CreateEventSubscription(subscription); + + public Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken _ = default) => _innerService.CreateNewWorkflow(workflow); + + public void EnsureStoreExists() => _innerService.EnsureStoreExists(); + + public Task GetEvent(string id, CancellationToken _ = default) => _innerService.GetEvent(id); + + public Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetEvents(eventName, eventKey, asOf); + + public Task> GetRunnableEvents(DateTime asAt, CancellationToken _ = default) => _innerService.GetRunnableEvents(asAt); + + public Task> GetRunnableInstances(DateTime asAt, CancellationToken _ = default) => _innerService.GetRunnableInstances(asAt); + + public Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetSubscriptions(eventName, eventKey, asOf); + + public Task GetWorkflowInstance(string Id, CancellationToken _ = default) => _innerService.GetWorkflowInstance(Id); + + public Task> GetWorkflowInstances(IEnumerable ids, CancellationToken _ = default) => _innerService.GetWorkflowInstances(ids); + + public Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) => _innerService.GetWorkflowInstances(status, type, createdFrom, createdTo, skip, take); + + public Task MarkEventProcessed(string id, CancellationToken _ = default) => _innerService.MarkEventProcessed(id); + + public Task MarkEventUnprocessed(string id, CancellationToken _ = default) => _innerService.MarkEventUnprocessed(id); + + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) => _innerService.PersistErrors(errors); + + public Task PersistWorkflow(WorkflowInstance workflow, CancellationToken _ = default) => _innerService.PersistWorkflow(workflow); + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + await PersistWorkflow(workflow, cancellationToken); + + foreach(var subscription in subscriptions) + { + await CreateEventSubscription(subscription, cancellationToken); + } + } + + public Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) => _innerService.TerminateSubscription(eventSubscriptionId); + public Task GetSubscription(string eventSubscriptionId, CancellationToken _ = default) => _innerService.GetSubscription(eventSubscriptionId); + + public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetFirstOpenSubscription(eventName, eventKey, asOf); + + public Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken _ = default) => _innerService.SetSubscriptionToken(eventSubscriptionId, token, workerId, expiry); + + public Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken _ = default) => _innerService.ClearSubscriptionToken(eventSubscriptionId, token); + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/WorkflowCore/Services/DefaultWorkflowMiddlewareErrorHandler.cs b/src/WorkflowCore/Services/DefaultWorkflowMiddlewareErrorHandler.cs new file mode 100644 index 000000000..99c4652ff --- /dev/null +++ b/src/WorkflowCore/Services/DefaultWorkflowMiddlewareErrorHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; + +namespace WorkflowCore.Services +{ + /// + /// Default implementation of . Just logs the + /// thrown exception and moves on. + /// + public class DefaultWorkflowMiddlewareErrorHandler : IWorkflowMiddlewareErrorHandler + { + private readonly ILogger _log; + + public DefaultWorkflowMiddlewareErrorHandler(ILogger log) + { + _log = log; + } + + /// + /// Asynchronously handle the given exception. + /// + /// The exception to handle + /// A task that completes when handling is done. + public Task HandleAsync(Exception ex) + { + _log.LogError(ex, "An error occurred running workflow middleware: {Message}", ex.Message); + return Task.CompletedTask; + } + } +} diff --git a/src/WorkflowCore/Services/DefinitionStorage/DefinitionLoader.cs b/src/WorkflowCore/Services/DefinitionStorage/DefinitionLoader.cs deleted file mode 100644 index 28b1a3643..000000000 --- a/src/WorkflowCore/Services/DefinitionStorage/DefinitionLoader.cs +++ /dev/null @@ -1,208 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Dynamic.Core; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using WorkflowCore.Interface; -using WorkflowCore.Models; -using WorkflowCore.Primitives; -using WorkflowCore.Models.DefinitionStorage; -using WorkflowCore.Models.DefinitionStorage.v1; -using WorkflowCore.Exceptions; - -namespace WorkflowCore.Services.DefinitionStorage -{ - public class DefinitionLoader : IDefinitionLoader - { - private readonly IWorkflowRegistry _registry; - - public DefinitionLoader(IWorkflowRegistry registry) - { - _registry = registry; - } - - public WorkflowDefinition LoadDefinition(string json) - { - var source = JsonConvert.DeserializeObject(json); - var def = Convert(source); - _registry.RegisterWorkflow(def); - return def; - } - - private WorkflowDefinition Convert(DefinitionSourceV1 source) - { - var dataType = typeof(object); - if (!string.IsNullOrEmpty(source.DataType)) - dataType = FindType(source.DataType); - - var result = new WorkflowDefinition - { - Id = source.Id, - Version = source.Version, - Steps = ConvertSteps(source.Steps, dataType), - DefaultErrorBehavior = source.DefaultErrorBehavior, - DefaultErrorRetryInterval = source.DefaultErrorRetryInterval, - Description = source.Description, - DataType = dataType - }; - - return result; - } - - - private List ConvertSteps(ICollection source, Type dataType) - { - var result = new List(); - int i = 0; - var stack = new Stack(source.Reverse()); - var parents = new List(); - var compensatables = new List(); - - while (stack.Count > 0) - { - 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); - - if (!string.IsNullOrEmpty(nextStep.CancelCondition)) - { - containerType = typeof(CancellableStep<,>).MakeGenericType(stepType, dataType); - 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); - targetStep = (containerType.GetConstructor(new Type[] { cancelExprType }).Invoke(new[] { cancelExpr }) as WorkflowStep); - } - - if (nextStep.Saga) //TODO: cancellable saga??? - { - containerType = typeof(SagaContainer<>).MakeGenericType(stepType); - targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep); - } - - targetStep.Id = i; - targetStep.Name = nextStep.Name; - targetStep.ErrorBehavior = nextStep.ErrorBehavior; - targetStep.RetryInterval = nextStep.RetryInterval; - targetStep.Tag = $"{nextStep.Id}"; - - AttachInputs(nextStep, dataType, stepType, targetStep); - AttachOutputs(nextStep, dataType, stepType, targetStep); - - if (nextStep.Do != null) - { - foreach (var branch in nextStep.Do) - { - foreach (var child in branch.Reverse()) - stack.Push(child); - } - - if (nextStep.Do.Count > 0) - parents.Add(nextStep); - } - - if (nextStep.CompensateWith != null) - { - foreach (var compChild in nextStep.CompensateWith.Reverse()) - stack.Push(compChild); - - if (nextStep.CompensateWith.Count > 0) - compensatables.Add(nextStep); - } - - if (!string.IsNullOrEmpty(nextStep.NextStepId)) - targetStep.Outcomes.Add(new StepOutcome() { Tag = $"{nextStep.NextStepId}" }); - - result.Add(targetStep); - - i++; - } - - foreach (var step in result) - { - if (result.Any(x => x.Tag == step.Tag && x.Id != step.Id)) - throw new WorkflowDefinitionLoadException($"Duplicate step Id {step.Tag}"); - - foreach (var outcome in step.Outcomes) - { - if (result.All(x => x.Tag != outcome.Tag)) - throw new WorkflowDefinitionLoadException($"Cannot find step id {outcome.Tag}"); - - outcome.NextStep = result.Single(x => x.Tag == outcome.Tag).Id; - } - } - - foreach (var parent in parents) - { - var target = result.Single(x => x.Tag == parent.Id); - foreach (var branch in parent.Do) - { - var childTags = branch.Select(x => x.Id).ToList(); - target.Children.AddRange(result - .Where(x => childTags.Contains(x.Tag)) - .OrderBy(x => x.Id) - .Select(x => x.Id) - .Take(1) - .ToList()); - } - } - - foreach (var item in compensatables) - { - var target = result.Single(x => x.Tag == item.Id); - var tag = item.CompensateWith.Select(x => x.Id).FirstOrDefault(); - if (tag != null) - { - var compStep = result.FirstOrDefault(x => x.Tag == tag); - if (compStep != null) - target.CompensationStepId = compStep.Id; - } - } - - return result; - } - - private void AttachInputs(StepSourceV1 source, Type dataType, Type stepType, WorkflowStep step) - { - foreach (var input in source.Inputs) - { - var dataParameter = Expression.Parameter(dataType, "data"); - var contextParameter = Expression.Parameter(typeof(IStepExecutionContext), "context"); - var sourceExpr = DynamicExpressionParser.ParseLambda(new [] { dataParameter, contextParameter }, typeof(object), input.Value); - var targetExpr = Expression.Property(Expression.Parameter(stepType), input.Key); - - step.Inputs.Add(new DataMapping() - { - Source = sourceExpr, - Target = Expression.Lambda(targetExpr) - }); - } - } - - private void AttachOutputs(StepSourceV1 source, Type dataType, Type stepType, WorkflowStep step) - { - foreach (var output in source.Outputs) - { - var stepParameter = Expression.Parameter(stepType, "step"); - var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { stepParameter }, typeof(object), output.Value); - var targetExpr = Expression.Property(Expression.Parameter(dataType), output.Key); - - step.Outputs.Add(new DataMapping() - { - Source = sourceExpr, - Target = Expression.Lambda(targetExpr) - }); - } - } - - private Type FindType(string name) - { - return Type.GetType(name, true, true); - } - - } -} diff --git a/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs new file mode 100644 index 000000000..d8990c160 --- /dev/null +++ b/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services.ErrorHandlers +{ + public class CompensateHandler : IWorkflowErrorHandler + { + private readonly IExecutionPointerFactory _pointerFactory; + private readonly IDateTimeProvider _datetimeProvider; + private readonly WorkflowOptions _options; + + public WorkflowErrorHandling Type => WorkflowErrorHandling.Compensate; + + public CompensateHandler(IExecutionPointerFactory pointerFactory, IDateTimeProvider datetimeProvider, WorkflowOptions options) + { + _pointerFactory = pointerFactory; + _datetimeProvider = datetimeProvider; + _options = options; + } + + public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer exceptionPointer, WorkflowStep exceptionStep, Exception exception, Queue bubbleUpQueue) + { + var scope = new Stack(exceptionPointer.Scope.Reverse()); + scope.Push(exceptionPointer.Id); + ExecutionPointer compensationPointer = null; + + while (scope.Any()) + { + var pointerId = scope.Pop(); + var scopePointer = workflow.ExecutionPointers.FindById(pointerId); + var scopeStep = def.Steps.FindById(scopePointer.StepId); + + var resume = true; + var revert = false; + + var txnStack = new Stack(scope.Reverse()); + while (txnStack.Count > 0) + { + var parentId = txnStack.Pop(); + var parentPointer = workflow.ExecutionPointers.FindById(parentId); + var parentStep = def.Steps.FindById(parentPointer.StepId); + if ((!parentStep.ResumeChildrenAfterCompensation) || (parentStep.RevertChildrenAfterCompensation)) + { + resume = parentStep.ResumeChildrenAfterCompensation; + revert = parentStep.RevertChildrenAfterCompensation; + break; + } + } + + if ((scopeStep.ErrorBehavior ?? WorkflowErrorHandling.Compensate) != WorkflowErrorHandling.Compensate) + { + bubbleUpQueue.Enqueue(scopePointer); + continue; + } + + scopePointer.Active = false; + scopePointer.EndTime = _datetimeProvider.UtcNow; + scopePointer.Status = PointerStatus.Failed; + + if (scopeStep.CompensationStepId.HasValue) + { + scopePointer.Status = PointerStatus.Compensated; + + var nextCompensationPointer = _pointerFactory.BuildCompensationPointer(def, scopePointer, exceptionPointer, scopeStep.CompensationStepId.Value); + if (compensationPointer != null) + { + nextCompensationPointer.Active = false; + nextCompensationPointer.Status = PointerStatus.PendingPredecessor; + nextCompensationPointer.PredecessorId = compensationPointer.Id; + } + compensationPointer = nextCompensationPointer; + workflow.ExecutionPointers.Add(compensationPointer); + + if (resume) + { + foreach (var outcomeTarget in scopeStep.Outcomes.Where(x => x.Matches(workflow.Data))) + workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, scopePointer, outcomeTarget)); + } + } + + if (revert) + { + var prevSiblings = workflow.ExecutionPointers + .Where(x => scopePointer.Scope.SequenceEqual(x.Scope) && x.Id != scopePointer.Id && x.Status == PointerStatus.Complete) + .OrderByDescending(x => x.EndTime) + .ToList(); + + foreach (var siblingPointer in prevSiblings) + { + var siblingStep = def.Steps.FindById(siblingPointer.StepId); + if (siblingStep.CompensationStepId.HasValue) + { + var nextCompensationPointer = _pointerFactory.BuildCompensationPointer(def, siblingPointer, exceptionPointer, siblingStep.CompensationStepId.Value); + if (compensationPointer != null) + { + nextCompensationPointer.Active = false; + nextCompensationPointer.Status = PointerStatus.PendingPredecessor; + nextCompensationPointer.PredecessorId = compensationPointer.Id; + compensationPointer = nextCompensationPointer; + } + workflow.ExecutionPointers.Add(nextCompensationPointer); + + siblingPointer.Status = PointerStatus.Compensated; + } + } + } + } + } + } +} diff --git a/src/WorkflowCore/Services/ErrorHandlers/RetryHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/RetryHandler.cs new file mode 100644 index 000000000..84fbe5bd3 --- /dev/null +++ b/src/WorkflowCore/Services/ErrorHandlers/RetryHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services.ErrorHandlers +{ + public class RetryHandler : IWorkflowErrorHandler + { + private readonly IDateTimeProvider _datetimeProvider; + private readonly WorkflowOptions _options; + public WorkflowErrorHandling Type => WorkflowErrorHandling.Retry; + + public RetryHandler(IDateTimeProvider datetimeProvider, WorkflowOptions options) + { + _datetimeProvider = datetimeProvider; + _options = options; + } + + public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue) + { + pointer.RetryCount++; + pointer.SleepUntil = _datetimeProvider.UtcNow.Add(step.RetryInterval ?? def.DefaultErrorRetryInterval ?? _options.ErrorRetryInterval); + step.PrimeForRetry(pointer); + } + } +} diff --git a/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs new file mode 100755 index 000000000..3cc3279ec --- /dev/null +++ b/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Services.ErrorHandlers +{ + public class SuspendHandler : IWorkflowErrorHandler + { + private readonly ILifeCycleEventPublisher _eventPublisher; + private readonly IDateTimeProvider _datetimeProvider; + public WorkflowErrorHandling Type => WorkflowErrorHandling.Suspend; + + public SuspendHandler(ILifeCycleEventPublisher eventPublisher, IDateTimeProvider datetimeProvider) + { + _eventPublisher = eventPublisher; + _datetimeProvider = datetimeProvider; + } + + public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue) + { + workflow.Status = WorkflowStatus.Suspended; + _eventPublisher.PublishNotification(new WorkflowSuspended + { + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version + }); + + step.PrimeForRetry(pointer); + } + } +} diff --git a/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs new file mode 100755 index 000000000..6cafe5ece --- /dev/null +++ b/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Services.ErrorHandlers +{ + public class TerminateHandler : IWorkflowErrorHandler + { + private readonly ILifeCycleEventPublisher _eventPublisher; + private readonly IDateTimeProvider _dateTimeProvider; + public WorkflowErrorHandling Type => WorkflowErrorHandling.Terminate; + + public TerminateHandler(ILifeCycleEventPublisher eventPublisher, IDateTimeProvider dateTimeProvider) + { + _eventPublisher = eventPublisher; + _dateTimeProvider = dateTimeProvider; + } + + public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue) + { + workflow.Status = WorkflowStatus.Terminated; + workflow.CompleteTime = _dateTimeProvider.UtcNow; + + _eventPublisher.PublishNotification(new WorkflowTerminated + { + EventTimeUtc = _dateTimeProvider.UtcNow, + Reference = workflow.Reference, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version + }); + } + } +} diff --git a/src/WorkflowCore/Services/ExecutionPointerFactory.cs b/src/WorkflowCore/Services/ExecutionPointerFactory.cs index ae74b6821..5bd4b2846 100644 --- a/src/WorkflowCore/Services/ExecutionPointerFactory.cs +++ b/src/WorkflowCore/Services/ExecutionPointerFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -9,8 +8,7 @@ namespace WorkflowCore.Services { public class ExecutionPointerFactory : IExecutionPointerFactory { - - public ExecutionPointer BuildStartingPointer(WorkflowDefinition def) + public ExecutionPointer BuildGenesisPointer(WorkflowDefinition def) { return new ExecutionPointer { @@ -18,14 +16,14 @@ public ExecutionPointer BuildStartingPointer(WorkflowDefinition def) StepId = 0, Active = true, Status = PointerStatus.Pending, - StepName = Enumerable.First(def.Steps, x => x.Id == 0).Name + StepName = def.Steps.FindById(0).Name }; } - public ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, StepOutcome outcomeTarget) + public ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, IStepOutcome outcomeTarget) { var nextId = GenerateId(); - return new ExecutionPointer() + return new ExecutionPointer { Id = nextId, PredecessorId = pointer.Id, @@ -33,19 +31,19 @@ public ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointe Active = true, ContextItem = pointer.ContextItem, Status = PointerStatus.Pending, - StepName = def.Steps.First(x => x.Id == outcomeTarget.NextStep).Name, - Scope = new Stack(pointer.Scope) + StepName = def.Steps.FindById(outcomeTarget.NextStep).Name, + Scope = new List(pointer.Scope) }; } public ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPointer pointer, int childDefinitionId, object branch) { var childPointerId = GenerateId(); - var childScope = new Stack(pointer.Scope); - childScope.Push(pointer.Id); + var childScope = new List(pointer.Scope); + childScope.Insert(0, pointer.Id); pointer.Children.Add(childPointerId); - return new ExecutionPointer() + return new ExecutionPointer { Id = childPointerId, PredecessorId = pointer.Id, @@ -53,15 +51,15 @@ public ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPoint Active = true, ContextItem = branch, Status = PointerStatus.Pending, - StepName = def.Steps.First(x => x.Id == childDefinitionId).Name, - Scope = childScope + StepName = def.Steps.FindById(childDefinitionId).Name, + Scope = new List(childScope) }; } public ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, ExecutionPointer pointer, ExecutionPointer exceptionPointer, int compensationStepId) { var nextId = GenerateId(); - return new ExecutionPointer() + return new ExecutionPointer { Id = nextId, PredecessorId = exceptionPointer.Id, @@ -69,8 +67,8 @@ public ExecutionPointer BuildCompensationPointer(WorkflowDefinition def, Executi Active = true, ContextItem = pointer.ContextItem, Status = PointerStatus.Pending, - StepName = def.Steps.First(x => x.Id == compensationStepId).Name, - Scope = new Stack(pointer.Scope) + StepName = def.Steps.FindById(compensationStepId).Name, + Scope = new List(pointer.Scope) }; } diff --git a/src/WorkflowCore/Services/ExecutionResultProcessor.cs b/src/WorkflowCore/Services/ExecutionResultProcessor.cs old mode 100644 new mode 100755 index 8dc946ae8..3c684945f --- a/src/WorkflowCore/Services/ExecutionResultProcessor.cs +++ b/src/WorkflowCore/Services/ExecutionResultProcessor.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services { @@ -12,12 +13,16 @@ public class ExecutionResultProcessor : IExecutionResultProcessor private readonly IExecutionPointerFactory _pointerFactory; private readonly IDateTimeProvider _datetimeProvider; private readonly ILogger _logger; + private readonly ILifeCycleEventPublisher _eventPublisher; + private readonly IEnumerable _errorHandlers; private readonly WorkflowOptions _options; - public ExecutionResultProcessor(IExecutionPointerFactory pointerFactory, IDateTimeProvider datetimeProvider, WorkflowOptions options, ILoggerFactory loggerFactory) + public ExecutionResultProcessor(IExecutionPointerFactory pointerFactory, IDateTimeProvider datetimeProvider, ILifeCycleEventPublisher eventPublisher, IEnumerable errorHandlers, WorkflowOptions options, ILoggerFactory loggerFactory) { _pointerFactory = pointerFactory; _datetimeProvider = datetimeProvider; + _eventPublisher = eventPublisher; + _errorHandlers = errorHandlers; _options = options; _logger = loggerFactory.CreateLogger(); } @@ -28,7 +33,7 @@ public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition pointer.Outcome = result.OutcomeValue; if (result.SleepFor.HasValue) { - pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(result.SleepFor.Value); + pointer.SleepUntil = _datetimeProvider.UtcNow.Add(result.SleepFor.Value); pointer.Status = PointerStatus.Sleeping; } @@ -39,26 +44,49 @@ public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition pointer.Active = false; pointer.Status = PointerStatus.WaitingForEvent; - workflowResult.Subscriptions.Add(new EventSubscription() + workflowResult.Subscriptions.Add(new EventSubscription { WorkflowId = workflow.Id, StepId = pointer.StepId, + ExecutionPointerId = pointer.Id, EventName = pointer.EventName, EventKey = pointer.EventKey, - SubscribeAsOf = result.EventAsOf + SubscribeAsOf = result.EventAsOf, + SubscriptionData = result.SubscriptionData }); } if (result.Proceed) { pointer.Active = false; - pointer.EndTime = _datetimeProvider.Now.ToUniversalTime(); + pointer.EndTime = _datetimeProvider.UtcNow; pointer.Status = PointerStatus.Complete; - - foreach (var outcomeTarget in step.Outcomes.Where(x => object.Equals(x.GetValue(workflow.Data), result.OutcomeValue) || x.GetValue(workflow.Data) == null)) + + foreach (var outcomeTarget in step.Outcomes.Where(x => x.Matches(result, workflow.Data))) { workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, pointer, outcomeTarget)); } + + var pendingSubsequents = workflow.ExecutionPointers + .FindByStatus(PointerStatus.PendingPredecessor) + .Where(x => x.PredecessorId == pointer.Id); + + foreach (var subsequent in pendingSubsequents) + { + subsequent.Status = PointerStatus.Pending; + subsequent.Active = true; + } + + _eventPublisher.PublishNotification(new StepCompleted + { + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + ExecutionPointerId = pointer.Id, + StepId = step.Id, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version + }); } else { @@ -72,102 +100,39 @@ public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition } } - public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step) - { - pointer.Status = PointerStatus.Failed; - var compensatingStepId = FindScopeCompensationStepId(workflow, def, pointer); - var errorOption = (step.ErrorBehavior ?? (compensatingStepId.HasValue ? WorkflowErrorHandling.Compensate : def.DefaultErrorBehavior)); - SelectErrorStrategy(errorOption, workflow, def, pointer, step); - } - - private void SelectErrorStrategy(WorkflowErrorHandling errorOption, WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step) + public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception) { - switch (errorOption) + _eventPublisher.PublishNotification(new WorkflowError { - case WorkflowErrorHandling.Retry: - pointer.RetryCount++; - pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(step.RetryInterval ?? def.DefaultErrorRetryInterval ?? _options.ErrorRetryInterval); - step.PrimeForRetry(pointer); - break; - case WorkflowErrorHandling.Suspend: - workflow.Status = WorkflowStatus.Suspended; - break; - case WorkflowErrorHandling.Terminate: - workflow.Status = WorkflowStatus.Terminated; - break; - case WorkflowErrorHandling.Compensate: - Compensate(workflow, def, pointer); - break; - } - } - - private void Compensate(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer exceptionPointer) - { - var scope = new Stack(exceptionPointer.Scope); - scope.Push(exceptionPointer.Id); - - exceptionPointer.Active = false; - exceptionPointer.EndTime = _datetimeProvider.Now.ToUniversalTime(); - exceptionPointer.Status = PointerStatus.Failed; - - while (scope.Any()) + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version, + ExecutionPointerId = pointer.Id, + StepId = step.Id, + Message = exception.Message + }); + pointer.Status = PointerStatus.Failed; + + var queue = new Queue(); + queue.Enqueue(pointer); + + while (queue.Count > 0) { - var pointerId = scope.Pop(); - var pointer = workflow.ExecutionPointers.First(x => x.Id == pointerId); - var step = def.Steps.First(x => x.Id == pointer.StepId); + var exceptionPointer = queue.Dequeue(); + var exceptionStep = def.Steps.FindById(exceptionPointer.StepId); + var shouldCompensate = ShouldCompensate(workflow, def, exceptionPointer); + var errorOption = (exceptionStep.ErrorBehavior ?? (shouldCompensate ? WorkflowErrorHandling.Compensate : def.DefaultErrorBehavior)); - var resume = true; - var revert = false; - - if (scope.Any()) - { - var parentId = scope.Peek(); - var parentPointer = workflow.ExecutionPointers.First(x => x.Id == parentId); - var parentStep = def.Steps.First(x => x.Id == parentPointer.StepId); - resume = parentStep.ResumeChildrenAfterCompensation; - revert = parentStep.RevertChildrenAfterCompensation; - } - - if ((step.ErrorBehavior ?? WorkflowErrorHandling.Compensate) != WorkflowErrorHandling.Compensate) + foreach (var handler in _errorHandlers.Where(x => x.Type == errorOption)) { - SelectErrorStrategy(step.ErrorBehavior ?? WorkflowErrorHandling.Retry, workflow, def, pointer, step); - continue; - } - - if (step.CompensationStepId.HasValue) - { - pointer.Active = false; - pointer.EndTime = _datetimeProvider.Now.ToUniversalTime(); - pointer.Status = PointerStatus.Compensated; - - var compensationPointer = _pointerFactory.BuildCompensationPointer(def, pointer, exceptionPointer, step.CompensationStepId.Value); - workflow.ExecutionPointers.Add(compensationPointer); - - if (resume) - { - foreach (var outcomeTarget in step.Outcomes.Where(x => x.GetValue(workflow.Data) == null)) - workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, pointer, outcomeTarget)); - } - } - - if (revert) - { - var prevSiblings = workflow.ExecutionPointers.Where(x => pointer.Scope.SequenceEqual(x.Scope) && x.Id != pointer.Id && x.Status == PointerStatus.Complete).ToList(); - foreach (var siblingPointer in prevSiblings) - { - var siblingStep = def.Steps.First(x => x.Id == siblingPointer.StepId); - if (siblingStep.CompensationStepId.HasValue) - { - var compensationPointer = _pointerFactory.BuildCompensationPointer(def, siblingPointer, exceptionPointer, siblingStep.CompensationStepId.Value); - workflow.ExecutionPointers.Add(compensationPointer); - siblingPointer.Status = PointerStatus.Compensated; - } - } + handler.Handle(workflow, def, exceptionPointer, exceptionStep, exception, queue); } } } - - private int? FindScopeCompensationStepId(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer currentPointer) + + private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer currentPointer) { var scope = new Stack(currentPointer.Scope); scope.Push(currentPointer.Id); @@ -175,13 +140,13 @@ private void Compensate(WorkflowInstance workflow, WorkflowDefinition def, Execu while (scope.Count > 0) { var pointerId = scope.Pop(); - var pointer = workflow.ExecutionPointers.First(x => x.Id == pointerId); - var step = def.Steps.First(x => x.Id == pointer.StepId); - if (step.CompensationStepId.HasValue) - return step.CompensationStepId.Value; + var pointer = workflow.ExecutionPointers.FindById(pointerId); + var step = def.Steps.FindById(pointer.StepId); + if ((step.CompensationStepId.HasValue) || (step.RevertChildrenAfterCompensation)) + return true; } - return null; + return false; } } } \ No newline at end of file diff --git a/src/WorkflowCore/Services/FluentBuilders/ParallelStepBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/ParallelStepBuilder.cs index b8617a0ad..81cb34690 100644 --- a/src/WorkflowCore/Services/FluentBuilders/ParallelStepBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/ParallelStepBuilder.cs @@ -25,8 +25,12 @@ public ParallelStepBuilder(IWorkflowBuilder workflowBuilder, IStepBuilder public IParallelStepBuilder Do(Action> builder) { - int lastStep = WorkflowBuilder.LastStep; - builder.Invoke(WorkflowBuilder); + var lastStep = WorkflowBuilder.LastStep; + builder.Invoke(WorkflowBuilder); + + if (lastStep == WorkflowBuilder.LastStep) + throw new NotSupportedException("Empty Do block not supported"); + Step.Children.Add(lastStep + 1); //TODO: make more elegant return this; diff --git a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs index 25cd3b6c7..9e41f15ca 100644 --- a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs @@ -26,6 +26,12 @@ public IStepBuilder Name(string name) return this; } + public IStepBuilder Id(string id) + { + Step.ExternalId = id; + return this; + } + public IStepBuilder Then(Action> stepSetup = null) where TStep : IStepBody { @@ -39,7 +45,7 @@ public IStepBuilder Then(Action> } newStep.Name = newStep.Name ?? typeof(TStep).Name; - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -47,18 +53,18 @@ public IStepBuilder Then(Action> public IStepBuilder Then(IStepBuilder newStep) where TStep : IStepBody { - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Step.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Step.Id }); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep.Step); return stepBuilder; } public IStepBuilder Then(Func body) - { + { WorkflowStepInline newStep = new WorkflowStepInline(); newStep.Body = body; WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -68,55 +74,105 @@ public IStepBuilder Then(Action bo WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); stepBuilder.Input(x => x.Body, x => body); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } + public IStepBuilder Attach(string id) + { + Step.Outcomes.Add(new ValueOutcome + { + ExternalNextStepId = id + }); + + return this; + } + public IStepOutcomeBuilder When(object outcomeValue, string label = null) { - StepOutcome result = new StepOutcome(); - result.Value = x => outcomeValue; - result.Label = label; + Expression> expr = x => outcomeValue; + ValueOutcome result = new ValueOutcome + { + Value = expr, + Label = label + }; Step.Outcomes.Add(result); var outcomeBuilder = new StepOutcomeBuilder(WorkflowBuilder, result); return outcomeBuilder; } + public IStepBuilder Branch(object outcomeValue, IStepBuilder branch) where TStep : IStepBody + { + if (branch.WorkflowBuilder.Steps.Count == 0) + return this; + + WorkflowBuilder.AttachBranch(branch.WorkflowBuilder); + Expression> expr = x => outcomeValue; + + Step.Outcomes.Add(new ValueOutcome + { + Value = expr, + NextStep = branch.WorkflowBuilder.Steps[0].Id + }); + + return this; + } + + public IStepBuilder Branch(Expression> outcomeExpression, IStepBuilder branch) where TStep : IStepBody + { + if (branch.WorkflowBuilder.Steps.Count == 0) + return this; + + WorkflowBuilder.AttachBranch(branch.WorkflowBuilder); + + Step.Outcomes.Add(new ExpressionOutcome(outcomeExpression) + { + NextStep = branch.WorkflowBuilder.Steps[0].Id + }); + + return this; + } + public IStepBuilder Input(Expression> stepProperty, Expression> value) { - var mapping = new DataMapping(); - mapping.Source = value; - mapping.Target = stepProperty; - Step.Inputs.Add(mapping); + Step.Inputs.Add(new MemberMapParameter(value, stepProperty)); return this; } public IStepBuilder Input(Expression> stepProperty, Expression> value) { - var mapping = new DataMapping(); - mapping.Source = value; - mapping.Target = stepProperty; - Step.Inputs.Add(mapping); + Step.Inputs.Add(new MemberMapParameter(value, stepProperty)); + return this; + } + + public IStepBuilder Input(Action action) + { + Step.Inputs.Add(new ActionParameter(action)); + return this; + } + + public IStepBuilder Input(Action action) + { + Step.Inputs.Add(new ActionParameter(action)); return this; } public IStepBuilder Output(Expression> dataProperty, Expression> value) { - var mapping = new DataMapping(); - mapping.Source = value; - mapping.Target = dataProperty; - Step.Outputs.Add(mapping); + Step.Outputs.Add(new MemberMapParameter(value, dataProperty)); return this; } - public IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, Expression> cancelCondition = null) + public IStepBuilder Output(Action action) { - WorkflowStep newStep; + Step.Outputs.Add(new ActionParameter(action)); + return this; + } - if (cancelCondition != null) - newStep = new CancellableStep(cancelCondition); - else - newStep = new WorkflowStep(); + public IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, Expression> cancelCondition = null) + { + var newStep = new WorkflowStep(); + newStep.CancelCondition = cancelCondition; WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); @@ -128,18 +184,14 @@ public IStepBuilder WaitFor(string eventName, Expression step.EffectiveDate, effectiveDate); } - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } public IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, Expression> cancelCondition = null) { - WorkflowStep newStep; - - if (cancelCondition != null) - newStep = new CancellableStep(cancelCondition); - else - newStep = new WorkflowStep(); + var newStep = new WorkflowStep(); + newStep.CancelCondition = cancelCondition; WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); @@ -151,10 +203,10 @@ public IStepBuilder WaitFor(string eventName, Expression step.EffectiveDate, effectiveDate); } - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } - + public IStepBuilder End(string name) where TStep : IStepBody { var ancestor = IterateParents(Step.Id, name); @@ -207,7 +259,7 @@ public IStepBuilder EndWorkflow() { EndStep newStep = new EndStep(); WorkflowBuilder.AddStep(newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return this; } @@ -216,18 +268,25 @@ public IStepBuilder Delay(Expression> period var newStep = new WorkflowStep(); Expression> inputExpr = (x => x.Period); + newStep.Inputs.Add(new MemberMapParameter(period, inputExpr)); - var mapping = new DataMapping() - { - Source = period, - Target = inputExpr - }; + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public IStepBuilder Decide(Expression> expression) + { + var newStep = new WorkflowStep(); - newStep.Inputs.Add(mapping); + Expression> inputExpr = (x => x.Expression); + newStep.Inputs.Add(new MemberMapParameter(expression, inputExpr)); WorkflowBuilder.AddStep(newStep); - var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -235,20 +294,50 @@ public IStepBuilder Delay(Expression> period public IContainerStepBuilder ForEach(Expression> collection) { var newStep = new WorkflowStep(); - + Expression> inputExpr = (x => x.Collection); + newStep.Inputs.Add(new MemberMapParameter(collection, inputExpr)); - var mapping = new DataMapping() - { - Source = collection, - Target = inputExpr - }; - newStep.Inputs.Add(mapping); + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel) + { + var newStep = new WorkflowStep(); + + Expression> inputExpr = (x => x.Collection); + newStep.Inputs.Add(new MemberMapParameter(collection, inputExpr)); + + Expression> pExpr = (x => x.RunParallel); + newStep.Inputs.Add(new MemberMapParameter(runParallel, pExpr)); WorkflowBuilder.AddStep(newStep); - var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel) + { + var newStep = new WorkflowStep(); + + Expression> inputExpr = (x => x.Collection); + newStep.Inputs.Add(new MemberMapParameter(collection, inputExpr)); + + Expression> pExpr = (x => x.RunParallel); + newStep.Inputs.Add(new MemberMapParameter(runParallel, pExpr)); + + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -258,18 +347,27 @@ public IContainerStepBuilder While(Expression(); Expression> inputExpr = (x => x.Condition); + newStep.Inputs.Add(new MemberMapParameter(condition, inputExpr)); - var mapping = new DataMapping() - { - Source = condition, - Target = inputExpr - }; - newStep.Inputs.Add(mapping); + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public IContainerStepBuilder While(Expression> condition) + { + var newStep = new WorkflowStep(); + + Expression> inputExpr = (x => x.Condition); + newStep.Inputs.Add(new MemberMapParameter(condition, inputExpr)); WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -279,34 +377,36 @@ public IContainerStepBuilder If(Expression> con var newStep = new WorkflowStep(); Expression> inputExpr = (x => x.Condition); + newStep.Inputs.Add(new MemberMapParameter(condition, inputExpr)); - var mapping = new DataMapping() - { - Source = condition, - Target = inputExpr - }; + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - newStep.Inputs.Add(mapping); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public IContainerStepBuilder If(Expression> condition) + { + var newStep = new WorkflowStep(); + + Expression> inputExpr = (x => x.Condition); + newStep.Inputs.Add(new MemberMapParameter(condition, inputExpr)); WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } - + public IContainerStepBuilder When(Expression> outcomeValue, string label = null) { var newStep = new WorkflowStep(); Expression> inputExpr = (x => x.ExpectedOutcome); - var mapping = new DataMapping() - { - Source = outcomeValue, - Target = inputExpr - }; - - newStep.Inputs.Add(mapping); + newStep.Inputs.Add(new MemberMapParameter(outcomeValue, inputExpr)); IStepBuilder switchBuilder; @@ -314,7 +414,7 @@ public IContainerStepBuilder When(Expression(); WorkflowBuilder.AddStep(switchStep); - Step.Outcomes.Add(new StepOutcome() + Step.Outcomes.Add(new ValueOutcome { NextStep = switchStep.Id, Label = label @@ -325,7 +425,7 @@ public IContainerStepBuilder When(Expression); } - + WorkflowBuilder.AddStep(newStep); var stepBuilder = new ReturnStepBuilder(WorkflowBuilder, newStep, switchBuilder); switchBuilder.Step.Children.Add(newStep.Id); @@ -338,7 +438,7 @@ public IStepBuilder Saga(Action> builde var newStep = new SagaContainer(); WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); builder.Invoke(WorkflowBuilder); stepBuilder.Step.Children.Add(stepBuilder.Step.Id + 1); //TODO: make more elegant @@ -352,7 +452,7 @@ public IParallelStepBuilder Parallel() WorkflowBuilder.AddStep(newStep); var stepBuilder = new ParallelStepBuilder(WorkflowBuilder, newBuilder, newBuilder); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -361,33 +461,28 @@ public IContainerStepBuilder Schedule(Expression(); Expression> inputExpr = (x => x.Interval); - - var mapping = new DataMapping() - { - Source = time, - Target = inputExpr - }; - - newStep.Inputs.Add(mapping); + newStep.Inputs.Add(new MemberMapParameter(time, inputExpr)); WorkflowBuilder.AddStep(newStep); var stepBuilder = new ReturnStepBuilder(WorkflowBuilder, newStep, this); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } public IContainerStepBuilder Recur(Expression> interval, Expression> until) { - var newStep = new CancellableStep(until); + var newStep = new WorkflowStep(); + newStep.CancelCondition = until; + Expression> intervalExpr = (x => x.Interval); Expression> untilExpr = (x => x.StopCondition); - newStep.Inputs.Add(new DataMapping() { Source = interval, Target = intervalExpr }); - newStep.Inputs.Add(new DataMapping() { Source = until, Target = untilExpr }); + newStep.Inputs.Add(new MemberMapParameter(interval, intervalExpr)); + newStep.Inputs.Add(new MemberMapParameter(until, untilExpr)); WorkflowBuilder.AddStep(newStep); var stepBuilder = new ReturnStepBuilder(WorkflowBuilder, newStep, this); - Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -448,5 +543,50 @@ public IStepBuilder CompensateWithSequence(Action CancelCondition(Expression> cancelCondition, bool proceedAfterCancel = false) + { + Step.CancelCondition = cancelCondition; + Step.ProceedOnCancel = proceedAfterCancel; + return this; + } + + public IStepBuilder Activity(string activityName, Expression> parameters = null, Expression> effectiveDate = null, Expression> cancelCondition = null) + { + var newStep = new WorkflowStep(); + newStep.CancelCondition = cancelCondition; + + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + stepBuilder.Input((step) => step.ActivityName, (data) => activityName); + + if (parameters != null) + stepBuilder.Input((step) => step.Parameters, parameters); + + if (effectiveDate != null) + stepBuilder.Input((step) => step.EffectiveDate, effectiveDate); + + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + return stepBuilder; + } + + public IStepBuilder Activity(Expression> activityName, Expression> parameters = null, Expression> effectiveDate = null, Expression> cancelCondition = null) + { + var newStep = new WorkflowStep(); + newStep.CancelCondition = cancelCondition; + + WorkflowBuilder.AddStep(newStep); + var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); + stepBuilder.Input((step) => step.ActivityName, activityName); + + if (parameters != null) + stepBuilder.Input((step) => step.Parameters, parameters); + + if (effectiveDate != null) + stepBuilder.Input((step) => step.EffectiveDate, effectiveDate); + + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + return stepBuilder; + } } } diff --git a/src/WorkflowCore/Services/FluentBuilders/StepOutcomeBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/StepOutcomeBuilder.cs index 64f244aaa..1d654152a 100644 --- a/src/WorkflowCore/Services/FluentBuilders/StepOutcomeBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/StepOutcomeBuilder.cs @@ -8,9 +8,9 @@ namespace WorkflowCore.Services public class StepOutcomeBuilder : IStepOutcomeBuilder { public IWorkflowBuilder WorkflowBuilder { get; private set; } - public StepOutcome Outcome { get; private set; } + public ValueOutcome Outcome { get; private set; } - public StepOutcomeBuilder(IWorkflowBuilder workflowBuilder, StepOutcome outcome) + public StepOutcomeBuilder(IWorkflowBuilder workflowBuilder, ValueOutcome outcome) { WorkflowBuilder = workflowBuilder; Outcome = outcome; diff --git a/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs index a98a34185..adc02f488 100644 --- a/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Primitives; @@ -9,7 +11,9 @@ namespace WorkflowCore.Services { public class WorkflowBuilder : IWorkflowBuilder { - protected List Steps { get; set; } = new List(); + public List Steps { get; set; } = new List(); + + protected ICollection Branches { get; set; } = new List(); protected WorkflowErrorHandling DefaultErrorBehavior = WorkflowErrorHandling.Retry; @@ -25,20 +29,96 @@ public IWorkflowBuilder UseData() public virtual WorkflowDefinition Build(string id, int version) { - WorkflowDefinition result = new WorkflowDefinition(); - result.Id = id; - result.Version = version; - result.Steps = this.Steps; - result.DefaultErrorBehavior = DefaultErrorBehavior; - result.DefaultErrorRetryInterval = DefaultErrorRetryInterval; - return result; + AttachExternalIds(); + return new WorkflowDefinition + { + Id = id, + Version = version, + Steps = new WorkflowStepCollection(Steps), + DefaultErrorBehavior = DefaultErrorBehavior, + DefaultErrorRetryInterval = DefaultErrorRetryInterval + }; } public void AddStep(WorkflowStep step) { step.Id = Steps.Count(); Steps.Add(step); - } + } + + private void AttachExternalIds() + { + foreach (var step in Steps) + { + foreach (var outcome in step.Outcomes.Where(x => !string.IsNullOrEmpty(x.ExternalNextStepId))) + { + if (Steps.All(x => x.ExternalId != outcome.ExternalNextStepId)) + throw new KeyNotFoundException($"Cannot find step id {outcome.ExternalNextStepId}"); + + outcome.NextStep = Steps.Single(x => x.ExternalId == outcome.ExternalNextStepId).Id; + } + } + } + + public void AttachBranch(IWorkflowBuilder branch) + { + if (Branches.Contains(branch)) + return; + + var branchStart = LastStep + branch.LastStep + 1; + + foreach (var step in branch.Steps) + { + var oldId = step.Id; + step.Id = oldId + branchStart; + foreach (var step2 in branch.Steps) + { + foreach (var outcome in step2.Outcomes) + { + if (outcome.NextStep == oldId) + outcome.NextStep = step.Id; + } + + for (var i = 0; i < step2.Children.Count; i++) + { + if (step2.Children[i] == oldId) + step2.Children[i] = step.Id; + } + + if (step2.CompensationStepId == oldId) + { + step2.CompensationStepId = step.Id; + } + } + } + + foreach (var step in branch.Steps) + { + var oldId = step.Id; + AddStep(step); + foreach (var step2 in branch.Steps) + { + foreach (var outcome in step2.Outcomes) + { + if (outcome.NextStep == oldId) + outcome.NextStep = step.Id; + } + + for (var i = 0; i < step2.Children.Count; i++) + { + if (step2.Children[i] == oldId) + step2.Children[i] = step.Id; + } + + if (step2.CompensationStepId == oldId) + { + step2.CompensationStepId = step.Id; + } + } + } + + Branches.Add(branch); + } } @@ -102,6 +182,128 @@ public IWorkflowBuilder UseDefaultErrorBehavior(WorkflowErrorHandling beh DefaultErrorRetryInterval = retryInterval; return this; } + + public IWorkflowBuilder CreateBranch() + { + var result = new WorkflowBuilder(new List()); + return result; + } + + public IStepBuilder Then(Action> stepSetup = null) where TStep : IStepBody + { + return Start().Then(stepSetup); + } + + public IStepBuilder Then(IStepBuilder newStep) where TStep : IStepBody + { + return Start().Then(newStep); + } + + public IStepBuilder Then(Func body) + { + return Start().Then(body); + } + + public IStepBuilder Then(Action body) + { + return Start().Then(body); + } + + public IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, + Expression> cancelCondition = null) + { + return Start().WaitFor(eventName, eventKey, effectiveDate, cancelCondition); + } + + public IStepBuilder WaitFor(string eventName, Expression> eventKey, Expression> effectiveDate = null, + Expression> cancelCondition = null) + { + return Start().WaitFor(eventName, eventKey, effectiveDate, cancelCondition); + } + + public IStepBuilder Delay(Expression> period) + { + return Start().Delay(period); + } + + public IStepBuilder Decide(Expression> expression) + { + return Start().Decide(expression); + } + + public IContainerStepBuilder ForEach(Expression> collection) + { + return Start().ForEach(collection); + } + + public IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel) + { + return Start().ForEach(collection, runParallel); + } + + public IContainerStepBuilder ForEach(Expression> collection, Expression> runParallel) + { + return Start().ForEach(collection, runParallel); + } + + public IContainerStepBuilder While(Expression> condition) + { + return Start().While(condition); + } + + public IContainerStepBuilder While(Expression> condition) + { + return Start().While(condition); + } + + public IContainerStepBuilder If(Expression> condition) + { + return Start().If(condition); + } + + public IContainerStepBuilder If(Expression> condition) + { + return Start().If(condition); + } + + public IContainerStepBuilder When(Expression> outcomeValue, string label = null) + { + return ((IWorkflowModifier) Start()).When(outcomeValue, label); + } + + public IParallelStepBuilder Parallel() + { + return Start().Parallel(); + } + + public IStepBuilder Saga(Action> builder) + { + return Start().Saga(builder); + } + + public IContainerStepBuilder Schedule(Expression> time) + { + return Start().Schedule(time); + } + + public IContainerStepBuilder Recur(Expression> interval, Expression> until) + { + return Start().Recur(interval, until); + } + + public IStepBuilder Activity(string activityName, Expression> parameters = null, Expression> effectiveDate = null, + Expression> cancelCondition = null) + { + return Start().Activity(activityName, parameters, effectiveDate, cancelCondition); + } + public IStepBuilder Activity(Expression> activityName, Expression> parameters = null, Expression> effectiveDate = null, Expression> cancelCondition = null) + { + return Start().Activity(activityName, parameters, effectiveDate, cancelCondition); + } + + private IStepBuilder Start() + { + return StartWith(_ => ExecutionResult.Next()); + } } - } diff --git a/src/WorkflowCore/Services/GreyList.cs b/src/WorkflowCore/Services/GreyList.cs new file mode 100644 index 000000000..85b67fbc8 --- /dev/null +++ b/src/WorkflowCore/Services/GreyList.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading; +using WorkflowCore.Interface; + +namespace WorkflowCore.Services +{ + public class GreyList : IGreyList, IDisposable + { + private readonly Timer _cycleTimer; + private readonly ConcurrentDictionary _list; + private readonly ILogger _logger; + private readonly IDateTimeProvider _dateTimeProvider; + private const int CYCLE_TIME = 30; + private const int TTL = 5; + + public GreyList(ILoggerFactory loggerFactory, IDateTimeProvider dateTimeProvider) + { + _logger = loggerFactory.CreateLogger(); + _dateTimeProvider = dateTimeProvider; + _list = new ConcurrentDictionary(); + _cycleTimer = new Timer(new TimerCallback(Cycle), null, TimeSpan.FromMinutes(CYCLE_TIME), TimeSpan.FromMinutes(CYCLE_TIME)); + } + + public void Add(string id) + { + _list.AddOrUpdate(id, _dateTimeProvider.Now, (key, val) => _dateTimeProvider.Now); + } + + public bool Contains(string id) + { + if (!_list.TryGetValue(id, out var start)) + return false; + + var result = start > (_dateTimeProvider.Now.AddMinutes(-1 * TTL)); + + if (!result) + _list.TryRemove(id, out var _); + + return result; + } + + private void Cycle(object target) + { + try + { + _list.Clear(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + } + } + + public void Dispose() + { + _cycleTimer.Dispose(); + } + + public void Remove(string id) + { + _list.TryRemove(id, out var _); + } + } +} diff --git a/src/WorkflowCore/Services/LifeCycleEventPublisher.cs b/src/WorkflowCore/Services/LifeCycleEventPublisher.cs new file mode 100644 index 000000000..e73c6bf4f --- /dev/null +++ b/src/WorkflowCore/Services/LifeCycleEventPublisher.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Services +{ + public class LifeCycleEventPublisher : ILifeCycleEventPublisher, IDisposable + { + private readonly ILifeCycleEventHub _eventHub; + private readonly WorkflowOptions _workflowOptions; + private readonly ILogger _logger; + private BlockingCollection _outbox; + private Task _dispatchTask; + + public LifeCycleEventPublisher(ILifeCycleEventHub eventHub, WorkflowOptions workflowOptions, ILoggerFactory loggerFactory) + { + _eventHub = eventHub; + _workflowOptions = workflowOptions; + _outbox = new BlockingCollection(); + _logger = loggerFactory.CreateLogger(GetType()); + } + + public void PublishNotification(LifeCycleEvent evt) + { + if (_outbox.IsAddingCompleted || !_workflowOptions.EnableLifeCycleEventsPublisher) + return; + + _outbox.Add(evt); + } + + public void Start() + { + if (_dispatchTask != null) + { + throw new InvalidOperationException(); + } + + if (_outbox.IsAddingCompleted) + { + _outbox = new BlockingCollection(); + } + + _dispatchTask = new Task(Execute); + _dispatchTask.Start(); + } + + public void Stop() + { + _outbox.CompleteAdding(); + _dispatchTask.Wait(); + _dispatchTask = null; + } + + public void Dispose() + { + _outbox.Dispose(); + } + + private async void Execute() + { + foreach (var evt in _outbox.GetConsumingEnumerable()) + { + try + { + await _eventHub.PublishNotification(evt); + } + catch (Exception ex) + { + _logger.LogError(default(EventId), ex, ex.Message); + } + } + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/ScopeProvider.cs b/src/WorkflowCore/Services/ScopeProvider.cs new file mode 100644 index 000000000..5c52d8eb9 --- /dev/null +++ b/src/WorkflowCore/Services/ScopeProvider.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using WorkflowCore.Interface; + +namespace WorkflowCore.Services +{ + /// + /// A concrete implementation for the IScopeProvider interface + /// Could be used for context-aware scope creation customization + /// + public class ScopeProvider : IScopeProvider + { + private readonly IServiceScopeFactory _serviceScopeFactory; + + public ScopeProvider(IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + } + + public IServiceScope CreateScope(IStepExecutionContext context) + { + return _serviceScopeFactory.CreateScope(); + } + } +} diff --git a/src/WorkflowCore/Services/StepExecutor.cs b/src/WorkflowCore/Services/StepExecutor.cs new file mode 100644 index 000000000..4e45574e5 --- /dev/null +++ b/src/WorkflowCore/Services/StepExecutor.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + /// + /// Executes the workflow step and applies any to the step. + /// + public class StepExecutor : IStepExecutor + { + private readonly IEnumerable _stepMiddleware; + + public StepExecutor( + IEnumerable stepMiddleware + ) + { + _stepMiddleware = stepMiddleware; + } + + /// + /// Runs the passed in the given while applying + /// any registered in the system. Middleware will be run in the + /// order in which they were registered with DI with middleware declared earlier starting earlier and + /// completing later. + /// + /// The in which to execute the step. + /// The body. + /// A to wait for the result of running the step + public async Task ExecuteStep( + IStepExecutionContext context, + IStepBody body + ) + { + // Build the middleware chain by reducing over all the middleware in reverse starting with step body + // and building step delegates that call out to the next delegate in the chain + Task Step() => body.RunAsync(context); + var middlewareChain = _stepMiddleware + .Reverse() + .Aggregate( + (WorkflowStepDelegate) Step, + (previous, middleware) => () => middleware.HandleAsync(context, body, previous) + ); + + // Run the middleware chain + return await middlewareChain(); + } + } +} diff --git a/src/WorkflowCore/Services/SyncWorkflowRunner.cs b/src/WorkflowCore/Services/SyncWorkflowRunner.cs new file mode 100644 index 000000000..5317a8966 --- /dev/null +++ b/src/WorkflowCore/Services/SyncWorkflowRunner.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Exceptions; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + public class SyncWorkflowRunner : ISyncWorkflowRunner + { + private readonly IWorkflowHost _host; + private readonly IWorkflowExecutor _executor; + private readonly IDistributedLockProvider _lockService; + private readonly IWorkflowRegistry _registry; + private readonly IPersistenceProvider _persistenceStore; + private readonly IExecutionPointerFactory _pointerFactory; + private readonly IQueueProvider _queueService; + private readonly IDateTimeProvider _dateTimeProvider; + + public SyncWorkflowRunner(IWorkflowHost host, IWorkflowExecutor executor, IDistributedLockProvider lockService, IWorkflowRegistry registry, IPersistenceProvider persistenceStore, IExecutionPointerFactory pointerFactory, IQueueProvider queueService, IDateTimeProvider dateTimeProvider) + { + _host = host; + _executor = executor; + _lockService = lockService; + _registry = registry; + _persistenceStore = persistenceStore; + _pointerFactory = pointerFactory; + _queueService = queueService; + _dateTimeProvider = dateTimeProvider; + } + + public Task RunWorkflowSync(string workflowId, int version, TData data, + string reference, TimeSpan timeOut, bool persistSate = true) + where TData : new() + { + return RunWorkflowSync(workflowId, version, data, reference, new CancellationTokenSource(timeOut).Token, + persistSate); + } + + public async Task RunWorkflowSync(string workflowId, int version, TData data, string reference, CancellationToken token, bool persistSate = true) + where TData : new() + { + var def = _registry.GetDefinition(workflowId, version); + if (def == null) + { + throw new WorkflowNotRegisteredException(workflowId, version); + } + + var wf = new WorkflowInstance + { + WorkflowDefinitionId = workflowId, + Version = def.Version, + Data = data, + Description = def.Description, + NextExecution = 0, + CreateTime = _dateTimeProvider.UtcNow, + Status = WorkflowStatus.Suspended, + Reference = reference + }; + + if ((def.DataType != null) && (data == null)) + { + if (typeof(TData) == def.DataType) + wf.Data = new TData(); + else + wf.Data = def.DataType.GetConstructor(new Type[0]).Invoke(new object[0]); + } + + wf.ExecutionPointers.Add(_pointerFactory.BuildGenesisPointer(def)); + + var id = Guid.NewGuid().ToString(); + + if (persistSate) + id = await _persistenceStore.CreateNewWorkflow(wf, token); + else + wf.Id = id; + + wf.Status = WorkflowStatus.Runnable; + + if (!await _lockService.AcquireLock(id, CancellationToken.None)) + { + throw new InvalidOperationException(); + } + + try + { + while ((wf.Status == WorkflowStatus.Runnable) && !token.IsCancellationRequested) + { + await _executor.Execute(wf, token); + if (persistSate) + await _persistenceStore.PersistWorkflow(wf, token); + } + } + finally + { + await _lockService.ReleaseLock(id); + } + + if (persistSate) + await _queueService.QueueWork(id, QueueType.Index); + + return wf; + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowActivity.cs b/src/WorkflowCore/Services/WorkflowActivity.cs new file mode 100644 index 000000000..b4599d367 --- /dev/null +++ b/src/WorkflowCore/Services/WorkflowActivity.cs @@ -0,0 +1,124 @@ +using System.Diagnostics; +using OpenTelemetry.Trace; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + internal static class WorkflowActivity + { + private static readonly ActivitySource ActivitySource = new ActivitySource("WorkflowCore"); + + internal static Activity StartHost() + { + var activityName = "workflow start host"; + return ActivitySource.StartRootActivity(activityName, ActivityKind.Internal); + } + + internal static Activity StartConsume(QueueType queueType) + { + var activityName = $"workflow consume {GetQueueType(queueType)}"; + var activity = ActivitySource.StartRootActivity(activityName, ActivityKind.Consumer); + + activity?.SetTag("workflow.queue", queueType); + + return activity; + } + + + internal static Activity StartPoll(string type) + { + var activityName = $"workflow poll {type}"; + var activity = ActivitySource.StartRootActivity(activityName, ActivityKind.Client); + + activity?.SetTag("workflow.poll", type); + + return activity; + } + + internal static void Enrich(WorkflowInstance workflow, string action) + { + var activity = Activity.Current; + if (activity != null) + { + activity.DisplayName = $"workflow {action} {workflow.WorkflowDefinitionId}"; + activity.SetTag("workflow.id", workflow.Id); + activity.SetTag("workflow.definition", workflow.WorkflowDefinitionId); + activity.SetTag("workflow.status", workflow.Status); + } + } + + internal static void Enrich(WorkflowStep workflowStep) + { + var activity = Activity.Current; + if (activity != null) + { + var stepName = string.IsNullOrEmpty(workflowStep.Name) + ? "inline" + : workflowStep.Name; + + activity.DisplayName += $" step {stepName}"; + activity.SetTag("workflow.step.id", workflowStep.Id); + activity.SetTag("workflow.step.name", stepName); + activity.SetTag("workflow.step.type", workflowStep.BodyType.Name); + } + } + + internal static void Enrich(WorkflowExecutorResult result) + { + var activity = Activity.Current; + if (activity != null) + { + activity.SetTag("workflow.subscriptions.count", result.Subscriptions.Count); + activity.SetTag("workflow.errors.count", result.Errors.Count); + + if (result.Errors.Count > 0) + { + activity.SetStatus(Status.Error); + activity.SetStatus(ActivityStatusCode.Error); + } + } + } + + internal static void Enrich(Event evt) + { + var activity = Activity.Current; + if (activity != null) + { + activity.DisplayName = $"workflow process {evt.EventName}"; + activity.SetTag("workflow.event.id", evt.Id); + activity.SetTag("workflow.event.name", evt.EventName); + activity.SetTag("workflow.event.processed", evt.IsProcessed); + } + } + + internal static void EnrichWithDequeuedItem(this Activity activity, string item) + { + if (activity != null) + { + activity.SetTag("workflow.queue.item", item); + } + } + + private static Activity StartRootActivity( + this ActivitySource activitySource, + string name, + ActivityKind kind) + { + Activity.Current = null; + + return activitySource.StartActivity(name, kind); + } + + private static string GetQueueType(QueueType queueType) + { + switch (queueType) + { + case QueueType.Workflow: return "workflow"; + case QueueType.Event: return "event"; + case QueueType.Index: return "index"; + default: return "unknown"; + } + } + } +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowController.cs b/src/WorkflowCore/Services/WorkflowController.cs old mode 100644 new mode 100755 index c9f68ccdf..79272e084 --- a/src/WorkflowCore/Services/WorkflowController.cs +++ b/src/WorkflowCore/Services/WorkflowController.cs @@ -1,12 +1,14 @@ using System; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using WorkflowCore.Exceptions; using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services { @@ -17,39 +19,44 @@ public class WorkflowController : IWorkflowController private readonly IWorkflowRegistry _registry; private readonly IQueueProvider _queueProvider; private readonly IExecutionPointerFactory _pointerFactory; + private readonly ILifeCycleEventHub _eventHub; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; + private readonly IDateTimeProvider _dateTimeProvider; - - public WorkflowController(IPersistenceProvider persistenceStore, IDistributedLockProvider lockProvider, IWorkflowRegistry registry, IQueueProvider queueProvider, IExecutionPointerFactory pointerFactory, ILoggerFactory loggerFactory) + public WorkflowController(IPersistenceProvider persistenceStore, IDistributedLockProvider lockProvider, IWorkflowRegistry registry, IQueueProvider queueProvider, IExecutionPointerFactory pointerFactory, ILifeCycleEventHub eventHub, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IDateTimeProvider dateTimeProvider) { _persistenceStore = persistenceStore; _lockProvider = lockProvider; _registry = registry; _queueProvider = queueProvider; _pointerFactory = pointerFactory; + _eventHub = eventHub; + _serviceProvider = serviceProvider; _logger = loggerFactory.CreateLogger(); + _dateTimeProvider = dateTimeProvider; } - public Task StartWorkflow(string workflowId, object data = null) + public Task StartWorkflow(string workflowId, object data = null, string reference=null) { - return StartWorkflow(workflowId, null, data); + return StartWorkflow(workflowId, null, data, reference); } - public Task StartWorkflow(string workflowId, int? version, object data = null) + public Task StartWorkflow(string workflowId, int? version, object data = null, string reference=null) { - return StartWorkflow(workflowId, version, data); + return StartWorkflow(workflowId, version, data, reference); } - public Task StartWorkflow(string workflowId, TData data = null) - where TData : class + public Task StartWorkflow(string workflowId, TData data = null, string reference = null) + where TData : class, new() { - return StartWorkflow(workflowId, null, data); + return StartWorkflow(workflowId, null, data, reference); } - public async Task StartWorkflow(string workflowId, int? version, TData data = null) - where TData : class + public async Task StartWorkflow(string workflowId, int? version, TData data = null, string reference=null) + where TData : class, new() { - + var def = _registry.GetDefinition(workflowId, version); if (def == null) { @@ -63,31 +70,50 @@ public async Task StartWorkflow(string workflowId, int? version, Data = data, Description = def.Description, NextExecution = 0, - CreateTime = DateTime.Now.ToUniversalTime(), - Status = WorkflowStatus.Runnable + CreateTime = _dateTimeProvider.UtcNow, + Status = WorkflowStatus.Runnable, + Reference = reference }; if ((def.DataType != null) && (data == null)) { - wf.Data = TypeExtensions.GetConstructor(def.DataType, new Type[] { }).Invoke(null); + if (typeof(TData) == def.DataType) + wf.Data = new TData(); + else + wf.Data = def.DataType.GetConstructor(new Type[0]).Invoke(new object[0]); } - wf.ExecutionPointers.Add(_pointerFactory.BuildStartingPointer(def)); + wf.ExecutionPointers.Add(_pointerFactory.BuildGenesisPointer(def)); + + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunPreMiddleware(wf, def); + } string id = await _persistenceStore.CreateNewWorkflow(wf); await _queueProvider.QueueWork(id, QueueType.Workflow); + await _queueProvider.QueueWork(id, QueueType.Index); + await _eventHub.PublishNotification(new WorkflowStarted + { + EventTimeUtc = _dateTimeProvider.UtcNow, + Reference = reference, + WorkflowInstanceId = id, + WorkflowDefinitionId = def.Id, + Version = def.Version + }); return id; } public async Task PublishEvent(string eventName, string eventKey, object eventData, DateTime? effectiveDate = null) { - _logger.LogDebug("Creating event {0} {1}", eventName, eventKey); + _logger.LogDebug("Creating event {EventName} {EventKey}", eventName, eventKey); Event evt = new Event(); if (effectiveDate.HasValue) evt.EventTime = effectiveDate.Value.ToUniversalTime(); else - evt.EventTime = DateTime.Now.ToUniversalTime(); + evt.EventTime = _dateTimeProvider.UtcNow; evt.EventData = eventData; evt.EventKey = eventKey; @@ -102,7 +128,7 @@ public async Task SuspendWorkflow(string workflowId) { if (!await _lockProvider.AcquireLock(workflowId, new CancellationToken())) return false; - + try { var wf = await _persistenceStore.GetWorkflowInstance(workflowId); @@ -110,6 +136,15 @@ public async Task SuspendWorkflow(string workflowId) { wf.Status = WorkflowStatus.Suspended; await _persistenceStore.PersistWorkflow(wf); + await _queueProvider.QueueWork(workflowId, QueueType.Index); + await _eventHub.PublishNotification(new WorkflowSuspended + { + EventTimeUtc = _dateTimeProvider.UtcNow, + Reference = wf.Reference, + WorkflowInstanceId = wf.Id, + WorkflowDefinitionId = wf.WorkflowDefinitionId, + Version = wf.Version + }); return true; } @@ -137,6 +172,15 @@ public async Task ResumeWorkflow(string workflowId) wf.Status = WorkflowStatus.Runnable; await _persistenceStore.PersistWorkflow(wf); requeue = true; + await _queueProvider.QueueWork(workflowId, QueueType.Index); + await _eventHub.PublishNotification(new WorkflowResumed + { + EventTimeUtc = _dateTimeProvider.UtcNow, + Reference = wf.Reference, + WorkflowInstanceId = wf.Id, + WorkflowDefinitionId = wf.WorkflowDefinitionId, + Version = wf.Version + }); return true; } @@ -148,8 +192,6 @@ public async Task ResumeWorkflow(string workflowId) if (requeue) await _queueProvider.QueueWork(workflowId, QueueType.Workflow); } - - return false; } public async Task TerminateWorkflow(string workflowId) @@ -162,8 +204,20 @@ public async Task TerminateWorkflow(string workflowId) try { var wf = await _persistenceStore.GetWorkflowInstance(workflowId); + wf.Status = WorkflowStatus.Terminated; + wf.CompleteTime = _dateTimeProvider.UtcNow; + await _persistenceStore.PersistWorkflow(wf); + await _queueProvider.QueueWork(workflowId, QueueType.Index); + await _eventHub.PublishNotification(new WorkflowTerminated + { + EventTimeUtc = _dateTimeProvider.UtcNow, + Reference = wf.Reference, + WorkflowInstanceId = wf.Id, + WorkflowDefinitionId = wf.WorkflowDefinitionId, + Version = wf.Version + }); return true; } finally @@ -171,20 +225,20 @@ public async Task TerminateWorkflow(string workflowId) await _lockProvider.ReleaseLock(workflowId); } } - + public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + where TWorkflow : IWorkflow { - TWorkflow wf = new TWorkflow(); + var wf = ActivatorUtilities.CreateInstance(_serviceProvider); _registry.RegisterWorkflow(wf); } public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + where TWorkflow : IWorkflow where TData : new() { - TWorkflow wf = new TWorkflow(); - _registry.RegisterWorkflow(wf); + var wf = ActivatorUtilities.CreateInstance(_serviceProvider); + _registry.RegisterWorkflow(wf); } } } \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs old mode 100644 new mode 100755 index 53c5918b6..da3e9cd85 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -1,13 +1,14 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; +using System.Threading; using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services { @@ -15,174 +16,190 @@ public class WorkflowExecutor : IWorkflowExecutor { protected readonly IWorkflowRegistry _registry; protected readonly IServiceProvider _serviceProvider; + protected readonly IScopeProvider _scopeProvider; protected readonly IDateTimeProvider _datetimeProvider; protected readonly ILogger _logger; private readonly IExecutionResultProcessor _executionResultProcessor; + private readonly ICancellationProcessor _cancellationProcessor; + private readonly ILifeCycleEventPublisher _publisher; private readonly WorkflowOptions _options; private IWorkflowHost Host => _serviceProvider.GetService(); - public WorkflowExecutor(IWorkflowRegistry registry, IServiceProvider serviceProvider, IDateTimeProvider datetimeProvider, IExecutionResultProcessor executionResultProcessor, WorkflowOptions options, ILoggerFactory loggerFactory) + public WorkflowExecutor(IWorkflowRegistry registry, IServiceProvider serviceProvider, IScopeProvider scopeProvider, IDateTimeProvider datetimeProvider, IExecutionResultProcessor executionResultProcessor, ILifeCycleEventPublisher publisher, ICancellationProcessor cancellationProcessor, WorkflowOptions options, ILoggerFactory loggerFactory) { _serviceProvider = serviceProvider; + _scopeProvider = scopeProvider; _registry = registry; _datetimeProvider = datetimeProvider; + _publisher = publisher; + _cancellationProcessor = cancellationProcessor; _options = options; _logger = loggerFactory.CreateLogger(); _executionResultProcessor = executionResultProcessor; } - public async Task Execute(WorkflowInstance workflow) + public async Task Execute(WorkflowInstance workflow, CancellationToken cancellationToken = default) { var wfResult = new WorkflowExecutorResult(); - var exePointers = new List(workflow.ExecutionPointers.Where(x => x.Active && (!x.SleepUntil.HasValue || x.SleepUntil < _datetimeProvider.Now.ToUniversalTime()))); + var exePointers = new List(workflow.ExecutionPointers.Where(x => x.Active && (!x.SleepUntil.HasValue || x.SleepUntil < _datetimeProvider.UtcNow))); var def = _registry.GetDefinition(workflow.WorkflowDefinitionId, workflow.Version); if (def == null) { - _logger.LogError("Workflow {0} version {1} is not registered", workflow.WorkflowDefinitionId, workflow.Version); + _logger.LogError("Workflow {WorkflowDefinitionId} version {Version} is not registered", workflow.WorkflowDefinitionId, workflow.Version); return wfResult; } + _cancellationProcessor.ProcessCancellations(workflow, def, wfResult); + foreach (var pointer in exePointers) { - var step = def.Steps.First(x => x.Id == pointer.StepId); - if (step != null) + if (!pointer.Active) + continue; + + var step = def.Steps.FindById(pointer.StepId); + if (step == null) { - try - { - pointer.Status = PointerStatus.Running; - switch (step.InitForExecution(wfResult, def, workflow, pointer)) - { - case ExecutionPipelineDirective.Defer: - continue; - case ExecutionPipelineDirective.EndWorkflow: - workflow.Status = WorkflowStatus.Complete; - workflow.CompleteTime = _datetimeProvider.Now.ToUniversalTime(); - continue; - } - - if (!pointer.StartTime.HasValue) - { - pointer.StartTime = _datetimeProvider.Now.ToUniversalTime(); - } - - _logger.LogDebug("Starting step {0} on workflow {1}", step.Name, workflow.Id); - - IStepBody body = step.ConstructBody(_serviceProvider); - - if (body == null) - { - _logger.LogError("Unable to construct step body {0}", step.BodyType.ToString()); - pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(_options.ErrorRetryInterval); - wfResult.Errors.Add(new ExecutionError() - { - WorkflowId = workflow.Id, - ExecutionPointerId = pointer.Id, - ErrorTime = _datetimeProvider.Now.ToUniversalTime(), - Message = String.Format("Unable to construct step body {0}", step.BodyType.ToString()) - }); - continue; - } - - IStepExecutionContext context = new StepExecutionContext() - { - Workflow = workflow, - Step = step, - PersistenceData = pointer.PersistenceData, - ExecutionPointer = pointer, - Item = pointer.ContextItem - }; - - ProcessInputs(workflow, step, body, context); - - switch (step.BeforeExecute(wfResult, context, pointer, body)) - { - case ExecutionPipelineDirective.Defer: - continue; - case ExecutionPipelineDirective.EndWorkflow: - workflow.Status = WorkflowStatus.Complete; - workflow.CompleteTime = _datetimeProvider.Now.ToUniversalTime(); - continue; - } - - var result = await body.RunAsync(context); - - if (result.Proceed) - { - ProcessOutputs(workflow, step, body); - } - - _executionResultProcessor.ProcessExecutionResult(workflow, def, pointer, step, result, wfResult); - step.AfterExecute(wfResult, context, result, pointer); - } - catch (Exception ex) + _logger.LogError("Unable to find step {StepId} in workflow definition", pointer.StepId); + pointer.SleepUntil = _datetimeProvider.UtcNow.Add(_options.ErrorRetryInterval); + wfResult.Errors.Add(new ExecutionError { - _logger.LogError("Workflow {0} raised error on step {1} Message: {2}", workflow.Id, pointer.StepId, ex.Message); - wfResult.Errors.Add(new ExecutionError() - { - WorkflowId = workflow.Id, - ExecutionPointerId = pointer.Id, - ErrorTime = _datetimeProvider.Now.ToUniversalTime(), - Message = ex.Message - }); - - _executionResultProcessor.HandleStepException(workflow, def, pointer, step); - Host.ReportStepError(workflow, step, ex); - } + WorkflowId = workflow.Id, + ExecutionPointerId = pointer.Id, + ErrorTime = _datetimeProvider.UtcNow, + Message = $"Unable to find step {pointer.StepId} in workflow definition" + }); + continue; } - else + + WorkflowActivity.Enrich(step); + try { - _logger.LogError("Unable to find step {0} in workflow definition", pointer.StepId); - pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(_options.ErrorRetryInterval); - wfResult.Errors.Add(new ExecutionError() + if (!InitializeStep(workflow, step, wfResult, def, pointer)) + continue; + + await ExecuteStep(workflow, step, pointer, wfResult, def, cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Workflow {WorkflowId} raised error on step {StepId} Message: {Message}", workflow.Id, pointer.StepId, ex.Message); + wfResult.Errors.Add(new ExecutionError { WorkflowId = workflow.Id, ExecutionPointerId = pointer.Id, - ErrorTime = _datetimeProvider.Now.ToUniversalTime(), - Message = String.Format("Unable to find step {0} in workflow definition", pointer.StepId) + ErrorTime = _datetimeProvider.UtcNow, + Message = ex.Message }); - } + _executionResultProcessor.HandleStepException(workflow, def, pointer, step, ex); + Host.ReportStepError(workflow, step, ex); + } + _cancellationProcessor.ProcessCancellations(workflow, def, wfResult); } ProcessAfterExecutionIteration(workflow, def, wfResult); - DetermineNextExecutionTime(workflow); + await DetermineNextExecutionTime(workflow, def); + + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunExecuteMiddleware(workflow, def); + } return wfResult; } - private void ProcessInputs(WorkflowInstance workflow, WorkflowStep step, IStepBody body, IStepExecutionContext context) + private bool InitializeStep(WorkflowInstance workflow, WorkflowStep step, WorkflowExecutorResult wfResult, WorkflowDefinition def, ExecutionPointer pointer) { - //TODO: Move to own class - foreach (var input in step.Inputs) + switch (step.InitForExecution(wfResult, def, workflow, pointer)) { - var member = (input.Target.Body as MemberExpression); - object resolvedValue = null; + case ExecutionPipelineDirective.Defer: + return false; + case ExecutionPipelineDirective.EndWorkflow: + workflow.Status = WorkflowStatus.Complete; + workflow.CompleteTime = _datetimeProvider.UtcNow; + return false; + } - switch (input.Source.Parameters.Count) + if (pointer.Status != PointerStatus.Running) + { + pointer.Status = PointerStatus.Running; + _publisher.PublishNotification(new StepStarted { - case 1: - resolvedValue = input.Source.Compile().DynamicInvoke(workflow.Data); - break; - case 2: - resolvedValue = input.Source.Compile().DynamicInvoke(workflow.Data, context); - break; - default: - throw new ArgumentException(); - } + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + ExecutionPointerId = pointer.Id, + StepId = step.Id, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version + }); + } - step.BodyType.GetProperty(member.Member.Name).SetValue(body, resolvedValue); + if (!pointer.StartTime.HasValue) + { + pointer.StartTime = _datetimeProvider.UtcNow; } + + return true; } - private void ProcessOutputs(WorkflowInstance workflow, WorkflowStep step, IStepBody body) + private async Task ExecuteStep(WorkflowInstance workflow, WorkflowStep step, ExecutionPointer pointer, WorkflowExecutorResult wfResult, WorkflowDefinition def, CancellationToken cancellationToken = default) { - foreach (var output in step.Outputs) + IStepExecutionContext context = new StepExecutionContext + { + Workflow = workflow, + Step = step, + PersistenceData = pointer.PersistenceData, + ExecutionPointer = pointer, + Item = pointer.ContextItem, + CancellationToken = cancellationToken + }; + + using (var scope = _scopeProvider.CreateScope(context)) { - var member = (output.Target.Body as MemberExpression); - var resolvedValue = output.Source.Compile().DynamicInvoke(body); - var data = workflow.Data; - data.GetType().GetProperty(member.Member.Name).SetValue(data, resolvedValue); + _logger.LogDebug("Starting step {StepName} on workflow {WorkflowId}", step.Name, workflow.Id); + + IStepBody body = step.ConstructBody(scope.ServiceProvider); + var stepExecutor = scope.ServiceProvider.GetRequiredService(); + + if (body == null) + { + _logger.LogError("Unable to construct step body {BodyType}", step.BodyType.ToString()); + pointer.SleepUntil = _datetimeProvider.UtcNow.Add(_options.ErrorRetryInterval); + wfResult.Errors.Add(new ExecutionError + { + WorkflowId = workflow.Id, + ExecutionPointerId = pointer.Id, + ErrorTime = _datetimeProvider.UtcNow, + Message = $"Unable to construct step body {step.BodyType}" + }); + return; + } + + foreach (var input in step.Inputs) + input.AssignInput(workflow.Data, body, context); + + switch (step.BeforeExecute(wfResult, context, pointer, body)) + { + case ExecutionPipelineDirective.Defer: + return; + case ExecutionPipelineDirective.EndWorkflow: + workflow.Status = WorkflowStatus.Complete; + workflow.CompleteTime = _datetimeProvider.UtcNow; + return; + } + + var result = await stepExecutor.ExecuteStep(context, body); + + if (result.Proceed) + { + foreach (var output in step.Outputs) + output.AssignOutput(workflow.Data, body, context); + } + + _executionResultProcessor.ProcessExecutionResult(workflow, def, pointer, step, result, wfResult); + step.AfterExecute(wfResult, context, result, pointer); } } @@ -192,17 +209,20 @@ private void ProcessAfterExecutionIteration(WorkflowInstance workflow, WorkflowD foreach (var pointer in pointers) { - var step = workflowDef.Steps.First(x => x.Id == pointer.StepId); + var step = workflowDef.Steps.FindById(pointer.StepId); step?.AfterWorkflowIteration(workflowResult, workflowDef, workflow, pointer); } } - private void DetermineNextExecutionTime(WorkflowInstance workflow) + private async Task DetermineNextExecutionTime(WorkflowInstance workflow, WorkflowDefinition def) { + //TODO: move to own class workflow.NextExecution = null; if (workflow.Status == WorkflowStatus.Complete) + { return; + } foreach (var pointer in workflow.ExecutionPointers.Where(x => x.Active && (x.Children ?? new List()).Count == 0)) { @@ -212,55 +232,47 @@ private void DetermineNextExecutionTime(WorkflowInstance workflow) return; } - long pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; + var pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; workflow.NextExecution = Math.Min(pointerSleep, workflow.NextExecution ?? pointerSleep); } - if (workflow.NextExecution == null) + foreach (var pointer in workflow.ExecutionPointers.Where(x => x.Active && (x.Children ?? new List()).Count > 0)) { - foreach (var pointer in workflow.ExecutionPointers.Where(x => x.Active && (x.Children ?? new List()).Count > 0)) + if (!workflow.ExecutionPointers.FindByScope(pointer.Id).All(x => x.EndTime.HasValue)) + continue; + + if (!pointer.SleepUntil.HasValue) { - if (workflow.ExecutionPointers.Where(x => pointer.Children.Contains(x.Id)).All(x => IsBranchComplete(workflow.ExecutionPointers, x.Id))) - { - if (!pointer.SleepUntil.HasValue) - { - workflow.NextExecution = 0; - return; - } - - long pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; - workflow.NextExecution = Math.Min(pointerSleep, workflow.NextExecution ?? pointerSleep); - } + workflow.NextExecution = 0; + return; } - } - if ((workflow.NextExecution == null) && (workflow.ExecutionPointers.All(x => x.EndTime != null))) - { - workflow.Status = WorkflowStatus.Complete; - workflow.CompleteTime = _datetimeProvider.Now.ToUniversalTime(); + var pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; + workflow.NextExecution = Math.Min(pointerSleep, workflow.NextExecution ?? pointerSleep); } - } - - private bool IsBranchComplete(IEnumerable pointers, string rootId) - { - //TODO: move to own class - var root = pointers.First(x => x.Id == rootId); - if (root.EndTime == null) + if ((workflow.NextExecution != null) || (workflow.ExecutionPointers.Any(x => x.EndTime == null))) { - return false; + return; } - var list = pointers.Where(x => x.PredecessorId == rootId).ToList(); - - bool result = true; + workflow.Status = WorkflowStatus.Complete; + workflow.CompleteTime = _datetimeProvider.UtcNow; - foreach (var item in list) + using (var scope = _serviceProvider.CreateScope()) { - result = result && IsBranchComplete(pointers, item.Id); + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunPostMiddleware(workflow, def); } - return result; + _publisher.PublishNotification(new WorkflowCompleted + { + EventTimeUtc = _datetimeProvider.UtcNow, + Reference = workflow.Reference, + WorkflowInstanceId = workflow.Id, + WorkflowDefinitionId = workflow.WorkflowDefinitionId, + Version = workflow.Version + }); } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowHost.cs b/src/WorkflowCore/Services/WorkflowHost.cs index f9cc2d1e8..73c8850fa 100644 --- a/src/WorkflowCore/Services/WorkflowHost.cs +++ b/src/WorkflowCore/Services/WorkflowHost.cs @@ -4,22 +4,24 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; using WorkflowCore.Interface; using WorkflowCore.Models; -using System.Reflection; -using WorkflowCore.Exceptions; +using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services { public class WorkflowHost : IWorkflowHost, IDisposable - { - protected bool _shutdown = true; + { + protected bool _shutdown = true; protected readonly IServiceProvider _serviceProvider; private readonly IEnumerable _backgroundTasks; private readonly IWorkflowController _workflowController; + private readonly IActivityController _activityController; public event StepErrorEventHandler OnStepError; + public event LifeCycleEventHandler OnLifeCycleEvent; // Public dependencies to allow for extension method access. public IPersistenceProvider PersistenceStore { get; private set; } @@ -29,7 +31,10 @@ public class WorkflowHost : IWorkflowHost, IDisposable public IQueueProvider QueueProvider { get; private set; } public ILogger Logger { get; private set; } - public WorkflowHost(IPersistenceProvider persistenceStore, IQueueProvider queueProvider, WorkflowOptions options, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, IEnumerable backgroundTasks, IWorkflowController workflowController) + private readonly ILifeCycleEventHub _lifeCycleEventHub; + private readonly ISearchIndex _searchIndex; + + public WorkflowHost(IPersistenceProvider persistenceStore, IQueueProvider queueProvider, WorkflowOptions options, ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IWorkflowRegistry registry, IDistributedLockProvider lockProvider, IEnumerable backgroundTasks, IWorkflowController workflowController, ILifeCycleEventHub lifeCycleEventHub, ISearchIndex searchIndex, IActivityController activityController) { PersistenceStore = persistenceStore; QueueProvider = queueProvider; @@ -40,30 +45,31 @@ public WorkflowHost(IPersistenceProvider persistenceStore, IQueueProvider queueP LockProvider = lockProvider; _backgroundTasks = backgroundTasks; _workflowController = workflowController; - persistenceStore.EnsureStoreExists(); + _searchIndex = searchIndex; + _activityController = activityController; + _lifeCycleEventHub = lifeCycleEventHub; } - - public Task StartWorkflow(string workflowId, object data = null) + + public Task StartWorkflow(string workflowId, object data = null, string reference=null) { - return _workflowController.StartWorkflow(workflowId, data); + return _workflowController.StartWorkflow(workflowId, data, reference); } - public Task StartWorkflow(string workflowId, int? version, object data = null) + public Task StartWorkflow(string workflowId, int? version, object data = null, string reference=null) { - return _workflowController.StartWorkflow(workflowId, version, data); + return _workflowController.StartWorkflow(workflowId, version, data, reference); } - public Task StartWorkflow(string workflowId, TData data = null) - where TData : class + public Task StartWorkflow(string workflowId, TData data = null, string reference=null) + where TData : class, new() { - return _workflowController.StartWorkflow(workflowId, null, data); + return _workflowController.StartWorkflow(workflowId, null, data, reference); } - - - public Task StartWorkflow(string workflowId, int? version, TData data = null) - where TData : class + + public Task StartWorkflow(string workflowId, int? version, TData data = null, string reference=null) + where TData : class, new() { - return _workflowController.StartWorkflow(workflowId, version, data); + return _workflowController.StartWorkflow(workflowId, version, data, reference); } public Task PublishEvent(string eventName, string eventKey, object eventData, DateTime? effectiveDate = null) @@ -72,45 +78,74 @@ public Task PublishEvent(string eventName, string eventKey, object eventData, Da } public void Start() - { - _shutdown = false; - PersistenceStore.EnsureStoreExists(); - QueueProvider.Start().Wait(); - LockProvider.Start().Wait(); - - Logger.LogInformation("Starting backgroud tasks"); - - foreach (var task in _backgroundTasks) - task.Start(); + { + StartAsync(CancellationToken.None).Wait(); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var activity = WorkflowActivity.StartHost(); + try + { + _shutdown = false; + PersistenceStore.EnsureStoreExists(); + await QueueProvider.Start(); + await LockProvider.Start(); + await _lifeCycleEventHub.Start(); + await _searchIndex.Start(); + + // Event subscriptions are removed when stopping the event hub. + // Add them when starting. + AddEventSubscriptions(); + + Logger.LogInformation("Starting background tasks"); + + foreach (var task in _backgroundTasks) + task.Start(); + } + catch (Exception ex) + { + activity.RecordException(ex); + throw; + } + finally + { + activity?.Dispose(); + } } public void Stop() { - _shutdown = true; - + StopAsync(CancellationToken.None).Wait(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _shutdown = true; + Logger.LogInformation("Stopping background tasks"); foreach (var th in _backgroundTasks) th.Stop(); - + Logger.LogInformation("Worker tasks stopped"); - - QueueProvider.Stop(); - LockProvider.Stop(); + + await QueueProvider.Stop(); + await LockProvider.Stop(); + await _searchIndex.Stop(); + await _lifeCycleEventHub.Stop(); } - - public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + + public void RegisterWorkflow() + where TWorkflow : IWorkflow { - TWorkflow wf = new TWorkflow(); - Registry.RegisterWorkflow(wf); + _workflowController.RegisterWorkflow(); } - public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + public void RegisterWorkflow() + where TWorkflow : IWorkflow where TData : new() { - TWorkflow wf = new TWorkflow(); - Registry.RegisterWorkflow(wf); + _workflowController.RegisterWorkflow(); } public Task SuspendWorkflow(string workflowId) @@ -128,6 +163,11 @@ public Task TerminateWorkflow(string workflowId) return _workflowController.TerminateWorkflow(workflowId); } + public void HandleLifeCycleEvent(LifeCycleEvent evt) + { + OnLifeCycleEvent?.Invoke(evt); + } + public void ReportStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) { OnStepError?.Invoke(workflow, step, exception); @@ -138,5 +178,30 @@ public void Dispose() if (!_shutdown) Stop(); } + + public Task GetPendingActivity(string activityName, string workerId, TimeSpan? timeout = null) + { + return _activityController.GetPendingActivity(activityName, workerId, timeout); + } + + public Task ReleaseActivityToken(string token) + { + return _activityController.ReleaseActivityToken(token); + } + + public Task SubmitActivitySuccess(string token, object result) + { + return _activityController.SubmitActivitySuccess(token, result); + } + + public Task SubmitActivityFailure(string token, object result) + { + return _activityController.SubmitActivityFailure(token, result); + } + + private void AddEventSubscriptions() + { + _lifeCycleEventHub.Subscribe(HandleLifeCycleEvent); + } } } diff --git a/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs new file mode 100644 index 000000000..d904c5aa5 --- /dev/null +++ b/src/WorkflowCore/Services/WorkflowMiddlewareRunner.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Services +{ + /// + public class WorkflowMiddlewareRunner : IWorkflowMiddlewareRunner + { + private static readonly WorkflowDelegate NoopWorkflowDelegate = () => Task.CompletedTask; + private readonly IEnumerable _middleware; + private readonly IServiceProvider _serviceProvider; + + public WorkflowMiddlewareRunner( + IEnumerable middleware, + IServiceProvider serviceProvider) + { + _middleware = middleware; + _serviceProvider = serviceProvider; + } + + /// + public async Task RunPreMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + var preMiddleware = _middleware + .Where(m => m.Phase == WorkflowMiddlewarePhase.PreWorkflow); + + await RunWorkflowMiddleware(workflow, preMiddleware); + } + + /// + public Task RunPostMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + return RunWorkflowMiddlewareWithErrorHandling( + workflow, + WorkflowMiddlewarePhase.PostWorkflow, + def.OnPostMiddlewareError); + } + + /// + public Task RunExecuteMiddleware(WorkflowInstance workflow, WorkflowDefinition def) + { + return RunWorkflowMiddlewareWithErrorHandling( + workflow, + WorkflowMiddlewarePhase.ExecuteWorkflow, + def.OnExecuteMiddlewareError); + } + + public async Task RunWorkflowMiddlewareWithErrorHandling( + WorkflowInstance workflow, + WorkflowMiddlewarePhase phase, + Type middlewareErrorType) + { + var middleware = _middleware.Where(m => m.Phase == phase); + + try + { + await RunWorkflowMiddleware(workflow, middleware); + } + catch (Exception exception) + { + var errorHandlerType = middlewareErrorType ?? typeof(IWorkflowMiddlewareErrorHandler); + + using (var scope = _serviceProvider.CreateScope()) + { + var typeInstance = scope.ServiceProvider.GetService(errorHandlerType); + if (typeInstance is IWorkflowMiddlewareErrorHandler handler) + { + await handler.HandleAsync(exception); + } + } + } + } + + private static Task RunWorkflowMiddleware( + WorkflowInstance workflow, + IEnumerable middlewareCollection) + { + return middlewareCollection + .Reverse() + .Aggregate( + NoopWorkflowDelegate, + (previous, middleware) => () => middleware.HandleAsync(workflow, previous))(); + } + } +} diff --git a/src/WorkflowCore/Services/WorkflowRegistry.cs b/src/WorkflowCore/Services/WorkflowRegistry.cs index a16a3cd2c..beed19c0e 100644 --- a/src/WorkflowCore/Services/WorkflowRegistry.cs +++ b/src/WorkflowCore/Services/WorkflowRegistry.cs @@ -1,6 +1,8 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -8,8 +10,9 @@ namespace WorkflowCore.Services { public class WorkflowRegistry : IWorkflowRegistry { - private readonly IServiceProvider _serviceProvider; - private readonly List> _registry = new List>(); + private readonly IServiceProvider _serviceProvider; + private readonly ConcurrentDictionary _registry = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _lastestVersion = new ConcurrentDictionary(); public WorkflowRegistry(IServiceProvider serviceProvider) { @@ -20,63 +23,83 @@ public WorkflowDefinition GetDefinition(string workflowId, int? version = null) { if (version.HasValue) { - var entry = _registry.FirstOrDefault(x => x.Item1 == workflowId && x.Item2 == version.Value); - if (entry == null) - { - return null; - } - - // TODO: What in the heack does Item3 mean? - return entry.Item3; + if (!_registry.ContainsKey($"{workflowId}-{version}")) + return default; + return _registry[$"{workflowId}-{version}"]; } else { - int maxVersion = _registry.Where(x => x.Item1 == workflowId).Max(x => x.Item2); - var entry = _registry.FirstOrDefault(x => x.Item1 == workflowId && x.Item2 == maxVersion); - if (entry == null) - { - return null; - } - - return entry.Item3; + if (!_lastestVersion.ContainsKey(workflowId)) + return default; + return _lastestVersion[workflowId]; } } - public void RegisterWorkflow(IWorkflow workflow) + public void DeregisterWorkflow(string workflowId, int version) { - if (_registry.Any(x => x.Item1 == workflow.Id && x.Item2 == workflow.Version)) + if (!_registry.ContainsKey($"{workflowId}-{version}")) + return; + + lock (_registry) { - throw new InvalidOperationException($"Workflow {workflow.Id} version {workflow.Version} is already registered"); + _registry.TryRemove($"{workflowId}-{version}", out var _); + if (_lastestVersion[workflowId].Version == version) + { + _lastestVersion.TryRemove(workflowId, out var _); + + var latest = _registry.Values.Where(x => x.Id == workflowId).OrderByDescending(x => x.Version).FirstOrDefault(); + if (latest != default) + _lastestVersion[workflowId] = latest; + } } + } - var builder = (_serviceProvider.GetService(typeof(IWorkflowBuilder)) as IWorkflowBuilder).UseData(); + public void RegisterWorkflow(IWorkflow workflow) + { + var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); - _registry.Add(new Tuple(workflow.Id, workflow.Version, def)); + RegisterWorkflow(def); } public void RegisterWorkflow(WorkflowDefinition definition) { - if (_registry.Any(x => x.Item1 == definition.Id && x.Item2 == definition.Version)) + if (_registry.ContainsKey($"{definition.Id}-{definition.Version}")) { throw new InvalidOperationException($"Workflow {definition.Id} version {definition.Version} is already registered"); } - _registry.Add(new Tuple(definition.Id, definition.Version, definition)); + lock (_registry) + { + _registry[$"{definition.Id}-{definition.Version}"] = definition; + if (!_lastestVersion.ContainsKey(definition.Id)) + { + _lastestVersion[definition.Id] = definition; + return; + } + + if (_lastestVersion[definition.Id].Version <= definition.Version) + _lastestVersion[definition.Id] = definition; + } } public void RegisterWorkflow(IWorkflow workflow) where TData : new() { - if (_registry.Any(x => x.Item1 == workflow.Id && x.Item2 == workflow.Version)) - { - throw new InvalidOperationException($"Workflow {workflow.Id} version {workflow.Version} is already registed"); - } - - var builder = (_serviceProvider.GetService(typeof(IWorkflowBuilder)) as IWorkflowBuilder).UseData(); + var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); - _registry.Add(new Tuple(workflow.Id, workflow.Version, def)); + RegisterWorkflow(def); + } + + public bool IsRegistered(string workflowId, int version) + { + return _registry.ContainsKey($"{workflowId}-{version}"); + } + + public IEnumerable GetAllDefinitions() + { + return _registry.Values; } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/WorkflowCore.csproj b/src/WorkflowCore/WorkflowCore.csproj index a2b677f4a..bb567d87c 100644 --- a/src/WorkflowCore/WorkflowCore.csproj +++ b/src/WorkflowCore/WorkflowCore.csproj @@ -2,46 +2,39 @@ Workflow Core - 1.4.0 Daniel Gerlag - netstandard1.3 + netstandard2.0 WorkflowCore WorkflowCore workflow;.NET;Core;state machine - https://github.com/danielgerlag/workflow-core - https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md - git - https://github.com/danielgerlag/workflow-core.git false false false Workflow Core is a light weight workflow engine targeting .NET Standard. - 1.6.0 - 1.6.0.0 - 1.6.0.0 - - - - - - - + + + + + + - - - + - + + + + + <_Parameter1>WorkflowCore.IntegrationTests + - - - + + diff --git a/src/extensions/WorkflowCore.Users/Interface/IUserTaskBuilder.cs b/src/extensions/WorkflowCore.Users/Interface/IUserTaskBuilder.cs index e1df05ac6..9451ced29 100644 --- a/src/extensions/WorkflowCore.Users/Interface/IUserTaskBuilder.cs +++ b/src/extensions/WorkflowCore.Users/Interface/IUserTaskBuilder.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq.Expressions; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Primitives; using WorkflowCore.Users.Primitives; -using WorkflowCore.Users.Services; namespace WorkflowCore.Users.Interface { diff --git a/src/extensions/WorkflowCore.Users/Interface/IUserTaskReturnBuilder.cs b/src/extensions/WorkflowCore.Users/Interface/IUserTaskReturnBuilder.cs index b6eed494c..37ee22b18 100644 --- a/src/extensions/WorkflowCore.Users/Interface/IUserTaskReturnBuilder.cs +++ b/src/extensions/WorkflowCore.Users/Interface/IUserTaskReturnBuilder.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Primitives; diff --git a/src/extensions/WorkflowCore.Users/Models/OpenUserAction.cs b/src/extensions/WorkflowCore.Users/Models/OpenUserAction.cs index 5d2ed8b8a..1452452a1 100644 --- a/src/extensions/WorkflowCore.Users/Models/OpenUserAction.cs +++ b/src/extensions/WorkflowCore.Users/Models/OpenUserAction.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Users.Models { diff --git a/src/extensions/WorkflowCore.Users/Models/UserAction.cs b/src/extensions/WorkflowCore.Users/Models/UserAction.cs index 61d5b4c48..d4d769824 100644 --- a/src/extensions/WorkflowCore.Users/Models/UserAction.cs +++ b/src/extensions/WorkflowCore.Users/Models/UserAction.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Users.Models { diff --git a/src/extensions/WorkflowCore.Users/Models/UserStep.cs b/src/extensions/WorkflowCore.Users/Models/UserStep.cs index e1f4b0c09..b8c147f65 100644 --- a/src/extensions/WorkflowCore.Users/Models/UserStep.cs +++ b/src/extensions/WorkflowCore.Users/Models/UserStep.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/extensions/WorkflowCore.Users/Models/UserStepContainer.cs b/src/extensions/WorkflowCore.Users/Models/UserStepContainer.cs index 9d308b7b5..e4238b9cc 100644 --- a/src/extensions/WorkflowCore.Users/Models/UserStepContainer.cs +++ b/src/extensions/WorkflowCore.Users/Models/UserStepContainer.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -28,7 +27,10 @@ public override ExecutionPipelineDirective InitForExecution(WorkflowExecutorResu Dictionary userOptions = new Dictionary(); foreach (var outcome in Outcomes) { - userOptions[outcome.Label ?? Convert.ToString(outcome.GetValue(workflow.Data) ?? "Proceed")] = outcome.GetValue(workflow.Data); + if (outcome is ValueOutcome) + { + userOptions[outcome.Label ?? Convert.ToString((outcome as ValueOutcome).GetValue(workflow.Data) ?? "Proceed")] = (outcome as ValueOutcome).GetValue(workflow.Data); + } } executionPointer.ExtensionAttributes["UserOptions"] = userOptions; @@ -36,7 +38,7 @@ public override ExecutionPipelineDirective InitForExecution(WorkflowExecutorResu executionPointer.EventName = "UserAction"; executionPointer.Active = false; - executorResult.Subscriptions.Add(new EventSubscription() + executorResult.Subscriptions.Add(new EventSubscription { WorkflowId = workflow.Id, StepId = executionPointer.StepId, diff --git a/src/extensions/WorkflowCore.Users/Primitives/Escalate.cs b/src/extensions/WorkflowCore.Users/Primitives/Escalate.cs index 67603556f..7a400bb87 100644 --- a/src/extensions/WorkflowCore.Users/Primitives/Escalate.cs +++ b/src/extensions/WorkflowCore.Users/Primitives/Escalate.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -16,7 +14,7 @@ public override ExecutionResult Run(IStepExecutionContext context) { if (context.PersistenceData != null) { - var taskStep = context.Workflow.ExecutionPointers.Find(x => x.Id == context.ExecutionPointer.PredecessorId); + var taskStep = context.Workflow.ExecutionPointers.FindById(context.ExecutionPointer.PredecessorId); if (!taskStep.EventPublished) { diff --git a/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs b/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs index f05194c9e..a56e000d3 100644 --- a/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs +++ b/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text; -using WorkflowCore.Interface; using WorkflowCore.Models; namespace WorkflowCore.Users.Primitives @@ -13,7 +9,7 @@ public class EscalateStep : WorkflowStep public override void AfterWorkflowIteration(WorkflowExecutorResult executorResult, WorkflowDefinition defintion, WorkflowInstance workflow, ExecutionPointer executionPointer) { base.AfterWorkflowIteration(executorResult, defintion, workflow, executionPointer); - var taskStep = workflow.ExecutionPointers.Find(x => x.Id == executionPointer.PredecessorId); + var taskStep = workflow.ExecutionPointers.FindById(executionPointer.PredecessorId); if (taskStep.EventPublished) { diff --git a/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs b/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs index ee757cd6d..7772a894c 100644 --- a/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs +++ b/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs @@ -50,18 +50,14 @@ public override ExecutionResult Run(IStepExecutionContext context) if (context.PersistenceData == null) { - var result = ExecutionResult.Branch(new List() { null }, new ControlPersistenceData() { ChildrenActive = true }); + var result = ExecutionResult.Branch(new List { context.Item }, new ControlPersistenceData { ChildrenActive = true }); result.OutcomeValue = action.OutcomeValue; return result; } if ((context.PersistenceData is ControlPersistenceData) && ((context.PersistenceData as ControlPersistenceData).ChildrenActive)) { - bool complete = true; - foreach (var childId in context.ExecutionPointer.Children) - complete = complete && IsBranchComplete(context.Workflow.ExecutionPointers, childId); - - if (complete) + if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) return ExecutionResult.Next(); else { @@ -78,13 +74,15 @@ private void SetupEscalations(IStepExecutionContext context) { foreach (var esc in _escalations) { - context.Workflow.ExecutionPointers.Add(new ExecutionPointer() + context.Workflow.ExecutionPointers.Add(new ExecutionPointer { Active = true, Id = Guid.NewGuid().ToString(), PredecessorId = context.ExecutionPointer.Id, StepId = esc.Id, - StepName = esc.Name + StepName = esc.Name, + Status = PointerStatus.Pending, + Scope = new List(context.ExecutionPointer.Scope) }); } } diff --git a/src/extensions/WorkflowCore.Users/Primitives/UserTaskStep.cs b/src/extensions/WorkflowCore.Users/Primitives/UserTaskStep.cs index f12696d41..cc568019b 100644 --- a/src/extensions/WorkflowCore.Users/Primitives/UserTaskStep.cs +++ b/src/extensions/WorkflowCore.Users/Primitives/UserTaskStep.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/extensions/WorkflowCore.Users/Properties/AssemblyInfo.cs b/src/extensions/WorkflowCore.Users/Properties/AssemblyInfo.cs index 4a776a642..fa59636da 100644 --- a/src/extensions/WorkflowCore.Users/Properties/AssemblyInfo.cs +++ b/src/extensions/WorkflowCore.Users/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/extensions/WorkflowCore.Users/ServiceExtensions/StepBuilderExtensions.cs b/src/extensions/WorkflowCore.Users/ServiceExtensions/StepBuilderExtensions.cs index 8221c5da9..6f3f42935 100644 --- a/src/extensions/WorkflowCore.Users/ServiceExtensions/StepBuilderExtensions.cs +++ b/src/extensions/WorkflowCore.Users/ServiceExtensions/StepBuilderExtensions.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; -using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Primitives; using WorkflowCore.Services; using WorkflowCore.Users.Interface; using WorkflowCore.Users.Models; @@ -22,7 +18,7 @@ public static IStepBuilder UserStep(this ISte { var newStep = new UserStepContainer(); newStep.Principal = assigner; - newStep.UserPrompt = userPrompt; + newStep.UserPrompt = userPrompt; builder.WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(builder.WorkflowBuilder, newStep); @@ -30,7 +26,7 @@ public static IStepBuilder UserStep(this ISte stepSetup.Invoke(stepBuilder); newStep.Name = newStep.Name ?? typeof(UserStepContainer).Name; - builder.Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -64,7 +60,25 @@ public static IUserTaskBuilder UserTask(this IStepBuild stepSetup.Invoke(stepBuilder); newStep.Name = newStep.Name ?? typeof(UserTask).Name; - builder.Step.Outcomes.Add(new StepOutcome() { NextStep = newStep.Id }); + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); + + return stepBuilder; + } + + public static IUserTaskBuilder UserTask(this IStepBuilder builder, string userPrompt, Expression> assigner, Action> stepSetup = null) + where TStepBody : IStepBody + { + var newStep = new UserTaskStep(); + builder.WorkflowBuilder.AddStep(newStep); + var stepBuilder = new UserTaskBuilder(builder.WorkflowBuilder, newStep); + stepBuilder.Input(step => step.AssignedPrincipal, assigner); + stepBuilder.Input(step => step.Prompt, data => userPrompt); + + if (stepSetup != null) + stepSetup.Invoke(stepBuilder); + + newStep.Name = newStep.Name ?? typeof(UserTask).Name; + builder.Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } diff --git a/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowHostExtensions.cs b/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowHostExtensions.cs index dbb44becc..606d5592f 100644 --- a/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowHostExtensions.cs +++ b/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowHostExtensions.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; -using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Services; using WorkflowCore.Users.Models; using WorkflowCore.Users.Primitives; @@ -15,7 +12,7 @@ public static class WorkflowHostExtensions { public static async Task PublishUserAction(this IWorkflowHost host, string actionKey, string user, object value) { - UserAction data = new UserAction() + UserAction data = new UserAction { User = user, OutcomeValue = value diff --git a/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowInstanceExtensions.cs b/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowInstanceExtensions.cs index de44c011b..464cedd14 100644 --- a/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowInstanceExtensions.cs +++ b/src/extensions/WorkflowCore.Users/ServiceExtensions/WorkflowInstanceExtensions.cs @@ -14,7 +14,7 @@ public static IEnumerable GetOpenUserActions(this WorkflowInstan var pointers = workflow.ExecutionPointers.Where(x => !x.EventPublished && x.EventName == UserTask.EventName).ToList(); foreach (var pointer in pointers) { - var item = new OpenUserAction() + var item = new OpenUserAction { Key = pointer.EventKey, Prompt = Convert.ToString(pointer.ExtensionAttributes[UserTask.ExtPrompt]), diff --git a/src/extensions/WorkflowCore.Users/Services/UserTaskBuilder.cs b/src/extensions/WorkflowCore.Users/Services/UserTaskBuilder.cs index 2bb232470..ccba7b239 100644 --- a/src/extensions/WorkflowCore.Users/Services/UserTaskBuilder.cs +++ b/src/extensions/WorkflowCore.Users/Services/UserTaskBuilder.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq.Expressions; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Primitives; @@ -26,11 +24,7 @@ public IUserTaskReturnBuilder WithOption(string value, string label) var newStep = new WorkflowStep(); Expression> inputExpr = (x => x.ExpectedOutcome); Expression> valueExpr = (x => value); - var mapping = new DataMapping() - { - Source = valueExpr, - Target = inputExpr - }; + var mapping = new MemberMapParameter(valueExpr, inputExpr); newStep.Inputs.Add(mapping); WorkflowBuilder.AddStep(newStep); @@ -57,7 +51,7 @@ public IUserTaskBuilder WithEscalation(Expression> var lastStep = WorkflowBuilder.LastStep; action.Invoke(WorkflowBuilder); if (WorkflowBuilder.LastStep > lastStep) - newStep.Outcomes.Add(new StepOutcome() { NextStep = lastStep + 1 }); + newStep.Outcomes.Add(new ValueOutcome { NextStep = lastStep + 1 }); } return this; diff --git a/src/extensions/WorkflowCore.Users/Services/UserTaskReturnBuilder.cs b/src/extensions/WorkflowCore.Users/Services/UserTaskReturnBuilder.cs index 70438ba8b..6bc06be4b 100644 --- a/src/extensions/WorkflowCore.Users/Services/UserTaskReturnBuilder.cs +++ b/src/extensions/WorkflowCore.Users/Services/UserTaskReturnBuilder.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Primitives; diff --git a/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj b/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj index 59dfd0151..66c96f4ec 100644 --- a/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj +++ b/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj @@ -2,9 +2,8 @@ Workflow Core extensions for human workflow - 1.1.0 Daniel Gerlag - netstandard1.3 + netstandard2.0 WorkflowCore.Users WorkflowCore.Users workflow;.NET;Core;state machine;WorkflowCore;human;user @@ -12,15 +11,10 @@ https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git https://github.com/danielgerlag/workflow-core.git - 1.6.1 - $(PackageTargetFallback);dnxcore50 false false false Provides extensions for Workflow Core to enable human workflows. - 1.3.0 - 1.3.0.0 - 1.3.0.0 @@ -28,7 +22,7 @@ - + diff --git a/src/extensions/WorkflowCore.WebAPI/Controllers/EventsController.cs b/src/extensions/WorkflowCore.WebAPI/Controllers/EventsController.cs index f2ec00029..4930aa22d 100644 --- a/src/extensions/WorkflowCore.WebAPI/Controllers/EventsController.cs +++ b/src/extensions/WorkflowCore.WebAPI/Controllers/EventsController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using WorkflowCore.Interface; diff --git a/src/extensions/WorkflowCore.WebAPI/Controllers/WorkflowsController.cs b/src/extensions/WorkflowCore.WebAPI/Controllers/WorkflowsController.cs index ef4da1aa3..3c88df3fb 100644 --- a/src/extensions/WorkflowCore.WebAPI/Controllers/WorkflowsController.cs +++ b/src/extensions/WorkflowCore.WebAPI/Controllers/WorkflowsController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -45,7 +44,7 @@ public async Task Get(string id) [HttpPost("{id}")] [HttpPost("{id}/{version}")] - public async Task Post(string id, int? version, [FromBody]JObject data) + public async Task Post(string id, int? version, string reference, [FromBody]JObject data) { string workflowId = null; var def = _registry.GetDefinition(id, version); @@ -55,11 +54,11 @@ public async Task Post(string id, int? version, [FromBody]JObject { var dataStr = JsonConvert.SerializeObject(data); var dataObj = JsonConvert.DeserializeObject(dataStr, def.DataType); - workflowId = await _workflowHost.StartWorkflow(id, version, dataObj); + workflowId = await _workflowHost.StartWorkflow(id, version, dataObj, reference); } else { - workflowId = await _workflowHost.StartWorkflow(id, version, null); + workflowId = await _workflowHost.StartWorkflow(id, version, null, reference); } return Ok(workflowId); diff --git a/src/extensions/WorkflowCore.WebAPI/Properties/AssemblyInfo.cs b/src/extensions/WorkflowCore.WebAPI/Properties/AssemblyInfo.cs index 3ad8e3878..b5a5d4e35 100644 --- a/src/extensions/WorkflowCore.WebAPI/Properties/AssemblyInfo.cs +++ b/src/extensions/WorkflowCore.WebAPI/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/extensions/WorkflowCore.WebAPI/ServiceCollectionExtensions.cs b/src/extensions/WorkflowCore.WebAPI/ServiceCollectionExtensions.cs deleted file mode 100644 index bfe6bbd73..000000000 --- a/src/extensions/WorkflowCore.WebAPI/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class ServiceCollectionExtensions - { - - } -} diff --git a/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj b/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj index 325e4012f..f6eb6ea92 100644 --- a/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj +++ b/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj @@ -2,9 +2,8 @@ Workflow Core REST API - 1.1.0 Daniel Gerlag - netstandard1.6 + netstandard2.0 WorkflowCore.WebAPI WorkflowCore.WebAPI workflow;.NET;Core;state machine;WorkflowCore;REST;API @@ -12,12 +11,9 @@ https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git https://github.com/danielgerlag/workflow-core.git - 1.6.1 - $(PackageTargetFallback);dnxcore50 false false false - 1.3.0 WebAPI wrapper for Workflow host @@ -28,7 +24,7 @@ - + diff --git a/src/logo.png b/src/logo.png new file mode 100644 index 000000000..aadfc93fa Binary files /dev/null and b/src/logo.png differ diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/Models/Lock.cs b/src/providers/WorkflowCore.LockProviders.Redlock/Models/Lock.cs deleted file mode 100644 index 01e460d69..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/Models/Lock.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Adapted from https://github.com/KidFashion/redlock-cs - */ -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace WorkflowCore.LockProviders.Redlock.Models -{ - public class Lock - { - - public Lock(RedisKey resource, RedisValue val, TimeSpan validity) - { - this.resource = resource; - this.val = val; - this.validity_time = validity; - } - - private RedisKey resource; - - private RedisValue val; - - private TimeSpan validity_time; - - public RedisKey Resource { get { return resource; } } - - public RedisValue Value { get { return val; } } - - public TimeSpan Validity { get { return validity_time; } } - } -} diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.LockProviders.Redlock/Properties/AssemblyInfo.cs deleted file mode 100644 index 4c574c709..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("WorkflowCore.LockProviders.Redlock")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("05250d58-a59e-4212-8d55-e7bc0396e9f5")] diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/README.md b/src/providers/WorkflowCore.LockProviders.Redlock/README.md deleted file mode 100644 index 166706ee6..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Redis Relock DLM provider for Workflow Core - -Provides [DLM](https://en.wikipedia.org/wiki/Distributed_lock_manager) support on [Workflow Core](../../README.md) using [Redis Redlock](http://redis.io/topics/distlock). - -This makes it possible to have a cluster of nodes processing your workflows, along with a queue provider. - -## Installing - -Install the NuGet package "WorkflowCore.LockProviders.Redlock" - -``` -PM> Install-Package WorkflowCore.LockProviders.Redlock -``` - -## Usage - -Use the .UseRedlock extension method when building your service provider. - -```C# -redis = ConnectionMultiplexer.Connect("127.0.0.1"); -services.AddWorkflow(x => x.UseRedlock(redis)); -``` - -*Adapted from https://github.com/KidFashion/redlock-cs* diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.LockProviders.Redlock/ServiceCollectionExtensions.cs deleted file mode 100644 index 50ae19886..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using WorkflowCore.Models; -using WorkflowCore.LockProviders.Redlock.Services; -using StackExchange.Redis; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class ServiceCollectionExtensions - { - public static WorkflowOptions UseRedlock(this WorkflowOptions options, IConnectionMultiplexer connectionMultiplexer) - { - options.UseDistributedLockManager(sp => new RedlockProvider(connectionMultiplexer)); - return options; - } - } -} diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/Services/RedlockProvider.cs b/src/providers/WorkflowCore.LockProviders.Redlock/Services/RedlockProvider.cs deleted file mode 100644 index 566ab6d9c..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/Services/RedlockProvider.cs +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Adapted from https://github.com/KidFashion/redlock-cs - */ -using StackExchange.Redis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using WorkflowCore.Interface; -using WorkflowCore.LockProviders.Redlock.Models; - -namespace WorkflowCore.LockProviders.Redlock.Services -{ -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public class RedlockProvider : IDistributedLockProvider - { - const int DefaultRetryCount = 3; - readonly TimeSpan DefaultRetryDelay = new TimeSpan(0, 0, 0, 0, 200); - const double ClockDriveFactor = 0.01; - - protected int Quorum { get { return (redisMasterDictionary.Count / 2) + 1; } } - - const String UnlockScript = @" - if redis.call(""get"",KEYS[1]) == ARGV[1] then - return redis.call(""del"",KEYS[1]) - else - return 0 - end"; - - private static List OwnLocks = new List(); - - public RedlockProvider(params IConnectionMultiplexer[] list) - { - foreach (var item in list) - this.redisMasterDictionary.Add(item.GetEndPoints().First().ToString(), item); - } - - public async Task AcquireLock(string Id, CancellationToken cancellationToken) - { - Lock lockObject = null; - if (Lock(Id, TimeSpan.FromMinutes(30), out lockObject)) - { - OwnLocks.Add(lockObject); - return true; - } - return false; - } - - public async Task ReleaseLock(string Id) - { - var list = OwnLocks.Where(x => x.Resource == Id).ToList(); - foreach (var lockObject in list) - { - Unlock(lockObject); - OwnLocks.Remove(lockObject); - } - } - - protected static byte[] CreateUniqueLockId() - { - return Guid.NewGuid().ToByteArray(); - } - - - protected Dictionary redisMasterDictionary = new Dictionary(); - - //TODO: Refactor passing a ConnectionMultiplexer - protected bool LockInstance(string redisServer, string resource, byte[] val, TimeSpan ttl) - { - - bool succeeded; - try - { - var redis = this.redisMasterDictionary[redisServer]; - succeeded = redis.GetDatabase().StringSet(resource, val, ttl, When.NotExists); - } - catch (Exception) - { - succeeded = false; - } - return succeeded; - } - - //TODO: Refactor passing a ConnectionMultiplexer - protected void UnlockInstance(string redisServer, string resource, byte[] val) - { - RedisKey[] key = { resource }; - RedisValue[] values = { val }; - var redis = redisMasterDictionary[redisServer]; - redis.GetDatabase().ScriptEvaluate( - UnlockScript, - key, - values - ); - } - - protected bool Lock(RedisKey resource, TimeSpan ttl, out Lock lockObject) - { - var val = CreateUniqueLockId(); - Lock innerLock = null; - bool successfull = retry(DefaultRetryCount, DefaultRetryDelay, () => - { - try - { - int n = 0; - var startTime = DateTime.Now; - - // Use keys - for_each_redis_registered( - redis => - { - if (LockInstance(redis, resource, val, ttl)) n += 1; - } - ); - - /* - * Add 2 milliseconds to the drift to account for Redis expires - * precision, which is 1 millisecond, plus 1 millisecond min drift - * for small TTLs. - */ - var drift = Convert.ToInt32((ttl.TotalMilliseconds * ClockDriveFactor) + 2); - var validity_time = ttl - (DateTime.Now - startTime) - new TimeSpan(0, 0, 0, 0, drift); - - if (n >= Quorum && validity_time.TotalMilliseconds > 0) - { - innerLock = new Lock(resource, val, validity_time); - return true; - } - else - { - for_each_redis_registered( - redis => - { - UnlockInstance(redis, resource, val); - } - ); - return false; - } - } - catch (Exception) - { return false; } - }); - - lockObject = innerLock; - return successfull; - } - - protected void for_each_redis_registered(Action action) - { - foreach (var item in redisMasterDictionary) - { - action(item.Value); - } - } - - protected void for_each_redis_registered(Action action) - { - foreach (var item in redisMasterDictionary) - { - action(item.Key); - } - } - - protected bool retry(int retryCount, TimeSpan retryDelay, Func action) - { - int maxRetryDelay = (int)retryDelay.TotalMilliseconds; - Random rnd = new Random(); - int currentRetry = 0; - - while (currentRetry++ < retryCount) - { - if (action()) return true; - Thread.Sleep(rnd.Next(maxRetryDelay)); - } - return false; - } - - protected void Unlock(Lock lockObject) - { - for_each_redis_registered(redis => - { - UnlockInstance(redis, lockObject.Resource, lockObject.Value); - }); - } - - public async Task Start() - { - - } - - public async Task Stop() - { - - } - - } -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously -} diff --git a/src/providers/WorkflowCore.LockProviders.Redlock/WorkflowCore.LockProviders.Redlock.csproj b/src/providers/WorkflowCore.LockProviders.Redlock/WorkflowCore.LockProviders.Redlock.csproj deleted file mode 100644 index e51b242ea..000000000 --- a/src/providers/WorkflowCore.LockProviders.Redlock/WorkflowCore.LockProviders.Redlock.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - Workflow Core Redlock distributed lock manager - 1.1.0 - Daniel Gerlag - netstandard1.3 - WorkflowCore.LockProviders.Redlock - WorkflowCore.LockProviders.Redlock - workflow;.NET;Core;state machine;WorkflowCore;Redlock - https://github.com/danielgerlag/workflow-core - https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md - git - https://github.com/danielgerlag/workflow-core.git - 1.6.1 - $(PackageTargetFallback);dnxcore50 - false - false - false - 1.3.0 - - Distributed lock provider for Workflow-core using Redis - 1.3.0.0 - 1.3.0.0 - - - - - - - - - - - - - - - - diff --git a/src/providers/WorkflowCore.LockProviders.SqlServer/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.LockProviders.SqlServer/ServiceCollectionExtensions.cs index e93985658..1bdcab46c 100644 --- a/src/providers/WorkflowCore.LockProviders.SqlServer/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.LockProviders.SqlServer/ServiceCollectionExtensions.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using WorkflowCore.LockProviders.SqlServer; using WorkflowCore.Models; diff --git a/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs b/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs index 7726d3ebf..92795d871 100644 --- a/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs +++ b/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs @@ -5,7 +5,6 @@ using WorkflowCore.Interface; using System.Data; using System.Collections.Generic; -using System.Collections.Concurrent; using System.Threading; namespace WorkflowCore.LockProviders.SqlServer @@ -133,12 +132,9 @@ public async Task ReleaseLock(string Id) } } - public async Task Start() - { - } - - public async Task Stop() - { - } + public Task Start() => Task.CompletedTask; + + public Task Stop() => Task.CompletedTask; + } } diff --git a/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj b/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj index d6c4ab17f..5c798129a 100644 --- a/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj +++ b/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj @@ -1,15 +1,14 @@  - netstandard1.3 + netstandard2.0 Distributed lock provider for Workflow-core using SQL Server https://github.com/danielgerlag/workflow-core https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md - 1.4.0 - + diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs index 9018bf73d..b0b2b1d99 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Models; using WorkflowCore.Persistence.EntityFramework.Models; @@ -10,7 +9,7 @@ namespace WorkflowCore.Persistence.EntityFramework { internal static class ExtensionMethods { - private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; internal static PersistedWorkflow ToPersistable(this WorkflowInstance instance, PersistedWorkflow persistable = null) { @@ -26,19 +25,19 @@ internal static PersistedWorkflow ToPersistable(this WorkflowInstance instance, persistable.WorkflowDefinitionId = instance.WorkflowDefinitionId; persistable.Status = instance.Status; persistable.CreateTime = instance.CreateTime; - persistable.CompleteTime = instance.CompleteTime; + persistable.CompleteTime = instance.CompleteTime; foreach (var ep in instance.ExecutionPointers) { - var persistedEP = persistable.ExecutionPointers.FirstOrDefault(x => x.Id == ep.Id); + var persistedEP = persistable.ExecutionPointers.FindById(ep.Id); if (persistedEP == null) { persistedEP = new PersistedExecutionPointer(); + persistedEP.Id = ep.Id ?? Guid.NewGuid().ToString(); persistable.ExecutionPointers.Add(persistedEP); - } - - persistedEP.Id = ep.Id ?? Guid.NewGuid().ToString(); + } + persistedEP.StepId = ep.StepId; persistedEP.Active = ep.Active; persistedEP.SleepUntil = ep.SleepUntil; @@ -100,9 +99,14 @@ internal static PersistedSubscription ToPersistable(this EventSubscription insta result.EventKey = instance.EventKey; result.EventName = instance.EventName; result.StepId = instance.StepId; + result.ExecutionPointerId = instance.ExecutionPointerId; result.WorkflowId = instance.WorkflowId; result.SubscribeAsOf = DateTime.SpecifyKind(instance.SubscribeAsOf, DateTimeKind.Utc); - + result.SubscriptionData = JsonConvert.SerializeObject(instance.SubscriptionData, SerializerSettings); + result.ExternalToken = instance.ExternalToken; + result.ExternalTokenExpiry = instance.ExternalTokenExpiry; + result.ExternalWorkerId = instance.ExternalWorkerId; + return result; } @@ -119,6 +123,16 @@ internal static PersistedEvent ToPersistable(this Event instance) return result; } + internal static PersistedScheduledCommand ToPersistable(this ScheduledCommand instance) + { + var result = new PersistedScheduledCommand(); + result.CommandName = instance.CommandName; + result.Data = instance.Data; + result.ExecuteTime = instance.ExecuteTime; + + return result; + } + internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow instance) { WorkflowInstance result = new WorkflowInstance(); @@ -131,13 +145,14 @@ internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow insta result.WorkflowDefinitionId = instance.WorkflowDefinitionId; result.Status = instance.Status; result.CreateTime = DateTime.SpecifyKind(instance.CreateTime, DateTimeKind.Utc); - if (result.CompleteTime.HasValue) + if (instance.CompleteTime.HasValue) result.CompleteTime = DateTime.SpecifyKind(instance.CompleteTime.Value, DateTimeKind.Utc); + result.ExecutionPointers = new ExecutionPointerCollection(instance.ExecutionPointers.Count + 8); + foreach (var ep in instance.ExecutionPointers) { - var pointer = new ExecutionPointer(); - result.ExecutionPointers.Add(pointer); + var pointer = new ExecutionPointer(); pointer.Id = ep.Id; pointer.StepId = ep.StepId; @@ -171,12 +186,14 @@ internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow insta pointer.Status = ep.Status; if (!string.IsNullOrEmpty(ep.Scope)) - pointer.Scope = new Stack(ep.Scope.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)); + pointer.Scope = new List(ep.Scope.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)); foreach (var attr in ep.ExtensionAttributes) { pointer.ExtensionAttributes[attr.AttributeKey] = JsonConvert.DeserializeObject(attr.AttributeValue, SerializerSettings); } + + result.ExecutionPointers.Add(pointer); } return result; @@ -189,8 +206,13 @@ internal static EventSubscription ToEventSubscription(this PersistedSubscription result.EventKey = instance.EventKey; result.EventName = instance.EventName; result.StepId = instance.StepId; + result.ExecutionPointerId = instance.ExecutionPointerId; result.WorkflowId = instance.WorkflowId; result.SubscribeAsOf = DateTime.SpecifyKind(instance.SubscribeAsOf, DateTimeKind.Utc); + result.SubscriptionData = JsonConvert.DeserializeObject(instance.SubscriptionData, SerializerSettings); + result.ExternalToken = instance.ExternalToken; + result.ExternalTokenExpiry = instance.ExternalTokenExpiry; + result.ExternalWorkerId = instance.ExternalWorkerId; return result; } @@ -207,5 +229,15 @@ internal static Event ToEvent(this PersistedEvent instance) return result; } + + internal static ScheduledCommand ToScheduledCommand(this PersistedScheduledCommand instance) + { + var result = new ScheduledCommand(); + result.CommandName = instance.CommandName; + result.Data = instance.Data; + result.ExecuteTime = instance.ExecuteTime; + + return result; + } } } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Interfaces/IWorkflowDbContextFactory.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Interfaces/IWorkflowDbContextFactory.cs new file mode 100644 index 000000000..61dfb5214 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Interfaces/IWorkflowDbContextFactory.cs @@ -0,0 +1,10 @@ +using System; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.EntityFramework.Interfaces +{ + public interface IWorkflowDbContextFactory + { + WorkflowDbContext Build(); + } +} diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedEvent.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedEvent.cs index d60090c83..832bee16b 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedEvent.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedEvent.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Persistence.EntityFramework.Models { diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionError.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionError.cs index 1a181a821..7741aa767 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionError.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionError.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Threading.Tasks; -using WorkflowCore.Models; namespace WorkflowCore.Persistence.EntityFramework.Models { diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointer.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointer.cs index 984e27735..c7b5b48eb 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointer.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointer.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Models; namespace WorkflowCore.Persistence.EntityFramework.Models diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointerCollection.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointerCollection.cs new file mode 100644 index 000000000..cbff64146 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointerCollection.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WorkflowCore.Persistence.EntityFramework.Models +{ + public class PersistedExecutionPointerCollection : ICollection + { + private readonly Dictionary _dictionary; + + public PersistedExecutionPointerCollection() + { + _dictionary = new Dictionary(); + } + + public PersistedExecutionPointerCollection(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + public IEnumerator GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public PersistedExecutionPointer FindById(string id) + { + if (!_dictionary.ContainsKey(id)) + return null; + + return _dictionary[id]; + } + + public void Add(PersistedExecutionPointer item) + { + _dictionary.Add(item.Id, item); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(PersistedExecutionPointer item) + { + return _dictionary.ContainsValue(item); + } + + public void CopyTo(PersistedExecutionPointer[] array, int arrayIndex) + { + _dictionary.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(PersistedExecutionPointer item) + { + return _dictionary.Remove(item.Id); + } + + public int Count => _dictionary.Count; + public bool IsReadOnly => false; + } +} diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExtensionAttribute.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExtensionAttribute.cs index 3786e9e5a..5a382d8a1 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExtensionAttribute.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExtensionAttribute.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Threading.Tasks; -using WorkflowCore.Models; namespace WorkflowCore.Persistence.EntityFramework.Models { diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedScheduledCommand.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedScheduledCommand.cs new file mode 100644 index 000000000..7dad156a3 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedScheduledCommand.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace WorkflowCore.Persistence.EntityFramework.Models +{ + public class PersistedScheduledCommand + { + [Key] + public long PersistenceId { get; set; } + + [MaxLength(200)] + public string CommandName { get; set; } + + [MaxLength(500)] + public string Data { get; set; } + + public long ExecuteTime { get; set; } + } +} diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedSubscription.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedSubscription.cs index 0eb32e42d..fcc10f005 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedSubscription.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedSubscription.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Persistence.EntityFramework.Models { @@ -20,6 +17,9 @@ public class PersistedSubscription public int StepId { get; set; } + [MaxLength(200)] + public string ExecutionPointerId { get; set; } + [MaxLength(200)] public string EventName { get; set; } @@ -27,5 +27,15 @@ public class PersistedSubscription public string EventKey { get; set; } public DateTime SubscribeAsOf { get; set; } + + public string SubscriptionData { get; set; } + + [MaxLength(200)] + public string ExternalToken { get; set; } + + [MaxLength(200)] + public string ExternalWorkerId { get; set; } + + public DateTime? ExternalTokenExpiry { get; set; } } } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs index 626e2661f..5c427a988 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Models; namespace WorkflowCore.Persistence.EntityFramework.Models @@ -27,9 +24,8 @@ public class PersistedWorkflow [MaxLength(200)] public string Reference { get; set; } - public virtual List ExecutionPointers { get; set; } = new List(); + public virtual PersistedExecutionPointerCollection ExecutionPointers { get; set; } = new PersistedExecutionPointerCollection(); - //[Index] public long? NextExecution { get; set; } public string Data { get; set; } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs index 9371ecd66..d147caeeb 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs index aa095a281..22ba680f5 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs @@ -2,132 +2,73 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Persistence.EntityFramework.Models; using WorkflowCore.Models; -using Microsoft.EntityFrameworkCore.Metadata.Builders; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using System.Threading; namespace WorkflowCore.Persistence.EntityFramework.Services { - public abstract class EntityFrameworkPersistenceProvider : DbContext, IPersistenceProvider + public class EntityFrameworkPersistenceProvider : IPersistenceProvider { - protected readonly bool _canCreateDB; - protected readonly bool _canMigrateDB; - private readonly AutoResetEvent _mutex = new AutoResetEvent(true); + private readonly bool _canCreateDB; + private readonly bool _canMigrateDB; + private readonly IWorkflowDbContextFactory _contextFactory; - protected EntityFrameworkPersistenceProvider(bool canCreateDB, bool canMigrateDB) - { - _canCreateDB = canCreateDB; - _canMigrateDB = canMigrateDB; - } + public bool SupportsScheduledCommands => true; - protected abstract void ConfigureWorkflowStorage(EntityTypeBuilder builder); - protected abstract void ConfigureExecutionPointerStorage(EntityTypeBuilder builder); - protected abstract void ConfigureExecutionErrorStorage(EntityTypeBuilder builder); - protected abstract void ConfigureExetensionAttributeStorage(EntityTypeBuilder builder); - protected abstract void ConfigureSubscriptionStorage(EntityTypeBuilder builder); - protected abstract void ConfigureEventStorage(EntityTypeBuilder builder); - - protected override void OnModelCreating(ModelBuilder modelBuilder) + public EntityFrameworkPersistenceProvider(IWorkflowDbContextFactory contextFactory, bool canCreateDB, bool canMigrateDB) { - base.OnModelCreating(modelBuilder); - - var workflows = modelBuilder.Entity(); - workflows.HasIndex(x => x.InstanceId).IsUnique(); - workflows.HasIndex(x => x.NextExecution); - - var executionPointers = modelBuilder.Entity(); - var executionErrors = modelBuilder.Entity(); - var extensionAttributes = modelBuilder.Entity(); - - var subscriptions = modelBuilder.Entity(); - subscriptions.HasIndex(x => x.SubscriptionId).IsUnique(); - subscriptions.HasIndex(x => x.EventName); - subscriptions.HasIndex(x => x.EventKey); - - var events = modelBuilder.Entity(); - events.HasIndex(x => x.EventId).IsUnique(); - events.HasIndex(x => new { x.EventName, x.EventKey }); - events.HasIndex(x => x.EventTime); - events.HasIndex(x => x.IsProcessed); - - ConfigureWorkflowStorage(workflows); - ConfigureExecutionPointerStorage(executionPointers); - ConfigureExecutionErrorStorage(executionErrors); - ConfigureExetensionAttributeStorage(extensionAttributes); - ConfigureSubscriptionStorage(subscriptions); - ConfigureEventStorage(events); + _contextFactory = contextFactory; + _canCreateDB = canCreateDB; + _canMigrateDB = canMigrateDB; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - base.OnConfiguring(optionsBuilder); - optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - } - - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { subscription.Id = Guid.NewGuid().ToString(); var persistable = subscription.ToPersistable(); - var result = Set().Add(persistable); - await SaveChangesAsync(); - Entry(persistable).State = EntityState.Detached; + var result = db.Set().Add(persistable); + await db.SaveChangesAsync(cancellationToken); return subscription.Id; } - finally - { - _mutex.Set(); - } } - public async Task CreateNewWorkflow(WorkflowInstance workflow) + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { workflow.Id = Guid.NewGuid().ToString(); var persistable = workflow.ToPersistable(); - var result = Set().Add(persistable); - await SaveChangesAsync(); - Entry(persistable).State = EntityState.Detached; + var result = db.Set().Add(persistable); + await db.SaveChangesAsync(cancellationToken); return workflow.Id; } - finally - { - _mutex.Set(); - } } - public async Task> GetRunnableInstances(DateTime asAt) + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var now = asAt.ToUniversalTime().Ticks; - var raw = await Set() + var raw = await db.Set() .Where(x => x.NextExecution.HasValue && (x.NextExecution <= now) && (x.Status == WorkflowStatus.Runnable)) .Select(x => x.InstanceId) - .ToListAsync(); + .ToListAsync(cancellationToken); return raw.Select(s => s.ToString()).ToList(); } - finally - { - _mutex.Set(); - } } public async Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { - IQueryable query = Set() + IQueryable query = db.Set() .Include(wf => wf.ExecutionPointers) .ThenInclude(ep => ep.ExtensionAttributes) .Include(wf => wf.ExecutionPointers) @@ -153,206 +94,199 @@ public async Task> GetWorkflowInstances(WorkflowSt return result; } - finally - { - _mutex.Set(); - } } - - public async Task GetWorkflowInstance(string Id) + + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var uid = new Guid(Id); - var raw = await Set() + var raw = await db.Set() .Include(wf => wf.ExecutionPointers) .ThenInclude(ep => ep.ExtensionAttributes) .Include(wf => wf.ExecutionPointers) - .FirstAsync(x => x.InstanceId == uid); + .FirstAsync(x => x.InstanceId == uid, cancellationToken); if (raw == null) return null; return raw.ToWorkflowInstance(); } - finally + } + + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) + { + if (ids == null) { - _mutex.Set(); + return new List(); + } + + using (var db = ConstructDbContext()) + { + var uids = ids.Select(i => new Guid(i)); + var raw = db.Set() + .Include(wf => wf.ExecutionPointers) + .ThenInclude(ep => ep.ExtensionAttributes) + .Include(wf => wf.ExecutionPointers) + .Where(x => uids.Contains(x.InstanceId)); + + return (await raw.ToListAsync(cancellationToken)).Select(i => i.ToWorkflowInstance()); } } - public async Task PersistWorkflow(WorkflowInstance workflow) + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var uid = new Guid(workflow.Id); - var existingEntity = await Set() + var existingEntity = await db.Set() .Where(x => x.InstanceId == uid) .Include(wf => wf.ExecutionPointers) .ThenInclude(ep => ep.ExtensionAttributes) .Include(wf => wf.ExecutionPointers) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); var persistable = workflow.ToPersistable(existingEntity); - await SaveChangesAsync(); - Entry(persistable).State = EntityState.Detached; - foreach (var ep in persistable.ExecutionPointers) - { - Entry(ep).State = EntityState.Detached; + await db.SaveChangesAsync(cancellationToken); + } + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) + { + var uid = new Guid(workflow.Id); + var existingEntity = await db.Set() + .Where(x => x.InstanceId == uid) + .Include(wf => wf.ExecutionPointers) + .ThenInclude(ep => ep.ExtensionAttributes) + .Include(wf => wf.ExecutionPointers) + .AsTracking() + .FirstAsync(cancellationToken); - foreach (var attr in ep.ExtensionAttributes) - Entry(attr).State = EntityState.Detached; + var workflowPersistable = workflow.ToPersistable(existingEntity); + foreach (var subscription in subscriptions) + { + subscription.Id = Guid.NewGuid().ToString(); + var subscriptionPersistable = subscription.ToPersistable(); + db.Set().Add(subscriptionPersistable); } - } - finally - { - _mutex.Set(); + + await db.SaveChangesAsync(cancellationToken); } } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var uid = new Guid(eventSubscriptionId); - var existing = await Set().FirstAsync(x => x.SubscriptionId == uid); - Set().Remove(existing); - await SaveChangesAsync(); - } - finally - { - _mutex.Set(); + var existing = await db.Set().FirstAsync(x => x.SubscriptionId == uid, cancellationToken); + db.Set().Remove(existing); + await db.SaveChangesAsync(cancellationToken); } } - + public virtual void EnsureStoreExists() { - if (_canCreateDB && !_canMigrateDB) + using (var context = ConstructDbContext()) { - Database.EnsureCreated(); - return; - } + if (_canCreateDB && !_canMigrateDB) + { + context.Database.EnsureCreated(); + return; + } - if (_canMigrateDB) - { - Database.Migrate(); - return; + if (_canMigrateDB) + { + context.Database.Migrate(); + return; + } } } - public async Task> GetSubcriptions(string eventName, string eventKey, DateTime asOf) + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { asOf = asOf.ToUniversalTime(); - var raw = await Set() + var raw = await db.Set() .Where(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf) - .ToListAsync(); + .ToListAsync(cancellationToken); return raw.Select(item => item.ToEventSubscription()).ToList(); } - finally - { - _mutex.Set(); - } } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { newEvent.Id = Guid.NewGuid().ToString(); var persistable = newEvent.ToPersistable(); - var result = Set().Add(persistable); - await SaveChangesAsync(); - Entry(persistable).State = EntityState.Detached; + var result = db.Set().Add(persistable); + await db.SaveChangesAsync(cancellationToken); return newEvent.Id; } - finally - { - _mutex.Set(); - } } - public async Task GetEvent(string id) + public async Task GetEvent(string id, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { Guid uid = new Guid(id); - var raw = await Set() - .FirstAsync(x => x.EventId == uid); + var raw = await db.Set() + .FirstAsync(x => x.EventId == uid, cancellationToken); if (raw == null) return null; return raw.ToEvent(); } - finally - { - _mutex.Set(); - } } - public async Task> GetRunnableEvents(DateTime asAt) + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) { var now = asAt.ToUniversalTime(); - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { asAt = asAt.ToUniversalTime(); - var raw = await Set() + var raw = await db.Set() .Where(x => !x.IsProcessed) .Where(x => x.EventTime <= now) .Select(x => x.EventId) - .ToListAsync(); + .ToListAsync(cancellationToken); return raw.Select(s => s.ToString()).ToList(); } - finally - { - _mutex.Set(); - } } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var uid = new Guid(id); - var existingEntity = await Set() + var existingEntity = await db.Set() .Where(x => x.EventId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); existingEntity.IsProcessed = true; - await SaveChangesAsync(); - Entry(existingEntity).State = EntityState.Detached; - } - finally - { - _mutex.Set(); + await db.SaveChangesAsync(cancellationToken); } } - public async Task> GetEvents(string eventName, string eventKey, DateTime asOf) + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { - var raw = await Set() + var raw = await db.Set() .Where(x => x.EventName == eventName && x.EventKey == eventKey) .Where(x => x.EventTime >= asOf) .Select(x => x.EventId) - .ToListAsync(); + .ToListAsync(cancellationToken); var result = new List(); @@ -361,52 +295,144 @@ public async Task> GetEvents(string eventName, string eventK return result; } - finally - { - _mutex.Set(); - } } - public async Task MarkEventUnprocessed(string id) + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var uid = new Guid(id); - var existingEntity = await Set() + var existingEntity = await db.Set() .Where(x => x.EventId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); existingEntity.IsProcessed = false; - await SaveChangesAsync(); - Entry(existingEntity).State = EntityState.Detached; - } - finally - { - _mutex.Set(); + await db.SaveChangesAsync(cancellationToken); } } - public async Task PersistErrors(IEnumerable errors) + public async Task PersistErrors(IEnumerable errors, CancellationToken cancellationToken = default) { - _mutex.WaitOne(); - try + using (var db = ConstructDbContext()) { var executionErrors = errors as ExecutionError[] ?? errors.ToArray(); if (executionErrors.Any()) { foreach (var error in executionErrors) { - Set().Add(error.ToPersistable()); + db.Set().Add(error.ToPersistable()); } - await SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); } } - finally + } + + private WorkflowDbContext ConstructDbContext() + { + return _contextFactory.Build(); + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) + { + var uid = new Guid(eventSubscriptionId); + var raw = await db.Set().FirstOrDefaultAsync(x => x.SubscriptionId == uid, cancellationToken); + + return raw?.ToEventSubscription(); + } + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) { - _mutex.Set(); + var raw = await db.Set().FirstOrDefaultAsync(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf && x.ExternalToken == null, cancellationToken); + + return raw?.ToEventSubscription(); + } + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) + { + var uid = new Guid(eventSubscriptionId); + var existingEntity = await db.Set() + .Where(x => x.SubscriptionId == uid) + .AsTracking() + .FirstAsync(cancellationToken); + + existingEntity.ExternalToken = token; + existingEntity.ExternalWorkerId = workerId; + existingEntity.ExternalTokenExpiry = expiry; + await db.SaveChangesAsync(cancellationToken); + + return true; + } + } + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) + { + var uid = new Guid(eventSubscriptionId); + var existingEntity = await db.Set() + .Where(x => x.SubscriptionId == uid) + .AsTracking() + .FirstAsync(cancellationToken); + + if (existingEntity.ExternalToken != token) + throw new InvalidOperationException(); + + existingEntity.ExternalToken = null; + existingEntity.ExternalWorkerId = null; + existingEntity.ExternalTokenExpiry = null; + await db.SaveChangesAsync(cancellationToken); + } + } + + public async Task ScheduleCommand(ScheduledCommand command) + { + try + { + using (var db = ConstructDbContext()) + { + var persistable = command.ToPersistable(); + var result = db.Set().Add(persistable); + await db.SaveChangesAsync(); + } + } + catch (DbUpdateException) + { + //log + } + } + + public async Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + using (var db = ConstructDbContext()) + { + var cursor = db.Set() + .Where(x => x.ExecuteTime < asOf.UtcDateTime.Ticks) + .AsAsyncEnumerable(); + + await foreach (var command in cursor) + { + try + { + await action(command.ToScheduledCommand()); + using var db2 = ConstructDbContext(); + db2.Set().Remove(command); + await db2.SaveChangesAsync(); + } + catch (Exception) + { + //TODO: add logger + } + } } } } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowDbContext.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowDbContext.cs new file mode 100644 index 000000000..53e0967e7 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowDbContext.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using WorkflowCore.Persistence.EntityFramework.Models; + +namespace WorkflowCore.Persistence.EntityFramework.Services +{ + public abstract class WorkflowDbContext : DbContext + { + protected abstract void ConfigureWorkflowStorage(EntityTypeBuilder builder); + protected abstract void ConfigureExecutionPointerStorage(EntityTypeBuilder builder); + protected abstract void ConfigureExecutionErrorStorage(EntityTypeBuilder builder); + protected abstract void ConfigureExetensionAttributeStorage(EntityTypeBuilder builder); + protected abstract void ConfigureSubscriptionStorage(EntityTypeBuilder builder); + protected abstract void ConfigureEventStorage(EntityTypeBuilder builder); + protected abstract void ConfigureScheduledCommandStorage(EntityTypeBuilder builder); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + var workflows = modelBuilder.Entity(); + workflows.HasIndex(x => x.InstanceId).IsUnique(); + workflows.HasIndex(x => x.NextExecution); + + var executionPointers = modelBuilder.Entity(); + var executionErrors = modelBuilder.Entity(); + var extensionAttributes = modelBuilder.Entity(); + + var subscriptions = modelBuilder.Entity(); + subscriptions.HasIndex(x => x.SubscriptionId).IsUnique(); + subscriptions.HasIndex(x => x.EventName); + subscriptions.HasIndex(x => x.EventKey); + + var events = modelBuilder.Entity(); + events.HasIndex(x => x.EventId).IsUnique(); + events.HasIndex(x => new { x.EventName, x.EventKey }); + events.HasIndex(x => x.EventTime); + events.HasIndex(x => x.IsProcessed); + + var commands = modelBuilder.Entity(); + commands.HasIndex(x => x.ExecuteTime); + commands.HasIndex(x => new { x.CommandName, x.Data}).IsUnique(); + + ConfigureWorkflowStorage(workflows); + ConfigureExecutionPointerStorage(executionPointers); + ConfigureExecutionErrorStorage(executionErrors); + ConfigureExetensionAttributeStorage(extensionAttributes); + ConfigureSubscriptionStorage(subscriptions); + ConfigureEventStorage(events); + ConfigureScheduledCommandStorage(commands); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs new file mode 100644 index 000000000..904895809 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Models; + +namespace WorkflowCore.Persistence.EntityFramework.Services +{ + public class WorkflowPurger : IWorkflowPurger + { + private readonly IWorkflowDbContextFactory _contextFactory; + + public WorkflowPurger(IWorkflowDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + var olderThanUtc = olderThan.ToUniversalTime(); + using (var db = ConstructDbContext()) + { + var workflows = await db.Set().Where(x => x.Status == status && x.CompleteTime < olderThanUtc).ToListAsync(cancellationToken); + foreach (var wf in workflows) + { + foreach (var pointer in wf.ExecutionPointers) + { + foreach (var extAttr in pointer.ExtensionAttributes) + { + db.Remove(extAttr); + } + + db.Remove(pointer); + } + db.Remove(wf); + } + + await db.SaveChangesAsync(cancellationToken); + } + } + + + private WorkflowDbContext ConstructDbContext() + { + return _contextFactory.Build(); + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj b/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj index 851918adb..b5fb079c9 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj @@ -2,9 +2,8 @@ Workflow Core EntityFramework Core Persistence Provider - 1.1.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0 WorkflowCore.Persistence.EntityFramework WorkflowCore.Persistence.EntityFramework workflow;.NET;Core;state machine;WorkflowCore;EntityFramework;EntityFrameworkCore @@ -15,20 +14,24 @@ false false false - 1.6.0 Base package for Workflow-core peristence providers using entity framework - 1.6.0.0 - 1.6.0.0 + + + + + + + + + - - - + diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/ConfigOptions.cs b/src/providers/WorkflowCore.Persistence.MongoDB/ConfigOptions.cs index c5b9cddbd..0efd90b42 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/ConfigOptions.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/ConfigOptions.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Persistence.MongoDB { diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.MongoDB/Properties/AssemblyInfo.cs index df8354da4..4c1a32df7 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/README.md b/src/providers/WorkflowCore.Persistence.MongoDB/README.md index 5dacb7c6c..911d8a9e4 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/README.md +++ b/src/providers/WorkflowCore.Persistence.MongoDB/README.md @@ -17,3 +17,35 @@ Use the .UseMongoDB extension method when building your service provider. ```C# services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); ``` + +### State object serialization + +By default (to maintain backwards compatibility), the state object is serialized using a two step serialization process using object -> JSON -> BSON serialization. +This approach has some limitations, for example you cannot control which types will be used in MongoDB for particular fields and you cannot use basic types that are not present in JSON (decimal, timestamp, etc). + +To eliminate these limitations, you can use a direct object -> BSON serialization and utilize all serialization possibilities that MongoDb driver provides. You can read more in the [MongoDb CSharp documentation](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/). +To enable direct serilization you need to register a class map for you state class somewhere in your startup process before you run `WorkflowHost`. + +```C# +private void RunWorkflow() +{ + var host = this._serviceProvider.GetService(); + if (host == null) + { + return; + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(MyWorkflowState))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + }); + } + + host.RegisterWorkflow(); + + host.Start(); +} + +``` diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs index c5a1f67da..9534fe0b2 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ using MongoDB.Driver; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Persistence.MongoDB.Services; @@ -10,14 +8,49 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static WorkflowOptions UseMongoDB(this WorkflowOptions options, string mongoUrl, string databaseName) + public static WorkflowOptions UseMongoDB( + this WorkflowOptions options, + string mongoUrl, + string databaseName, + Action configureClient = default) { options.UsePersistence(sp => { - var client = new MongoClient(mongoUrl); + var mongoClientSettings = MongoClientSettings.FromConnectionString(mongoUrl); + configureClient?.Invoke(mongoClientSettings); + var client = new MongoClient(mongoClientSettings); var db = client.GetDatabase(databaseName); return new MongoPersistenceProvider(db); }); + options.Services.AddTransient(sp => + { + var mongoClientSettings = MongoClientSettings.FromConnectionString(mongoUrl); + configureClient?.Invoke(mongoClientSettings); + var client = new MongoClient(mongoClientSettings); + var db = client.GetDatabase(databaseName); + return new WorkflowPurger(db); + }); + return options; + } + + public static WorkflowOptions UseMongoDB( + this WorkflowOptions options, + Func createDatabase) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (createDatabase == null) throw new ArgumentNullException(nameof(createDatabase)); + + options.UsePersistence(sp => + { + var db = createDatabase(sp); + return new MongoPersistenceProvider(db); + }); + options.Services.AddTransient(sp => + { + var db = createDatabase(sp); + return new WorkflowPurger(db); + }); + return options; } } diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/Services/AssemblyQualifiedDiscriminatorConvention.cs b/src/providers/WorkflowCore.Persistence.MongoDB/Services/AssemblyQualifiedDiscriminatorConvention.cs index 64908727e..b9845f6f8 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Services/AssemblyQualifiedDiscriminatorConvention.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/AssemblyQualifiedDiscriminatorConvention.cs @@ -1,8 +1,6 @@ using MongoDB.Bson.Serialization.Conventions; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.IO; diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/Services/DataObjectSerializer.cs b/src/providers/WorkflowCore.Persistence.MongoDB/Services/DataObjectSerializer.cs index 46d0d9623..dfa5f75fb 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Services/DataObjectSerializer.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/DataObjectSerializer.cs @@ -2,23 +2,20 @@ using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Dynamic.Core; -using System.Linq.Expressions; -using System.Threading.Tasks; -using WorkflowCore.Models; using Newtonsoft.Json; namespace WorkflowCore.Persistence.MongoDB.Services { public class DataObjectSerializer : SerializerBase { - private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { - TypeNameHandling = TypeNameHandling.All + TypeNameHandling = TypeNameHandling.Objects, }; - + public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { if (context.Reader.CurrentBsonType == BsonType.String) @@ -27,20 +24,135 @@ public override object Deserialize(BsonDeserializationContext context, BsonDeser return JsonConvert.DeserializeObject(raw, SerializerSettings); } - return BsonSerializer.Deserialize(context.Reader, typeof(object)); + var obj = BsonSerializer.Deserialize(context.Reader, typeof(object)); + return obj; } public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) { - string str = JsonConvert.SerializeObject(value, SerializerSettings); - var doc = BsonDocument.Parse(str); - var typeElem = doc.GetElement("$type"); - doc.RemoveElement(typeElem); - - if (doc.Elements.All(x => x.Name != "_t")) - doc.InsertAt(0, new BsonElement("_t", typeElem.Value)); + BsonDocument doc; + if (BsonClassMap.IsClassMapRegistered(value.GetType())) + { + doc = value.ToBsonDocument(); + doc.Remove("_t"); + doc.InsertAt(0, new BsonElement("_t", value.GetType().AssemblyQualifiedName)); + AddTypeInformation(doc.Elements, value, string.Empty); + } + else + { + var str = JsonConvert.SerializeObject(value, SerializerSettings); + doc = BsonDocument.Parse(str); + ConvertMetaFormat(doc); + } BsonSerializer.Serialize(context.Writer, doc); } + + private void AddTypeInformation(IEnumerable elements, object value, string xPath) + { + foreach (var element in elements) + { + var elementXPath = string.IsNullOrEmpty(xPath) ? element.Name : xPath + "." + element.Name; + if (element.Value.IsBsonDocument) + { + var doc = element.Value.AsBsonDocument; + doc.Remove("_t"); + doc.InsertAt(0, new BsonElement("_t", GetTypeNameFromXPath(value, elementXPath))); + AddTypeInformation(doc.Elements, value, elementXPath); + } + if (element.Value.IsBsonArray) + { + AddTypeInformation(element.Value.AsBsonArray, value, elementXPath); + } + } + } + + private string GetTypeNameFromXPath(object root, string xPath) + { + var parts = xPath.Split('.').ToList(); + object value = root; + while (parts.Count > 0) + { + var subPath = parts[0]; + if (subPath[0] == '[') + { + var index = Int32.Parse(subPath.Trim('[', ']')); + if ((value is IList) || value.GetType().IsArray) + { + IList list = (IList) value; + value = list[index]; + } + else + { + throw new NotSupportedException(); + } + } + else + { + var propInfo = value.GetType().GetProperty(subPath); + value = propInfo.GetValue(value); + } + + parts.RemoveAt(0); + } + + return value.GetType().AssemblyQualifiedName; + } + + private void AddTypeInformation(IEnumerable elements, object value, string xPath) + { + //foreach (var element in elements) + for (int i = 0; i < elements.Count(); i++) + { + var element = elements.ElementAt(i); + if (element.IsBsonDocument) + { + var doc = element.AsBsonDocument; + var elementXPath = xPath + $".[{i}]"; + doc.Remove("_t"); + doc.InsertAt(0, new BsonElement("_t", GetTypeNameFromXPath(value, elementXPath))); + AddTypeInformation(doc.Elements, value, elementXPath); + } + + if (element.IsBsonArray) + { + AddTypeInformation(element.AsBsonArray, value, xPath); + } + } + } + + private static void ConvertMetaFormat(BsonDocument root) + { + var stack = new Stack(); + stack.Push(root); + + while (stack.Count > 0) + { + var doc = stack.Pop(); + + if (doc.TryGetElement("$type", out var typeElem)) + { + doc.RemoveElement(typeElem); + + if (doc.Elements.All(x => x.Name != "_t")) + doc.InsertAt(0, new BsonElement("_t", typeElem.Value)); + } + + foreach (var subDoc in doc.Elements) + { + if (subDoc.Value.IsBsonDocument) + stack.Push(subDoc.Value.ToBsonDocument()); + + if (subDoc.Value.IsBsonArray) + { + foreach (var element in subDoc.Value.AsBsonArray) + { + if (element.IsBsonDocument) + stack.Push(element.ToBsonDocument()); + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs b/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs index fea94b5bb..fdab92cf6 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs @@ -1,20 +1,23 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.IdGenerators; -using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq; using WorkflowCore.Interface; using WorkflowCore.Models; +using System.Threading; namespace WorkflowCore.Persistence.MongoDB.Services { public class MongoPersistenceProvider : IPersistenceProvider { + internal const string WorkflowCollectionName = "wfc.workflows"; private readonly IMongoDatabase _database; public MongoPersistenceProvider(IMongoDatabase database) @@ -25,9 +28,16 @@ public MongoPersistenceProvider(IMongoDatabase database) static MongoPersistenceProvider() { + ConventionRegistry.Register( + "workflow.conventions", + new ConventionPack + { + new EnumRepresentationConvention(BsonType.String) + }, t => t.FullName?.StartsWith("WorkflowCore") ?? false); + BsonClassMap.RegisterClassMap(x => - { - x.MapIdProperty(y => y.Id) + { + x.MapIdProperty(y => y.Id) .SetIdGenerator(new StringObjectIdGenerator()); x.MapProperty(y => y.Data) .SetSerializer(new DataObjectSerializer()); @@ -36,7 +46,8 @@ static MongoPersistenceProvider() x.MapProperty(y => y.WorkflowDefinitionId); x.MapProperty(y => y.Version); x.MapProperty(y => y.NextExecution); - x.MapProperty(y => y.Status); + x.MapProperty(y => y.Status) + .SetSerializer(new EnumSerializer(BsonType.String)); x.MapProperty(y => y.CreateTime); x.MapProperty(y => y.CompleteTime); x.MapProperty(y => y.ExecutionPointers); @@ -48,9 +59,14 @@ static MongoPersistenceProvider() .SetIdGenerator(new StringObjectIdGenerator()); x.MapProperty(y => y.EventName); x.MapProperty(y => y.EventKey); - x.MapProperty(y => y.StepId); + x.MapProperty(y => y.StepId); + x.MapProperty(y => y.ExecutionPointerId); x.MapProperty(y => y.WorkflowId); - x.MapProperty(y => y.SubscribeAsOf); + x.MapProperty(y => y.SubscribeAsOf); + x.MapProperty(y => y.SubscriptionData); + x.MapProperty(y => y.ExternalToken); + x.MapProperty(y => y.ExternalTokenExpiry); + x.MapProperty(y => y.ExternalWorkerId); }); BsonClassMap.RegisterClassMap(x => @@ -64,15 +80,11 @@ static MongoPersistenceProvider() x.MapProperty(y => y.IsProcessed); }); - //BsonClassMap.RegisterClassMap(x => - //{ - // x.MapIdProperty(y => y.Id) - // .SetIdGenerator(new StringObjectIdGenerator()); - // x.MapProperty(y => y.ErrorTime); - // x.MapProperty(y => y.ExecutionPointerId); - // x.MapProperty(y => y.Message); - // x.MapProperty(y => y.WorkflowId); - //}); + BsonClassMap.RegisterClassMap(x => x.AutoMap()); + BsonClassMap.RegisterClassMap(x => x.AutoMap()); + BsonClassMap.RegisterClassMap(x => x.AutoMap()); + BsonClassMap.RegisterClassMap(x => x.AutoMap()) + .SetIgnoreExtraElements(true); } static bool indexesCreated = false; @@ -80,13 +92,46 @@ static void CreateIndexes(MongoPersistenceProvider instance) { if (!indexesCreated) { - instance.WorkflowInstances.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.NextExecution), new CreateIndexOptions() { Background = true, Name = "idx_nextExec" }); - instance.Events.Indexes.CreateOne(Builders.IndexKeys.Ascending(x => x.IsProcessed), new CreateIndexOptions() { Background = true, Name = "idx_processed" }); + instance.WorkflowInstances.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.NextExecution) + .Ascending(x => x.Status) + .Ascending(x => x.Id), + new CreateIndexOptions {Background = true, Name = "idx_nextExec_v2"})); + + instance.Events.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.IsProcessed), + new CreateIndexOptions {Background = true, Name = "idx_processed"})); + + instance.Events.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.EventName) + .Ascending(x => x.EventKey) + .Ascending(x => x.EventTime), + new CreateIndexOptions { Background = true, Name = "idx_namekey" })); + + instance.EventSubscriptions.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.EventName) + .Ascending(x => x.EventKey), + new CreateIndexOptions { Background = true, Name = "idx_namekey" })); + + instance.ScheduledCommands.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys + .Descending(x => x.ExecuteTime), + new CreateIndexOptions { Background = true, Name = "idx_exectime" })); + + instance.ScheduledCommands.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.CommandName) + .Ascending(x => x.Data), + new CreateIndexOptions { Background = true, Unique = true, Name = "idx_key" })); + indexesCreated = true; } } - private IMongoCollection WorkflowInstances => _database.GetCollection("wfc.workflows"); + private IMongoCollection WorkflowInstances => _database.GetCollection(WorkflowCollectionName); private IMongoCollection EventSubscriptions => _database.GetCollection("wfc.subscriptions"); @@ -94,36 +139,66 @@ static void CreateIndexes(MongoPersistenceProvider instance) private IMongoCollection ExecutionErrors => _database.GetCollection("wfc.execution_errors"); - public async Task CreateNewWorkflow(WorkflowInstance workflow) + private IMongoCollection ScheduledCommands => _database.GetCollection("wfc.scheduled_commands"); + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { - await WorkflowInstances.InsertOneAsync(workflow); + await WorkflowInstances.InsertOneAsync(workflow, cancellationToken: cancellationToken); return workflow.Id; } - public async Task PersistWorkflow(WorkflowInstance workflow) + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { - await WorkflowInstances.ReplaceOneAsync(x => x.Id == workflow.Id, workflow); + await WorkflowInstances.ReplaceOneAsync(x => x.Id == workflow.Id, workflow, cancellationToken: cancellationToken); + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + if (subscriptions == null || subscriptions.Count < 1) + { + await PersistWorkflow(workflow, cancellationToken); + return; + } + + using (var session = await _database.Client.StartSessionAsync(cancellationToken: cancellationToken)) + { + session.StartTransaction(); + await PersistWorkflow(workflow, cancellationToken); + await EventSubscriptions.InsertManyAsync(subscriptions, cancellationToken: cancellationToken); + await session.CommitTransactionAsync(cancellationToken); + } } - public async Task> GetRunnableInstances(DateTime asAt) + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) { var now = asAt.ToUniversalTime().Ticks; var query = WorkflowInstances .Find(x => x.NextExecution.HasValue && (x.NextExecution <= now) && (x.Status == WorkflowStatus.Runnable)) .Project(x => x.Id); - return await query.ToListAsync(); + return await query.ToListAsync(cancellationToken); } - public async Task GetWorkflowInstance(string Id) + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) { - var result = await WorkflowInstances.FindAsync(x => x.Id == Id); - return await result.FirstAsync(); + var result = await WorkflowInstances.FindAsync(x => x.Id == Id, cancellationToken: cancellationToken); + return await result.FirstAsync(cancellationToken); } - + + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) + { + if (ids == null) + { + return new List(); + } + + var result = await WorkflowInstances.FindAsync(x => ids.Contains(x.Id), cancellationToken: cancellationToken); + return await result.ToListAsync(cancellationToken); + } + public async Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) { - IQueryable result = WorkflowInstances.AsQueryable(); + IMongoQueryable result = WorkflowInstances.AsQueryable(); if (status.HasValue) result = result.Where(x => x.Status == status.Value); @@ -137,84 +212,161 @@ public async Task> GetWorkflowInstances(WorkflowSt if (createdTo.HasValue) result = result.Where(x => x.CreateTime <= createdTo.Value); - return result.Skip(skip).Take(take).ToList(); + return await result.Skip(skip).Take(take).ToListAsync(); } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) { - await EventSubscriptions.InsertOneAsync(subscription); + await EventSubscriptions.InsertOneAsync(subscription, cancellationToken: cancellationToken); return subscription.Id; } - - public async Task TerminateSubscription(string eventSubscriptionId) + + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + await EventSubscriptions.DeleteOneAsync(x => x.Id == eventSubscriptionId, cancellationToken); + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + var result = await EventSubscriptions.FindAsync(x => x.Id == eventSubscriptionId, cancellationToken: cancellationToken); + return await result.FirstOrDefaultAsync(cancellationToken); + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + var query = EventSubscriptions + .Find(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf && x.ExternalToken == null); + + return await query.FirstOrDefaultAsync(cancellationToken); + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) { - await EventSubscriptions.DeleteOneAsync(x => x.Id == eventSubscriptionId); + var update = Builders.Update + .Set(x => x.ExternalToken, token) + .Set(x => x.ExternalTokenExpiry, expiry) + .Set(x => x.ExternalWorkerId, workerId); + + var result = await EventSubscriptions.UpdateOneAsync(x => x.Id == eventSubscriptionId && x.ExternalToken == null, update, cancellationToken: cancellationToken); + return (result.ModifiedCount > 0); + } + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) + { + var update = Builders.Update + .Set(x => x.ExternalToken, null) + .Set(x => x.ExternalTokenExpiry, null) + .Set(x => x.ExternalWorkerId, null); + + await EventSubscriptions.UpdateOneAsync(x => x.Id == eventSubscriptionId && x.ExternalToken == token, update, cancellationToken: cancellationToken); } public void EnsureStoreExists() { - - } - - public async Task> GetSubcriptions(string eventName, string eventKey, DateTime asOf) + + } + + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) { var query = EventSubscriptions .Find(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf); - return await query.ToListAsync(); + return await query.ToListAsync(cancellationToken); } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) { - await Events.InsertOneAsync(newEvent); + await Events.InsertOneAsync(newEvent, cancellationToken: cancellationToken); return newEvent.Id; } - public async Task GetEvent(string id) + public async Task GetEvent(string id, CancellationToken cancellationToken = default) { - var result = await Events.FindAsync(x => x.Id == id); - return await result.FirstAsync(); + var result = await Events.FindAsync(x => x.Id == id, cancellationToken: cancellationToken); + return await result.FirstAsync(cancellationToken); } - public async Task> GetRunnableEvents(DateTime asAt) + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) { var now = asAt.ToUniversalTime(); var query = Events .Find(x => !x.IsProcessed && x.EventTime <= now) .Project(x => x.Id); - return await query.ToListAsync(); + return await query.ToListAsync(cancellationToken); } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) { var update = Builders.Update .Set(x => x.IsProcessed, true); - await Events.UpdateOneAsync(x => x.Id == id, update); + await Events.UpdateOneAsync(x => x.Id == id, update, cancellationToken: cancellationToken); } - public async Task> GetEvents(string eventName, string eventKey, DateTime asOf) + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) { var query = Events .Find(x => x.EventName == eventName && x.EventKey == eventKey && x.EventTime >= asOf) .Project(x => x.Id); - - return await query.ToListAsync(); + + return await query.ToListAsync(cancellationToken); } - public async Task MarkEventUnprocessed(string id) + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) { var update = Builders.Update .Set(x => x.IsProcessed, false); - await Events.UpdateOneAsync(x => x.Id == id, update); + await Events.UpdateOneAsync(x => x.Id == id, update, cancellationToken: cancellationToken); } - public async Task PersistErrors(IEnumerable errors) + public async Task PersistErrors(IEnumerable errors, CancellationToken cancellationToken = default) { if (errors.Any()) - await ExecutionErrors.InsertManyAsync(errors); + await ExecutionErrors.InsertManyAsync(errors, cancellationToken: cancellationToken); + } + + public bool SupportsScheduledCommands => true; + + public async Task ScheduleCommand(ScheduledCommand command) + { + try + { + await ScheduledCommands.InsertOneAsync(command); + } + catch (MongoWriteException ex) + { + if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) + return; + throw; + } + catch (MongoBulkWriteException ex) + { + if (ex.WriteErrors.All(x => x.Category == ServerErrorCategory.DuplicateKey)) + return; + throw; + } + } + + public async Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + var cursor = await ScheduledCommands.FindAsync(x => x.ExecuteTime < asOf.UtcDateTime.Ticks); + while (await cursor.MoveNextAsync(cancellationToken)) + { + foreach (var command in cursor.Current) + { + try + { + await action(command); + await ScheduledCommands.DeleteOneAsync(x => x.CommandName == command.CommandName && x.Data == command.Data); + } + catch (Exception) + { + //TODO: add logger + } + } + } } } } diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/Services/WorkflowPurger.cs b/src/providers/WorkflowCore.Persistence.MongoDB/Services/WorkflowPurger.cs new file mode 100644 index 000000000..56343d3f6 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/WorkflowPurger.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using WorkflowCore.Models; +using WorkflowCore.Interface; +using System.Threading; + +namespace WorkflowCore.Persistence.MongoDB.Services +{ + public class WorkflowPurger : IWorkflowPurger + { + private readonly IMongoDatabase _database; + + private IMongoCollection WorkflowInstances => _database.GetCollection(MongoPersistenceProvider.WorkflowCollectionName); + + public WorkflowPurger(IMongoDatabase database) + { + _database = database; + } + + public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + var olderThanUtc = olderThan.ToUniversalTime(); + await WorkflowInstances.DeleteManyAsync(x => x.Status == status + && x.CompleteTime < olderThanUtc, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj b/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj index 6d006caf0..e55685a2e 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj +++ b/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj @@ -2,9 +2,8 @@ Workflow Core MongoDB Persistence Provider - 1.1.0 Daniel Gerlag - netstandard1.3 + netstandard2.0 WorkflowCore.Persistence.MongoDB WorkflowCore.Persistence.MongoDB workflow;.NET;Core;state machine;WorkflowCore;MongoDB;Mongo @@ -12,15 +11,10 @@ https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git https://github.com/danielgerlag/workflow-core.git - 1.6.1 - $(PackageTargetFallback);dnxcore50 false false false - 1.6.0 Provides support to persist workflows running on Workflow Core to a MongoDB database. - 1.6.0.0 - 1.6.0.0 @@ -28,8 +22,8 @@ - - + + diff --git a/src/providers/WorkflowCore.Persistence.MySQL/MigrationContextFactory.cs b/src/providers/WorkflowCore.Persistence.MySQL/MigrationContextFactory.cs new file mode 100644 index 000000000..4a0e8216d --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/MigrationContextFactory.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.EntityFrameworkCore.Design; + +namespace WorkflowCore.Persistence.MySQL +{ + public class MigrationContextFactory : IDesignTimeDbContextFactory + { + public MysqlContext CreateDbContext(string[] args) + { + return new MysqlContext(@"Server=127.0.0.1;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.Designer.cs new file mode 100644 index 000000000..3df048a55 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.Designer.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20170126230815_InitialDatabase")] + partial class InitialDatabase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Message"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("ConcurrentFork"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey"); + + b.Property("EventName"); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("PathTerminator"); + + b.Property("PersistenceData"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("StepId"); + + b.Property("StepName"); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("PersistedExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedPublication", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("PublicationId"); + + b.Property("StepId"); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("PublicationId") + .IsUnique(); + + b.ToTable("PersistedPublication"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("PersistedSubscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("PersistedWorkflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("Errors") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.cs new file mode 100644 index 000000000..066f861f2 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170126230815_InitialDatabase.cs @@ -0,0 +1,213 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class InitialDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UnpublishedEvent", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + EventData = table.Column(nullable: true), + EventKey = table.Column(maxLength: 200, nullable: true), + EventName = table.Column(maxLength: 200, nullable: true), + PublicationId = table.Column(nullable: false), + StepId = table.Column(nullable: false), + WorkflowId = table.Column(maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UnpublishedEvent", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "Subscription", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + EventKey = table.Column(maxLength: 200, nullable: true), + EventName = table.Column(maxLength: 200, nullable: true), + StepId = table.Column(nullable: false), + SubscriptionId = table.Column(maxLength: 200, nullable: false), + WorkflowId = table.Column(maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Subscription", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "Workflow", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + CompleteTime = table.Column(nullable: true), + CreateTime = table.Column(nullable: false), + Data = table.Column(nullable: true), + Description = table.Column(maxLength: 500, nullable: true), + InstanceId = table.Column(maxLength: 200, nullable: false), + NextExecution = table.Column(nullable: true), + Status = table.Column(nullable: false), + Version = table.Column(nullable: false), + WorkflowDefinitionId = table.Column(maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Workflow", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "ExecutionPointer", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Active = table.Column(nullable: false), + ConcurrentFork = table.Column(nullable: false), + EndTime = table.Column(nullable: true), + EventData = table.Column(nullable: true), + EventKey = table.Column(nullable: true), + EventName = table.Column(nullable: true), + EventPublished = table.Column(nullable: false), + Id = table.Column(maxLength: 50, nullable: true), + PathTerminator = table.Column(nullable: false), + PersistenceData = table.Column(nullable: true), + SleepUntil = table.Column(nullable: true), + StartTime = table.Column(nullable: true), + StepId = table.Column(nullable: false), + StepName = table.Column(nullable: true), + WorkflowId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExecutionPointer", x => x.PersistenceId); + table.ForeignKey( + name: "FK_ExecutionPointer_Workflow_WorkflowId", + column: x => x.WorkflowId, + principalTable: "Workflow", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExecutionError", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ErrorTime = table.Column(nullable: false), + ExecutionPointerId = table.Column(nullable: false), + Id = table.Column(maxLength: 50, nullable: true), + Message = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExecutionError", x => x.PersistenceId); + table.ForeignKey( + name: "FK_ExecutionError_ExecutionPointer_ExecutionPointerId", + column: x => x.ExecutionPointerId, + principalTable: "ExecutionPointer", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExtensionAttribute", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AttributeKey = table.Column(maxLength: 100, nullable: true), + AttributeValue = table.Column(nullable: true), + ExecutionPointerId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExtensionAttribute", x => x.PersistenceId); + table.ForeignKey( + name: "FK_ExtensionAttribute_ExecutionPointer_ExecutionPointerId", + column: x => x.ExecutionPointerId, + principalTable: "ExecutionPointer", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExecutionError_ExecutionPointerId", + table: "ExecutionError", + column: "ExecutionPointerId"); + + migrationBuilder.CreateIndex( + name: "IX_ExecutionPointer_WorkflowId", + table: "ExecutionPointer", + column: "WorkflowId"); + + migrationBuilder.CreateIndex( + name: "IX_ExtensionAttribute_ExecutionPointerId", + table: "ExtensionAttribute", + column: "ExecutionPointerId"); + + migrationBuilder.CreateIndex( + name: "IX_UnpublishedEvent_PublicationId", + table: "UnpublishedEvent", + column: "PublicationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Subscription_EventKey", + table: "Subscription", + column: "EventKey"); + + migrationBuilder.CreateIndex( + name: "IX_Subscription_EventName", + table: "Subscription", + column: "EventName"); + + migrationBuilder.CreateIndex( + name: "IX_Subscription_SubscriptionId", + table: "Subscription", + column: "SubscriptionId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Workflow_InstanceId", + table: "Workflow", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Workflow_NextExecution", + table: "Workflow", + column: "NextExecution"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExecutionError"); + + migrationBuilder.DropTable( + name: "ExtensionAttribute"); + + migrationBuilder.DropTable( + name: "UnpublishedEvent"); + + migrationBuilder.DropTable( + name: "Subscription"); + + migrationBuilder.DropTable( + name: "ExecutionPointer"); + + migrationBuilder.DropTable( + name: "Workflow"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.Designer.cs new file mode 100644 index 000000000..21eb862bc --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.Designer.cs @@ -0,0 +1,237 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using WorkflowCore.Persistence.MySQL; +using WorkflowCore.Models; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20170312161610_Events")] + partial class Events + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("PersistedEvent"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Message"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("Active"); + + b.Property("ConcurrentFork"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey"); + + b.Property("EventName"); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("PathTerminator"); + + b.Property("PersistenceData"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("StepId"); + + b.Property("StepName"); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("PersistedExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("PersistedSubscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("PersistedWorkflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("Errors") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.cs new file mode 100644 index 000000000..8ecba38be --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170312161610_Events.cs @@ -0,0 +1,94 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class Events : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UnpublishedEvent"); + + migrationBuilder.AddColumn( + name: "SubscribeAsOf", + table: "Subscription", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.CreateTable( + name: "Event", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + EventData = table.Column(nullable: true), + EventId = table.Column(nullable: false), + EventKey = table.Column(maxLength: 200, nullable: true), + EventName = table.Column(maxLength: 200, nullable: true), + EventTime = table.Column(nullable: false), + IsProcessed = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Event", x => x.PersistenceId); + }); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventId", + table: "Event", + column: "EventId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventTime", + table: "Event", + column: "EventTime"); + + migrationBuilder.CreateIndex( + name: "IX_Event_IsProcessed", + table: "Event", + column: "IsProcessed"); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventName_EventKey", + table: "Event", + columns: new[] { "EventName", "EventKey" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Event"); + + migrationBuilder.DropColumn( + name: "SubscribeAsOf", + table: "Subscription"); + + migrationBuilder.CreateTable( + name: "UnpublishedEvent", + columns: table => new + { + PersistenceId = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + EventData = table.Column(nullable: true), + EventKey = table.Column(maxLength: 200, nullable: true), + EventName = table.Column(maxLength: 200, nullable: true), + PublicationId = table.Column(nullable: false), + StepId = table.Column(nullable: false), + WorkflowId = table.Column(maxLength: 200, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UnpublishedEvent", x => x.PersistenceId); + }); + + migrationBuilder.CreateIndex( + name: "IX_UnpublishedEvent_PublicationId", + table: "UnpublishedEvent", + column: "PublicationId", + unique: true); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.Designer.cs new file mode 100644 index 000000000..161803000 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.Designer.cs @@ -0,0 +1,234 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20170507214430_ControlStructures")] + partial class ControlStructures + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("PersistedEvent"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("PersistedExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("PersistedExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("PersistedSubscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("PersistedWorkflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.cs new file mode 100644 index 000000000..afa178e63 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170507214430_ControlStructures.cs @@ -0,0 +1,165 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class ControlStructures : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ExecutionError_ExecutionPointer_ExecutionPointerId", + table: "ExecutionError"); + + migrationBuilder.DropIndex( + name: "IX_ExecutionError_ExecutionPointerId", + table: "ExecutionError"); + + migrationBuilder.DropColumn( + name: "PathTerminator", + table: "ExecutionPointer"); + + migrationBuilder.DropColumn( + name: "Id", + table: "ExecutionError"); + + migrationBuilder.RenameColumn( + name: "ConcurrentFork", + table: "ExecutionPointer", + newName: "RetryCount").Annotation("Relational:ColumnType", "int"); + + migrationBuilder.AlterColumn( + name: "StepName", + table: "ExecutionPointer", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EventName", + table: "ExecutionPointer", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EventKey", + table: "ExecutionPointer", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Children", + table: "ExecutionPointer", + nullable: true); + + migrationBuilder.AddColumn( + name: "ContextItem", + table: "ExecutionPointer", + nullable: true); + + migrationBuilder.AddColumn( + name: "PredecessorId", + table: "ExecutionPointer", + maxLength: 100, + nullable: true); + + migrationBuilder.AlterColumn( + name: "ExecutionPointerId", + table: "ExecutionError", + maxLength: 100, + nullable: true, + oldClrType: typeof(long)); + + migrationBuilder.AddColumn( + name: "WorkflowId", + table: "ExecutionError", + maxLength: 100, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Children", + table: "ExecutionPointer"); + + migrationBuilder.DropColumn( + name: "ContextItem", + table: "ExecutionPointer"); + + migrationBuilder.DropColumn( + name: "PredecessorId", + table: "ExecutionPointer"); + + migrationBuilder.DropColumn( + name: "WorkflowId", + table: "ExecutionError"); + + migrationBuilder.RenameColumn( + name: "RetryCount", + table: "ExecutionPointer", + newName: "ConcurrentFork").Annotation("Relational:ColumnType", "int"); + + migrationBuilder.AlterColumn( + name: "StepName", + table: "ExecutionPointer", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EventName", + table: "ExecutionPointer", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EventKey", + table: "ExecutionPointer", + nullable: true, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "PathTerminator", + table: "ExecutionPointer", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "ExecutionPointerId", + table: "ExecutionError", + nullable: false, + oldClrType: typeof(string), + oldMaxLength: 100, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Id", + table: "ExecutionError", + maxLength: 50, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ExecutionError_ExecutionPointerId", + table: "ExecutionError", + column: "ExecutionPointerId"); + + migrationBuilder.AddForeignKey( + name: "FK_ExecutionError_ExecutionPointer_ExecutionPointerId", + table: "ExecutionError", + column: "ExecutionPointerId", + principalTable: "ExecutionPointer", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.Designer.cs new file mode 100644 index 000000000..77c52c52b --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.Designer.cs @@ -0,0 +1,238 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using WorkflowCore.Persistence.MySQL; +using WorkflowCore.Models; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20170519231452_PersistOutcome")] + partial class PersistOutcome + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("PersistedEvent"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("PersistedExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Outcome"); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("PersistedExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("PersistedSubscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("PersistedWorkflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.cs new file mode 100644 index 000000000..b866c55e3 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170519231452_PersistOutcome.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class PersistOutcome : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Outcome", + table: "ExecutionPointer", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Outcome", + table: "ExecutionPointer"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.Designer.cs new file mode 100644 index 000000000..4764d896b --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.Designer.cs @@ -0,0 +1,235 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using WorkflowCore.Persistence.MySQL; +using WorkflowCore.Models; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20170722200412_WfReference")] + partial class WfReference + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("PersistedEvent"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("PersistedExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Outcome"); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("PersistedExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("PersistedExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("PersistedSubscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Reference") + .HasMaxLength(200); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("PersistedWorkflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.cs new file mode 100644 index 000000000..ad738ec71 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20170722200412_WfReference.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class WfReference : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reference", + table: "Workflow", + maxLength: 200, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reference", + table: "Workflow"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.Designer.cs new file mode 100644 index 000000000..c4eace9b7 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.Designer.cs @@ -0,0 +1,244 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; +using WorkflowCore.Models; +using WorkflowCore.Persistence.MySQL; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20171223020844_StepScope")] + partial class StepScope + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Outcome"); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("Scope"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Reference") + .HasMaxLength(200); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.cs new file mode 100644 index 000000000..bec073b92 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20171223020844_StepScope.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class StepScope : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Scope", + table: "ExecutionPointer", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "ExecutionPointer", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Scope", + table: "ExecutionPointer"); + + migrationBuilder.DropColumn( + name: "Status", + table: "ExecutionPointer"); + + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.Designer.cs new file mode 100644 index 000000000..7f3acd2ce --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.Designer.cs @@ -0,0 +1,316 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.MySQL; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20200223041701_Activities")] + partial class Activities + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventData") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("EventId") + .HasColumnType("char(36)"); + + b.Property("EventKey") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("EventTime") + .HasColumnType("datetime(6)"); + + b.Property("IsProcessed") + .HasColumnType("tinyint(1)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ErrorTime") + .HasColumnType("datetime(6)"); + + b.Property("ExecutionPointerId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("Message") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("WorkflowId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Children") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ContextItem") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("EventData") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("EventKey") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("EventName") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("EventPublished") + .HasColumnType("tinyint(1)"); + + b.Property("Id") + .HasColumnType("varchar(50) CHARACTER SET utf8mb4") + .HasMaxLength(50); + + b.Property("Outcome") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("PersistenceData") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("PredecessorId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SleepUntil") + .HasColumnType("datetime(6)"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AttributeKey") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("AttributeValue") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ExecutionPointerId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ExternalToken") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime(6)"); + + b.Property("ExternalWorkerId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionData") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("SubscriptionId") + .HasColumnType("char(200)") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CompleteTime") + .HasColumnType("datetime(6)"); + + b.Property("CreateTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext CHARACTER SET utf8mb4"); + + b.Property("Description") + .HasColumnType("varchar(500) CHARACTER SET utf8mb4") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasColumnType("char(200)") + .HasMaxLength(200); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.cs new file mode 100644 index 000000000..38f78d9e5 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20200223041701_Activities.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class Activities : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExecutionPointerId", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalToken", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalTokenExpiry", + table: "Subscription", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalWorkerId", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "SubscriptionData", + table: "Subscription", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExecutionPointerId", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalToken", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalTokenExpiry", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalWorkerId", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "SubscriptionData", + table: "Subscription"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.Designer.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.Designer.cs new file mode 100644 index 000000000..94222e8d5 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.Designer.cs @@ -0,0 +1,357 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.MySQL; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + [Migration("20211023161949_scheduled-commands")] + partial class scheduledcommands + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventData") + .HasColumnType("longtext"); + + b.Property("EventId") + .HasColumnType("char(36)"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventTime") + .HasColumnType("datetime(6)"); + + b.Property("IsProcessed") + .HasColumnType("tinyint(1)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ErrorTime") + .HasColumnType("datetime(6)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Children") + .HasColumnType("longtext"); + + b.Property("ContextItem") + .HasColumnType("longtext"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("EventData") + .HasColumnType("longtext"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EventPublished") + .HasColumnType("tinyint(1)"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Outcome") + .HasColumnType("longtext"); + + b.Property("PersistenceData") + .HasColumnType("longtext"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("longtext"); + + b.Property("SleepUntil") + .HasColumnType("datetime(6)"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AttributeValue") + .HasColumnType("longtext"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique(); + + b.ToTable("ScheduledCommand"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime(6)"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionData") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("char(200)"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CompleteTime") + .HasColumnType("datetime(6)"); + + b.Property("CreateTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("char(200)"); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.cs new file mode 100644 index 000000000..b06303070 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/20211023161949_scheduled-commands.cs @@ -0,0 +1,337 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + public partial class scheduledcommands : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "InstanceId", + table: "Workflow", + type: "char(200)", + maxLength: 200, + nullable: false, + collation: "ascii_general_ci", + oldClrType: typeof(string), + oldType: "char(200)", + oldMaxLength: 200) + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Data", + table: "Workflow", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "SubscriptionId", + table: "Subscription", + type: "char(200)", + maxLength: 200, + nullable: false, + collation: "ascii_general_ci", + oldClrType: typeof(string), + oldType: "char(200)", + oldMaxLength: 200) + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "SubscriptionData", + table: "Subscription", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "AttributeValue", + table: "ExtensionAttribute", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Scope", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "PersistenceData", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Outcome", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "EventData", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "ContextItem", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Children", + table: "ExecutionPointer", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Message", + table: "ExecutionError", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "EventData", + table: "Event", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext CHARACTER SET utf8mb4", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ScheduledCommand", + columns: table => new + { + PersistenceId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + CommandName = table.Column(type: "varchar(200)", maxLength: 200, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Data = table.Column(type: "varchar(500)", maxLength: 500, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ExecuteTime = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScheduledCommand", x => x.PersistenceId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_CommandName_Data", + table: "ScheduledCommand", + columns: new[] { "CommandName", "Data" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_ExecuteTime", + table: "ScheduledCommand", + column: "ExecuteTime"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScheduledCommand"); + + migrationBuilder.AlterColumn( + name: "InstanceId", + table: "Workflow", + type: "char(200)", + maxLength: 200, + nullable: false, + oldClrType: typeof(Guid), + oldType: "char(200)", + oldMaxLength: 200) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "Data", + table: "Workflow", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "SubscriptionId", + table: "Subscription", + type: "char(200)", + maxLength: 200, + nullable: false, + oldClrType: typeof(Guid), + oldType: "char(200)", + oldMaxLength: 200) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "SubscriptionData", + table: "Subscription", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "AttributeValue", + table: "ExtensionAttribute", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Scope", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "PersistenceData", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Outcome", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "EventData", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "ContextItem", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Children", + table: "ExecutionPointer", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Message", + table: "ExecutionError", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "EventData", + table: "Event", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/MysqlPersistenceProviderModelSnapshot.cs b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/MysqlPersistenceProviderModelSnapshot.cs new file mode 100644 index 000000000..8a3e02e9b --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/MysqlPersistenceProviderModelSnapshot.cs @@ -0,0 +1,355 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.MySQL; + +namespace WorkflowCore.Persistence.MySQL.Migrations +{ + [DbContext(typeof(MysqlContext))] + partial class MysqlPersistenceProviderModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventData") + .HasColumnType("longtext"); + + b.Property("EventId") + .HasColumnType("char(36)"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventTime") + .HasColumnType("datetime(6)"); + + b.Property("IsProcessed") + .HasColumnType("tinyint(1)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ErrorTime") + .HasColumnType("datetime(6)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Children") + .HasColumnType("longtext"); + + b.Property("ContextItem") + .HasColumnType("longtext"); + + b.Property("EndTime") + .HasColumnType("datetime(6)"); + + b.Property("EventData") + .HasColumnType("longtext"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EventPublished") + .HasColumnType("tinyint(1)"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Outcome") + .HasColumnType("longtext"); + + b.Property("PersistenceData") + .HasColumnType("longtext"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("longtext"); + + b.Property("SleepUntil") + .HasColumnType("datetime(6)"); + + b.Property("StartTime") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AttributeValue") + .HasColumnType("longtext"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique(); + + b.ToTable("ScheduledCommand"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime(6)"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionData") + .HasColumnType("longtext"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("char(200)"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CompleteTime") + .HasColumnType("datetime(6)"); + + b.Property("CreateTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("char(200)"); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/MysqlContext.cs b/src/providers/WorkflowCore.Persistence.MySQL/MysqlContext.cs new file mode 100644 index 000000000..2b65f640d --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/MysqlContext.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using WorkflowCore.Persistence.EntityFramework.Models; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.MySQL +{ + public class MysqlContext : WorkflowDbContext + { + private readonly string _connectionString; + private readonly Action _mysqlOptionsAction; + + public MysqlContext(string connectionString, Action mysqlOptionsAction = null) + { + _connectionString = connectionString; + _mysqlOptionsAction = mysqlOptionsAction; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); +#if NETSTANDARD2_0 + optionsBuilder.UseMySql(_connectionString, _mysqlOptionsAction); +#elif NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + optionsBuilder.UseMySql(_connectionString, ServerVersion.AutoDetect(_connectionString), _mysqlOptionsAction); +#endif + } + + protected override void ConfigureSubscriptionStorage(EntityTypeBuilder builder) + { + builder.ToTable("Subscription"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureWorkflowStorage(EntityTypeBuilder builder) + { + builder.ToTable("Workflow"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureExecutionPointerStorage(EntityTypeBuilder builder) + { + builder.ToTable("ExecutionPointer"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureExecutionErrorStorage(EntityTypeBuilder builder) + { + builder.ToTable("ExecutionError"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureExetensionAttributeStorage(EntityTypeBuilder builder) + { + builder.ToTable("ExtensionAttribute"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureEventStorage(EntityTypeBuilder builder) + { + builder.ToTable("Event"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureScheduledCommandStorage(EntityTypeBuilder builder) + { + builder.ToTable("ScheduledCommand"); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/MysqlContextFactory.cs b/src/providers/WorkflowCore.Persistence.MySQL/MysqlContextFactory.cs new file mode 100644 index 000000000..428f0eeac --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/MysqlContextFactory.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.MySQL +{ + public class MysqlContextFactory : IWorkflowDbContextFactory + { + private readonly string _connectionString; + private readonly Action _mysqlOptionsAction; + + public MysqlContextFactory(string connectionString, Action mysqlOptionsAction = null) + { + _connectionString = connectionString; + _mysqlOptionsAction = mysqlOptionsAction; + } + + public WorkflowDbContext Build() + { + return new MysqlContext(_connectionString, _mysqlOptionsAction); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/README.md b/src/providers/WorkflowCore.Persistence.MySQL/README.md new file mode 100644 index 000000000..3eca71434 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/README.md @@ -0,0 +1,19 @@ +# MySQL Persistence provider for Workflow Core + +Provides support to persist workflows running on [Workflow Core](../../README.md) to a MySQL database. + +## Installing + +Install the NuGet package "WorkflowCore.Persistence.MySQL" + +``` +PM> Install-Package WorkflowCore.Persistence.MySQL -Pre +``` + +## Usage + +Use the .UseMySQL extension method when building your service provider. + +```C# +services.AddWorkflow(x => x.UseMySQL(@"Server=127.0.0.1;Database=workflow;User=root;Password=password;", true, true)); +``` diff --git a/src/providers/WorkflowCore.Persistence.MySQL/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.MySQL/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..b00fcc2c0 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/ServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Services; +using WorkflowCore.Persistence.MySQL; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseMySQL(this WorkflowOptions options, string connectionString, bool canCreateDB, bool canMigrateDB, Action mysqlOptionsAction = null) + { + options.UsePersistence(sp => new EntityFrameworkPersistenceProvider(new MysqlContextFactory(connectionString, mysqlOptionsAction), canCreateDB, canMigrateDB)); + options.Services.AddTransient(sp => new WorkflowPurger(new MysqlContextFactory(connectionString, mysqlOptionsAction))); + return options; + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj b/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj new file mode 100644 index 000000000..8b87ba216 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj @@ -0,0 +1,50 @@ + + + + Workflow Core MySQL Persistence Provider + 1.0.0 + Daniel Gerlag + netstandard2.1;net6.0 + WorkflowCore.Persistence.MySQL + WorkflowCore.Persistence.MySQL + workflow;.NET;Core;state machine;WorkflowCore;MySQL + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + git + https://github.com/danielgerlag/workflow-core.git + false + false + false + Provides support to persist workflows running on Workflow Core to a MySQL database. + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/MigrationContextFactory.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/MigrationContextFactory.cs index 10670d1ad..4424b01cd 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/MigrationContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/MigrationContextFactory.cs @@ -3,11 +3,11 @@ namespace WorkflowCore.Persistence.PostgreSQL { - public class MigrationContextFactory : IDesignTimeDbContextFactory + public class MigrationContextFactory : IDesignTimeDbContextFactory { - public PostgresPersistenceProvider CreateDbContext(string[] args) + public PostgresContext CreateDbContext(string[] args) { - return new PostgresPersistenceProvider(@"Server=127.0.0.1;Port=5432;Database=workflow;User Id=postgres;Password=password;", true, true); + return new PostgresContext(@"Server=127.0.0.1;Port=5432;Database=workflow;User Id=postgres;Password=password;","wfc"); } } } diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.Designer.cs index 5e2d8870a..b670d07ac 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.Designer.cs @@ -1,14 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.Models; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20170126230815_InitialDatabase")] partial class InitialDatabase { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs index eaa3af849..c74d0e6f1 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.Designer.cs index b8f1f01ab..3ca03d35d 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.Designer.cs @@ -1,14 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.Models; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20170312161610_Events")] partial class Events { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs index 71ee4473d..6be6f105b 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.Designer.cs index ae3911ab0..c68ca486f 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.Designer.cs @@ -1,14 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.Models; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20170507214430_ControlStructures")] partial class ControlStructures { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.cs index 8e44ba8aa..ddbe6630a 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170507214430_ControlStructures.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.PostgreSQL.Migrations diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.Designer.cs index 646fa3ff2..6e32384d0 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.Designer.cs @@ -1,14 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.Models; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20170519231452_PersistOutcome")] partial class PersistOutcome { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.cs index 4a5806080..0544d31e5 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170519231452_PersistOutcome.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.PostgreSQL.Migrations diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.Designer.cs index 945760e73..2bd0bd957 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.Designer.cs @@ -1,14 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.Models; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20170722200412_WfReference")] partial class WfReference { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.cs index 997299d80..a7058ff61 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170722200412_WfReference.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.PostgreSQL.Migrations diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.Designer.cs index 8aaa6a901..2220458b5 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.Designer.cs @@ -1,7 +1,7 @@ // using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; @@ -11,7 +11,7 @@ namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] [Migration("20171223020844_StepScope")] partial class StepScope { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.cs index 439bfd655..023ca1919 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20171223020844_StepScope.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; using System; -using System.Collections.Generic; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.Designer.cs new file mode 100644 index 000000000..0ba8dbd0a --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.Designer.cs @@ -0,0 +1,245 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WorkflowCore.Persistence.PostgreSQL; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20191214232718_SubscriptionData")] + partial class SubscriptionData + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Outcome"); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("Scope"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionData"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd(); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Reference") + .HasMaxLength(200); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.cs new file mode 100644 index 000000000..e95fcf6f7 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191214232718_SubscriptionData.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + public partial class SubscriptionData : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SubscriptionData", + schema: "wfc", + table: "Subscription", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubscriptionData", + schema: "wfc", + table: "Subscription"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.Designer.cs new file mode 100644 index 000000000..a067ca244 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.Designer.cs @@ -0,0 +1,324 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WorkflowCore.Persistence.PostgreSQL; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20191222174302_Activities")] + partial class Activities + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EventData") + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("EventKey") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("EventTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsProcessed") + .HasColumnType("boolean"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ErrorTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExecutionPointerId") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("WorkflowId") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Children") + .HasColumnType("text"); + + b.Property("ContextItem") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EventData") + .HasColumnType("text"); + + b.Property("EventKey") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("EventName") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("EventPublished") + .HasColumnType("boolean"); + + b.Property("Id") + .HasColumnType("character varying(50)") + .HasMaxLength(50); + + b.Property("Outcome") + .HasColumnType("text"); + + b.Property("PersistenceData") + .HasColumnType("text"); + + b.Property("PredecessorId") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.Property("SleepUntil") + .HasColumnType("timestamp without time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("StepName") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AttributeKey") + .HasColumnType("character varying(100)") + .HasMaxLength(100); + + b.Property("AttributeValue") + .HasColumnType("text"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EventKey") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("ExecutionPointerId") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("ExternalToken") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("ExternalTokenExpiry") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalWorkerId") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("SubscribeAsOf") + .HasColumnType("timestamp without time zone"); + + b.Property("SubscriptionData") + .HasColumnType("text"); + + b.Property("SubscriptionId") + .HasColumnType("uuid") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CompleteTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("character varying(500)") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasColumnType("uuid") + .HasMaxLength(200); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Version") + .HasColumnType("integer"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("character varying(200)") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.cs new file mode 100644 index 000000000..81787cf5b --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20191222174302_Activities.cs @@ -0,0 +1,170 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + public partial class Activities : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Workflow", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Subscription", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + migrationBuilder.AddColumn( + name: "ExecutionPointerId", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalToken", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalWorkerId", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExtensionAttribute", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExecutionPointer", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExecutionError", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Event", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExecutionPointerId", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalToken", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalWorkerId", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Workflow", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Subscription", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExtensionAttribute", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExecutionPointer", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "ExecutionError", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + migrationBuilder.AlterColumn( + name: "PersistenceId", + schema: "wfc", + table: "Event", + nullable: false, + oldClrType: typeof(long)) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.Designer.cs new file mode 100644 index 000000000..0608d5e41 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.Designer.cs @@ -0,0 +1,366 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WorkflowCore.Persistence.PostgreSQL; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20211023161649_scheduled-commands")] + partial class scheduledcommands + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.8") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EventData") + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EventTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsProcessed") + .HasColumnType("boolean"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("ErrorTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Children") + .HasColumnType("text"); + + b.Property("ContextItem") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EventData") + .HasColumnType("text"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EventPublished") + .HasColumnType("boolean"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Outcome") + .HasColumnType("text"); + + b.Property("PersistenceData") + .HasColumnType("text"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.Property("SleepUntil") + .HasColumnType("timestamp without time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("AttributeValue") + .HasColumnType("text"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique(); + + b.ToTable("ScheduledCommand", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("SubscribeAsOf") + .HasColumnType("timestamp without time zone"); + + b.Property("SubscriptionData") + .HasColumnType("text"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("uuid"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CompleteTime") + .HasColumnType("timestamp without time zone"); + + b.Property("CreateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("uuid"); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Version") + .HasColumnType("integer"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.cs new file mode 100644 index 000000000..0f1302181 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20211023161649_scheduled-commands.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + public partial class scheduledcommands : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScheduledCommand", + schema: "wfc", + columns: table => new + { + PersistenceId = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CommandName = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + Data = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + ExecuteTime = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScheduledCommand", x => x.PersistenceId); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_CommandName_Data", + schema: "wfc", + table: "ScheduledCommand", + columns: new[] { "CommandName", "Data" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_ExecuteTime", + schema: "wfc", + table: "ScheduledCommand", + column: "ExecuteTime"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScheduledCommand", + schema: "wfc"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs index 7fc91a776..d0278e0c8 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs @@ -1,44 +1,50 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using System; -using WorkflowCore.Models; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using WorkflowCore.Persistence.PostgreSQL; namespace WorkflowCore.Persistence.PostgreSQL.Migrations { - [DbContext(typeof(PostgresPersistenceProvider))] + [DbContext(typeof(PostgresContext))] partial class PostgresPersistenceProviderModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) - .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.8") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("EventData"); + b.Property("EventData") + .HasColumnType("text"); - b.Property("EventId"); + b.Property("EventId") + .HasColumnType("uuid"); b.Property("EventKey") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("EventName") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); - b.Property("EventTime"); + b.Property("EventTime") + .HasColumnType("timestamp without time zone"); - b.Property("IsProcessed"); + b.Property("IsProcessed") + .HasColumnType("boolean"); b.HasKey("PersistenceId"); @@ -51,125 +57,212 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventName", "EventKey"); - b.ToTable("Event","wfc"); + b.ToTable("Event", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("ErrorTime"); + b.Property("ErrorTime") + .HasColumnType("timestamp without time zone"); b.Property("ExecutionPointerId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); - b.Property("Message"); + b.Property("Message") + .HasColumnType("text"); b.Property("WorkflowId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.HasKey("PersistenceId"); - b.ToTable("ExecutionError","wfc"); + b.ToTable("ExecutionError", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("Active"); + b.Property("Active") + .HasColumnType("boolean"); - b.Property("Children"); + b.Property("Children") + .HasColumnType("text"); - b.Property("ContextItem"); + b.Property("ContextItem") + .HasColumnType("text"); - b.Property("EndTime"); + b.Property("EndTime") + .HasColumnType("timestamp without time zone"); - b.Property("EventData"); + b.Property("EventData") + .HasColumnType("text"); b.Property("EventKey") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("EventName") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); - b.Property("EventPublished"); + b.Property("EventPublished") + .HasColumnType("boolean"); b.Property("Id") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("character varying(50)"); - b.Property("Outcome"); + b.Property("Outcome") + .HasColumnType("text"); - b.Property("PersistenceData"); + b.Property("PersistenceData") + .HasColumnType("text"); b.Property("PredecessorId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); - b.Property("RetryCount"); + b.Property("RetryCount") + .HasColumnType("integer"); - b.Property("Scope"); + b.Property("Scope") + .HasColumnType("text"); - b.Property("SleepUntil"); + b.Property("SleepUntil") + .HasColumnType("timestamp without time zone"); - b.Property("StartTime"); + b.Property("StartTime") + .HasColumnType("timestamp without time zone"); - b.Property("Status"); + b.Property("Status") + .HasColumnType("integer"); - b.Property("StepId"); + b.Property("StepId") + .HasColumnType("integer"); b.Property("StepName") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); - b.Property("WorkflowId"); + b.Property("WorkflowId") + .HasColumnType("bigint"); b.HasKey("PersistenceId"); b.HasIndex("WorkflowId"); - b.ToTable("ExecutionPointer","wfc"); + b.ToTable("ExecutionPointer", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("AttributeKey") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); - b.Property("AttributeValue"); + b.Property("AttributeValue") + .HasColumnType("text"); - b.Property("ExecutionPointerId"); + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); b.HasKey("PersistenceId"); b.HasIndex("ExecutionPointerId"); - b.ToTable("ExtensionAttribute","wfc"); + b.ToTable("ExtensionAttribute", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique(); + + b.ToTable("ScheduledCommand", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); b.Property("EventKey") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("EventName") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); - b.Property("StepId"); + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); - b.Property("SubscribeAsOf"); + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("SubscribeAsOf") + .HasColumnType("timestamp without time zone"); + + b.Property("SubscriptionData") + .HasColumnType("text"); b.Property("SubscriptionId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uuid"); b.Property("WorkflowId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("PersistenceId"); @@ -180,37 +273,49 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SubscriptionId") .IsUnique(); - b.ToTable("Subscription","wfc"); + b.ToTable("Subscription", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => { b.Property("PersistenceId") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); - b.Property("CompleteTime"); + b.Property("CompleteTime") + .HasColumnType("timestamp without time zone"); - b.Property("CreateTime"); + b.Property("CreateTime") + .HasColumnType("timestamp without time zone"); - b.Property("Data"); + b.Property("Data") + .HasColumnType("text"); b.Property("Description") - .HasMaxLength(500); + .HasMaxLength(500) + .HasColumnType("character varying(500)"); b.Property("InstanceId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uuid"); - b.Property("NextExecution"); + b.Property("NextExecution") + .HasColumnType("bigint"); b.Property("Reference") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); - b.Property("Status"); + b.Property("Status") + .HasColumnType("integer"); - b.Property("Version"); + b.Property("Version") + .HasColumnType("integer"); b.Property("WorkflowDefinitionId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("PersistenceId"); @@ -219,7 +324,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("NextExecution"); - b.ToTable("Workflow","wfc"); + b.ToTable("Workflow", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => @@ -227,7 +332,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") .WithMany("ExecutionPointers") .HasForeignKey("WorkflowId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -235,7 +343,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") .WithMany("ExtensionAttributes") .HasForeignKey("ExecutionPointerId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); }); #pragma warning restore 612, 618 } diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresPersistenceProvider.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContext.cs similarity index 68% rename from src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresPersistenceProvider.cs rename to src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContext.cs index ecd8fcda8..3eb80b448 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContext.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using WorkflowCore.Persistence.EntityFramework.Models; @@ -9,14 +7,16 @@ namespace WorkflowCore.Persistence.PostgreSQL { - public class PostgresPersistenceProvider : EntityFrameworkPersistenceProvider + public class PostgresContext : WorkflowDbContext { private readonly string _connectionString; + private readonly string _schemaName; - public PostgresPersistenceProvider(string connectionString, bool canCreateDB, bool canMigrateDB) - :base(canCreateDB, canMigrateDB) + public PostgresContext(string connectionString,string schemaName) + :base() { _connectionString = connectionString; + _schemaName = schemaName; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -27,37 +27,43 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void ConfigureSubscriptionStorage(EntityTypeBuilder builder) { - builder.ToTable("Subscription", "wfc"); + builder.ToTable("Subscription", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } protected override void ConfigureWorkflowStorage(EntityTypeBuilder builder) { - builder.ToTable("Workflow", "wfc"); + builder.ToTable("Workflow", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } protected override void ConfigureExecutionPointerStorage(EntityTypeBuilder builder) { - builder.ToTable("ExecutionPointer", "wfc"); + builder.ToTable("ExecutionPointer", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } protected override void ConfigureExecutionErrorStorage(EntityTypeBuilder builder) { - builder.ToTable("ExecutionError", "wfc"); + builder.ToTable("ExecutionError", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } protected override void ConfigureExetensionAttributeStorage(EntityTypeBuilder builder) { - builder.ToTable("ExtensionAttribute", "wfc"); + builder.ToTable("ExtensionAttribute", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } protected override void ConfigureEventStorage(EntityTypeBuilder builder) { - builder.ToTable("Event", "wfc"); + builder.ToTable("Event", _schemaName); + builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); + } + + protected override void ConfigureScheduledCommandStorage(EntityTypeBuilder builder) + { + builder.ToTable("ScheduledCommand", _schemaName); builder.Property(x => x.PersistenceId).ValueGeneratedOnAdd(); } } diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs new file mode 100644 index 000000000..98a633e1d --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs @@ -0,0 +1,23 @@ +using System; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.PostgreSQL +{ + public class PostgresContextFactory : IWorkflowDbContextFactory + { + private readonly string _connectionString; + private readonly string _schemaName; + + public PostgresContextFactory(string connectionString, string schemaName) + { + _connectionString = connectionString; + _schemaName = schemaName; + } + + public WorkflowDbContext Build() + { + return new PostgresContext(_connectionString,_schemaName); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Properties/AssemblyInfo.cs index 036cf6f38..01280515a 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.cs index 203c336f5..9366c3936 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.cs @@ -1,17 +1,19 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.PostgreSQL; namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static WorkflowOptions UsePostgreSQL(this WorkflowOptions options, string connectionString, bool canCreateDB, bool canMigrateDB) + public static WorkflowOptions UsePostgreSQL(this WorkflowOptions options, + string connectionString, bool canCreateDB, bool canMigrateDB, string schemaName="wfc") { - options.UsePersistence(sp => new PostgresPersistenceProvider(connectionString, canCreateDB, canMigrateDB)); + options.UsePersistence(sp => new EntityFrameworkPersistenceProvider(new PostgresContextFactory(connectionString, schemaName), canCreateDB, canMigrateDB)); + options.Services.AddTransient(sp => new WorkflowPurger(new PostgresContextFactory(connectionString, schemaName))); return options; } } diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj b/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj index 6dcd7f3b2..583c706f0 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj @@ -2,9 +2,8 @@ Workflow Core PostgreSQL Persistence Provider - 1.5.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0 WorkflowCore.Persistence.PostgreSQL WorkflowCore.Persistence.PostgreSQL workflow;.NET;Core;state machine;WorkflowCore;PostgreSQL @@ -16,9 +15,6 @@ false false Provides support to persist workflows running on Workflow Core to a PostgreSQL database. - 1.6.0 - 1.6.0.0 - 1.6.0.0 @@ -26,14 +22,28 @@ - - - - + + + + All - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/README.md b/src/providers/WorkflowCore.Persistence.RavenDB/README.md new file mode 100644 index 000000000..fc4835e49 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/README.md @@ -0,0 +1,31 @@ + +# RavenDB Persistence provider for Workflow Core + +Provides support to persist workflows running on [Workflow Core](../../README.md) to a RavenDB database. + +## Installing + +Install the NuGet package "WorkflowCore.Persistence.RavenDB" + +``` +PM> Install-Package WorkflowCore.Persistence.RavenDB +``` + +## Usage + +Compose your RavenStoreOptions using the model provided by the library. + +```C# +var options = new RavenStoreOptions { + ServerUrl = "https://ravendbserver.domain.com:8080", + DatabaseName = "TestDatabase", + CertificatePath = System.IO.Path.Combine(AppContext.BaseDirectory, "Resources/servercert.pfx"), + CertificatePassword = "CertificatePassword" +} +``` + +Use the `.UseRavenDB` extension method when building your service provider, passing in the options you configured for the RavenDB store. + +```C# +services.AddWorkflow(x => x.UseRavenDB(options)); +``` diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/RavenStoreOptions.cs b/src/providers/WorkflowCore.Persistence.RavenDB/RavenStoreOptions.cs new file mode 100644 index 000000000..9208e0ce1 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/RavenStoreOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace WorkflowCore.Persistence.RavenDB +{ + public class RavenStoreOptions + { + public string ServerUrl { get; set; } + public string DatabaseName { get; set; } + public string CertificatePath { get; set; } + public string CertificatePassword { get; set; } + } +} diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.RavenDB/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..fb05b9ba3 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/ServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using System; +using WorkflowCore.Models; +using Raven.Client.Documents; +using System.Security.Cryptography.X509Certificates; +using WorkflowCore.Persistence.RavenDB.Services; +using WorkflowCore.Interface; +using WorkflowCore.Persistence.RavenDB; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseRavenDB(this WorkflowOptions options, RavenStoreOptions configOptions) + { + IDocumentStore store = new DocumentStore + { + Urls = new[] { configOptions.ServerUrl }, + Database = configOptions.DatabaseName, + Certificate = new X509Certificate2(configOptions.CertificatePath, configOptions.CertificatePassword) + }.Initialize(); + + options.UsePersistence(sp => + { + return new RavendbPersistenceProvider(store); + }); + + options.Services.AddTransient(sp => + { + return new WorkflowPurger(store); + }); + + return options; + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavenDbIndexes.cs b/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavenDbIndexes.cs new file mode 100644 index 000000000..77b4dbae5 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavenDbIndexes.cs @@ -0,0 +1,12 @@ +using Raven.Client.Documents.Indexes; +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.Persistence.RavenDB.Services +{ + // TODO: Implement Map for result bind of Index + public class WorkflowInstances_Id : AbstractIndexCreationTask { } + public class EventSubscriptions_Id : AbstractIndexCreationTask { } + public class Events_Id : AbstractIndexCreationTask { } + public class ExecutionErrors_Id : AbstractIndexCreationTask { } +} diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavendbPersistenceProvider.cs b/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavendbPersistenceProvider.cs new file mode 100644 index 000000000..3e1d5e2ea --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/Services/RavendbPersistenceProvider.cs @@ -0,0 +1,352 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Linq; +using Raven.Client.Documents.Operations; +using Raven.Client.Documents.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Persistence.RavenDB.Services +{ + public class RavendbPersistenceProvider : IPersistenceProvider + { + internal const string WorkflowCollectionName = "wfc.workflows"; + private readonly IDocumentStore _database; + static bool indexesCreated = false; + + public bool SupportsScheduledCommands => false; + + public RavendbPersistenceProvider(IDocumentStore database) + { + _database = database; + CreateIndexes(this); + } + + static void CreateIndexes(RavendbPersistenceProvider instance) + { + if (!indexesCreated) + { + /* + // create the indexes here based on assemby of classes in the file 'RavenDbIndexes.cs' + IndexCreation.CreateIndexes(typeof(WorkflowInstances_Id).Assembly, instance._database); + IndexCreation.CreateIndexes(typeof(EventSubscriptions_Id).Assembly, instance._database); + IndexCreation.CreateIndexes(typeof(Events_Id).Assembly, instance._database); + IndexCreation.CreateIndexes(typeof(ExecutionErrors_Id).Assembly, instance._database); + */ + indexesCreated = true; + } + } + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + await session.StoreAsync(workflow, cancellationToken); + var id = workflow.Id; + await session.SaveChangesAsync(cancellationToken); + return id; + } + } + + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + PatchSession(session, workflow); + await session.SaveChangesAsync(cancellationToken); + } + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + PatchSession(session, workflow); + + foreach (var subscription in subscriptions) + { + await session.StoreAsync(subscription, cancellationToken); + } + + await session.SaveChangesAsync(cancellationToken); + } + } + + private void PatchSession(IAsyncDocumentSession session, WorkflowInstance workflow) + { + session.Advanced.Patch(workflow.Id, x => x.WorkflowDefinitionId, workflow.WorkflowDefinitionId); + session.Advanced.Patch(workflow.Id, x => x.Version, workflow.Version); + session.Advanced.Patch(workflow.Id, x => x.Description, workflow.Description); + session.Advanced.Patch(workflow.Id, x => x.Reference, workflow.Reference); + session.Advanced.Patch(workflow.Id, x => x.ExecutionPointers, workflow.ExecutionPointers); + session.Advanced.Patch(workflow.Id, x => x.NextExecution, workflow.NextExecution); + session.Advanced.Patch(workflow.Id, x => x.Status, workflow.Status); + session.Advanced.Patch(workflow.Id, x => x.Data, workflow.Data); + session.Advanced.Patch(workflow.Id, x => x.CreateTime, workflow.CreateTime); + session.Advanced.Patch(workflow.Id, x => x.CompleteTime, workflow.CompleteTime); + + } + + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) + { + var now = asAt.ToUniversalTime().Ticks; + using (var session = _database.OpenAsyncSession()) + { + var l = session.Query().Where(w => w.NextExecution.HasValue + && (w.NextExecution <= now) + && (w.Status == WorkflowStatus.Runnable) + ).Select(x => x.Id); + + return await l.ToListAsync(cancellationToken); + } + } + + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var result = await session.Query().FirstOrDefaultAsync(x => x.Id == Id, cancellationToken); + return result; + } + } + + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) + { + if (ids == null) + { + return new List(); + } + + using (var session = _database.OpenAsyncSession()) + { + var list = session.Query().Where(x => x.Id.In(ids)); + return await list.ToListAsync(cancellationToken); + } + } + + public async Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) + { + using (var session = _database.OpenAsyncSession()) + { + var result = session.Query(); + + if (status.HasValue) + result = result.Where(x => x.Status == status.Value); + + if (!String.IsNullOrEmpty(type)) + result = result.Where(x => x.WorkflowDefinitionId == type); + + if (createdFrom.HasValue) + result = result.Where(x => x.CreateTime >= createdFrom.Value); + + if (createdTo.HasValue) + result = result.Where(x => x.CreateTime <= createdTo.Value); + + return await result.Skip(skip).Take(take).ToListAsync(); + } + } + + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + await session.StoreAsync(subscription, cancellationToken); + var id = subscription.Id; + await session.SaveChangesAsync(cancellationToken); + return id; + } + } + + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) + { + using (var session = _database.OpenAsyncSession()) + { + session.Delete(eventSubscriptionId); + await Task.CompletedTask; + } + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var result = session.Query().Where(x => x.Id == eventSubscriptionId); + return await result.FirstOrDefaultAsync(cancellationToken); + } + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var q = session.Query().Where(x => + x.EventName == eventKey + && x.EventKey == eventKey + && x.SubscribeAsOf <= asOf + && x.ExternalToken == null + ); + + return await q.FirstOrDefaultAsync(cancellationToken); + } + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) + { + try + { + // The query string + var strbuilder = new StringBuilder(); + strbuilder.Append("from EventSubscriptions as e "); + strbuilder.Append($"where e.Id = {eventSubscriptionId} and ExternalToken = null"); + strbuilder.Append("update"); + strbuilder.Append("{"); + strbuilder.Append($"e.ExternalToken = '{token}'"); + strbuilder.Append($"e.ExternalTokenExpiry = '{expiry}'"); + strbuilder.Append($"e.ExternalWorkerId = 'workerId'"); + strbuilder.Append("}"); + + var operation = await _database.Operations.SendAsync(new PatchByQueryOperation(strbuilder.ToString()), token: cancellationToken); + operation.WaitForCompletion(); + return true; + } + catch (Exception e) + { + return false; + } + } + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) + { + try + { + // The query string + var strbuilder = new StringBuilder(); + strbuilder.Append("from EventSubscriptions as e "); + strbuilder.Append($"where e.Id = {eventSubscriptionId} and ExternalToken = '{token}'"); + strbuilder.Append("update"); + strbuilder.Append("{"); + strbuilder.Append($"e.ExternalToken = null"); + strbuilder.Append($"e.ExternalTokenExpiry = null"); + strbuilder.Append($"e.ExternalWorkerId = null"); + strbuilder.Append("}"); + + var operation = await _database.Operations.SendAsync(new PatchByQueryOperation(strbuilder.ToString()), token: cancellationToken); + operation.WaitForCompletion(); + } + catch (Exception e) + { + throw e; + } + } + + public void EnsureStoreExists() { } + + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var q = session.Query().Where(x => + x.EventName == eventName + && x.EventKey == eventKey + && x.SubscribeAsOf <= asOf + ); + + return await q.ToListAsync(cancellationToken); + } + } + + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + await session.StoreAsync(newEvent, cancellationToken); + var id = newEvent.Id; + await session.SaveChangesAsync(cancellationToken); + return id; + } + } + + public async Task GetEvent(string id, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var result = session.Query().Where(x => x.Id == id); + return await result.FirstAsync(cancellationToken); + } + } + + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + var now = asAt.ToUniversalTime(); + var result = session.Query() + .Where(x => !x.IsProcessed && x.EventTime < now) + .Select(x => x.Id); + + return await result.ToListAsync(cancellationToken); + } + } + + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + session.Advanced.Patch(id, x => x.IsProcessed, true); + + await session.SaveChangesAsync(cancellationToken); + } + } + + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) + { + using (var session = _database.OpenAsyncSession()) + { + var q = session.Query() + .Where(x => + x.EventName == eventName + && x.EventKey == eventKey + && x.EventTime >= asOf) + .Select(x => x.Id); + + return await q.ToListAsync(cancellationToken); + } + } + + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) + { + using (var session = _database.OpenAsyncSession()) + { + session.Advanced.Patch(id, x => x.IsProcessed, false); + + await session.SaveChangesAsync(cancellationToken); + } + } + + public async Task PersistErrors(IEnumerable errors, CancellationToken cancellationToken = default) + { + if (errors.Any()) + { + var blk = _database.BulkInsert(token: cancellationToken); + foreach (var error in errors) + await blk.StoreAsync(error); + + } + } + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/Services/WorkflowPurger.cs b/src/providers/WorkflowCore.Persistence.RavenDB/Services/WorkflowPurger.cs new file mode 100644 index 000000000..c70fc09fd --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/Services/WorkflowPurger.cs @@ -0,0 +1,38 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using System.Threading.Tasks; +using Raven.Client.Documents; +using Raven.Client.Documents.Operations; +using Raven.Client.Documents.Queries; +using System.Threading; + +namespace WorkflowCore.Persistence.RavenDB.Services +{ + public class WorkflowPurger : IWorkflowPurger + { + private readonly IDocumentStore _database; + + public WorkflowPurger(IDocumentStore database) + { + _database = database; + } + + public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + await DeleteWorkflowInstances(status, olderThan, cancellationToken); + } + + + /// + /// Delete all Workflow Documents + /// + /// + private Task DeleteWorkflowInstances(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + var utcTime = olderThan.ToUniversalTime(); + var queryToDelete = new IndexQuery { Query = $"FROM {nameof(WorkflowInstance)} where status = '{status}' and CompleteTime < '{olderThan}'" }; + return _database.Operations.SendAsync(new DeleteByQueryOperation(queryToDelete, new QueryOperationOptions { AllowStale = false }), token: cancellationToken); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.RavenDB/WorkflowCore.Persistence.RavenDB.csproj b/src/providers/WorkflowCore.Persistence.RavenDB/WorkflowCore.Persistence.RavenDB.csproj new file mode 100644 index 000000000..69c8e94e7 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.RavenDB/WorkflowCore.Persistence.RavenDB.csproj @@ -0,0 +1,32 @@ + + + + Workflow Core RavenDB Persistence Provider + netstandard2.0 + WorkflowCore.Persistence.RavenDB + WorkflowCore.Persistence.RavenDB + workflow;.NET;Core;state machine;WorkflowCore;RavenDB + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + git + https://github.com/danielgerlag/workflow-core.git + false + false + false + Provides support to persist workflows running on Workflow Core to a RavenDB database. + + + + + + + + + + + + + + + + diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/MigrationContextFactory.cs b/src/providers/WorkflowCore.Persistence.SqlServer/MigrationContextFactory.cs index fccda6b2a..5481b310d 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/MigrationContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/MigrationContextFactory.cs @@ -3,11 +3,11 @@ namespace WorkflowCore.Persistence.SqlServer { - public class MigrationContextFactory : IDesignTimeDbContextFactory + public class MigrationContextFactory : IDesignTimeDbContextFactory { - public SqlServerPersistenceProvider CreateDbContext(string[] args) + public SqlServerContext CreateDbContext(string[] args) { - return new SqlServerPersistenceProvider(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;", true, true); + return new SqlServerContext(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;"); } } } diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.Designer.cs index 63aea02d1..8d4468da0 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.Designer.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20170126230839_InitialDatabase")] partial class InitialDatabase { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.cs index 12153905d..cc13adb32 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170126230839_InitialDatabase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.Designer.cs index c06faa691..1ef0bc767 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.Designer.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20170312161610_Events")] partial class Events { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.cs index bc4565904..c3854150f 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170312161610_Events.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.Designer.cs index cd5bf357e..c3f8548b7 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.Designer.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20170507214430_ControlStructures")] partial class ControlStructures { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.cs index 18e8c5b38..df0f88ab3 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170507214430_ControlStructures.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.SqlServer.Migrations diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.Designer.cs index c9173c824..9e6f9981e 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.Designer.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20170519231452_PersistOutcome")] partial class PersistOutcome { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.cs index b4fd690ae..dd472520e 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170519231452_PersistOutcome.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.SqlServer.Migrations diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.Designer.cs index 380fb7a2c..6738a21dd 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.Designer.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20170722195832_WfReference")] partial class WfReference { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.cs index 1e94cad6d..285e7d944 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20170722195832_WfReference.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; namespace WorkflowCore.Persistence.SqlServer.Migrations diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.Designer.cs index 21ede0209..3c12a0d96 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.Designer.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.Designer.cs @@ -11,7 +11,7 @@ namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] [Migration("20171223020645_StepScope")] partial class StepScope { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.cs index e9ff500e1..c7e1e4ec6 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20171223020645_StepScope.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; using System; -using System.Collections.Generic; namespace WorkflowCore.Persistence.SqlServer.Migrations { diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.Designer.cs new file mode 100644 index 000000000..2ba59aee2 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.Designer.cs @@ -0,0 +1,251 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.SqlServer; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerContext))] + [Migration("20191214232800_SubscriptionData")] + partial class SubscriptionData + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventData"); + + b.Property("EventId"); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("EventTime"); + + b.Property("IsProcessed"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100); + + b.Property("Message"); + + b.Property("WorkflowId") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Active"); + + b.Property("Children"); + + b.Property("ContextItem"); + + b.Property("EndTime"); + + b.Property("EventData"); + + b.Property("EventKey") + .HasMaxLength(100); + + b.Property("EventName") + .HasMaxLength(100); + + b.Property("EventPublished"); + + b.Property("Id") + .HasMaxLength(50); + + b.Property("Outcome"); + + b.Property("PersistenceData"); + + b.Property("PredecessorId") + .HasMaxLength(100); + + b.Property("RetryCount"); + + b.Property("Scope"); + + b.Property("SleepUntil"); + + b.Property("StartTime"); + + b.Property("Status"); + + b.Property("StepId"); + + b.Property("StepName") + .HasMaxLength(100); + + b.Property("WorkflowId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasMaxLength(100); + + b.Property("AttributeValue"); + + b.Property("ExecutionPointerId"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasMaxLength(200); + + b.Property("EventName") + .HasMaxLength(200); + + b.Property("StepId"); + + b.Property("SubscribeAsOf"); + + b.Property("SubscriptionData"); + + b.Property("SubscriptionId") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime"); + + b.Property("CreateTime"); + + b.Property("Data"); + + b.Property("Description") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasMaxLength(200); + + b.Property("NextExecution"); + + b.Property("Reference") + .HasMaxLength(200); + + b.Property("Status"); + + b.Property("Version"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.cs new file mode 100644 index 000000000..cc0f797e0 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191214232800_SubscriptionData.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + public partial class SubscriptionData : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SubscriptionData", + schema: "wfc", + table: "Subscription", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubscriptionData", + schema: "wfc", + table: "Subscription"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.Designer.cs new file mode 100644 index 000000000..b7e6afd54 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.Designer.cs @@ -0,0 +1,336 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.SqlServer; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerContext))] + [Migration("20191221210710_Activities")] + partial class Activities + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventKey") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("EventTime") + .HasColumnType("datetime2"); + + b.Property("IsProcessed") + .HasColumnType("bit"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime") + .HasColumnType("datetime2"); + + b.Property("ExecutionPointerId") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowId") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Children") + .HasColumnType("nvarchar(max)"); + + b.Property("ContextItem") + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventKey") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("EventName") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("EventPublished") + .HasColumnType("bit"); + + b.Property("Id") + .HasColumnType("nvarchar(50)") + .HasMaxLength(50); + + b.Property("Outcome") + .HasColumnType("nvarchar(max)"); + + b.Property("PersistenceData") + .HasColumnType("nvarchar(max)"); + + b.Property("PredecessorId") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("nvarchar(max)"); + + b.Property("SleepUntil") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasColumnType("nvarchar(100)") + .HasMaxLength(100); + + b.Property("AttributeValue") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("EventName") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("ExecutionPointerId") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("ExternalToken") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkerId") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime2"); + + b.Property("SubscriptionData") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("uniqueidentifier") + .HasMaxLength(200); + + b.Property("WorkflowId") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime") + .HasColumnType("datetime2"); + + b.Property("CreateTime") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(500)") + .HasMaxLength(500); + + b.Property("InstanceId") + .HasColumnType("uniqueidentifier") + .HasMaxLength(200); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("nvarchar(200)") + .HasMaxLength(200); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow","wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.cs new file mode 100644 index 000000000..70dfb4ac3 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20191221210710_Activities.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + public partial class Activities : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExecutionPointerId", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalToken", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription", + nullable: true); + + migrationBuilder.AddColumn( + name: "ExternalWorkerId", + schema: "wfc", + table: "Subscription", + maxLength: 200, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExecutionPointerId", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalToken", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription"); + + migrationBuilder.DropColumn( + name: "ExternalWorkerId", + schema: "wfc", + table: "Subscription"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.Designer.cs new file mode 100644 index 000000000..2fe0f62d2 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.Designer.cs @@ -0,0 +1,338 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.SqlServer; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerContext))] + [Migration("20210109172432_EFCore5")] + partial class EFCore5 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseIdentityColumns() + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventTime") + .HasColumnType("datetime2"); + + b.Property("IsProcessed") + .HasColumnType("bit"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("ErrorTime") + .HasColumnType("datetime2"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Children") + .HasColumnType("nvarchar(max)"); + + b.Property("ContextItem") + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EventPublished") + .HasColumnType("bit"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Outcome") + .HasColumnType("nvarchar(max)"); + + b.Property("PersistenceData") + .HasColumnType("nvarchar(max)"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("nvarchar(max)"); + + b.Property("SleepUntil") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AttributeValue") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime2"); + + b.Property("SubscriptionData") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .UseIdentityColumn(); + + b.Property("CompleteTime") + .HasColumnType("datetime2"); + + b.Property("CreateTime") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.cs new file mode 100644 index 000000000..e36775325 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20210109172432_EFCore5.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + public partial class EFCore5 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.Designer.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.Designer.cs new file mode 100644 index 000000000..84bed0be0 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.Designer.cs @@ -0,0 +1,381 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WorkflowCore.Persistence.SqlServer; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + [DbContext(typeof(SqlServerContext))] + [Migration("20211023161544_scheduled-commands")] + partial class scheduledcommands + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.8") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventId") + .HasColumnType("uniqueidentifier"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventTime") + .HasColumnType("datetime2"); + + b.Property("IsProcessed") + .HasColumnType("bit"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ErrorTime") + .HasColumnType("datetime2"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Active") + .HasColumnType("bit"); + + b.Property("Children") + .HasColumnType("nvarchar(max)"); + + b.Property("ContextItem") + .HasColumnType("nvarchar(max)"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("EventData") + .HasColumnType("nvarchar(max)"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EventPublished") + .HasColumnType("bit"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Outcome") + .HasColumnType("nvarchar(max)"); + + b.Property("PersistenceData") + .HasColumnType("nvarchar(max)"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("nvarchar(max)"); + + b.Property("SleepUntil") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("WorkflowId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("AttributeValue") + .HasColumnType("nvarchar(max)"); + + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique() + .HasFilter("[CommandName] IS NOT NULL AND [Data] IS NOT NULL"); + + b.ToTable("ScheduledCommand", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime2"); + + b.Property("SubscriptionData") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CompleteTime") + .HasColumnType("datetime2"); + + b.Property("CreateTime") + .HasColumnType("datetime2"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); + + b.Property("NextExecution") + .HasColumnType("bigint"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("int"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") + .WithMany("ExecutionPointers") + .HasForeignKey("WorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") + .WithMany("ExtensionAttributes") + .HasForeignKey("ExecutionPointerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.cs new file mode 100644 index 000000000..2d5ec1572 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/20211023161544_scheduled-commands.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace WorkflowCore.Persistence.SqlServer.Migrations +{ + public partial class scheduledcommands : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ScheduledCommand", + schema: "wfc", + columns: table => new + { + PersistenceId = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CommandName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Data = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + ExecuteTime = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScheduledCommand", x => x.PersistenceId); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_CommandName_Data", + schema: "wfc", + table: "ScheduledCommand", + columns: new[] { "CommandName", "Data" }, + unique: true, + filter: "[CommandName] IS NOT NULL AND [Data] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_ExecuteTime", + schema: "wfc", + table: "ScheduledCommand", + column: "ExecuteTime"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ScheduledCommand", + schema: "wfc"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs index 82e959442..39da38276 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs @@ -1,45 +1,52 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; -using System; -using WorkflowCore.Models; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using WorkflowCore.Persistence.SqlServer; namespace WorkflowCore.Persistence.SqlServer.Migrations { - [DbContext(typeof(SqlServerPersistenceProvider))] + [DbContext(typeof(SqlServerContext))] partial class SqlServerPersistenceProviderModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.1-rtm-125") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.8") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("EventData"); + b.Property("EventData") + .HasColumnType("nvarchar(max)"); - b.Property("EventId"); + b.Property("EventId") + .HasColumnType("uniqueidentifier"); b.Property("EventKey") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("EventName") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); - b.Property("EventTime"); + b.Property("EventTime") + .HasColumnType("datetime2"); - b.Property("IsProcessed"); + b.Property("IsProcessed") + .HasColumnType("bit"); b.HasKey("PersistenceId"); @@ -52,129 +59,223 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EventName", "EventKey"); - b.ToTable("Event","wfc"); + b.ToTable("Event", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("ErrorTime"); + b.Property("ErrorTime") + .HasColumnType("datetime2"); b.Property("ExecutionPointerId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); - b.Property("Message"); + b.Property("Message") + .HasColumnType("nvarchar(max)"); b.Property("WorkflowId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.HasKey("PersistenceId"); - b.ToTable("ExecutionError","wfc"); + b.ToTable("ExecutionError", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("Active"); + b.Property("Active") + .HasColumnType("bit"); - b.Property("Children"); + b.Property("Children") + .HasColumnType("nvarchar(max)"); - b.Property("ContextItem"); + b.Property("ContextItem") + .HasColumnType("nvarchar(max)"); - b.Property("EndTime"); + b.Property("EndTime") + .HasColumnType("datetime2"); - b.Property("EventData"); + b.Property("EventData") + .HasColumnType("nvarchar(max)"); b.Property("EventKey") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("EventName") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); - b.Property("EventPublished"); + b.Property("EventPublished") + .HasColumnType("bit"); b.Property("Id") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); - b.Property("Outcome"); + b.Property("Outcome") + .HasColumnType("nvarchar(max)"); - b.Property("PersistenceData"); + b.Property("PersistenceData") + .HasColumnType("nvarchar(max)"); b.Property("PredecessorId") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); - b.Property("RetryCount"); + b.Property("RetryCount") + .HasColumnType("int"); - b.Property("Scope"); + b.Property("Scope") + .HasColumnType("nvarchar(max)"); - b.Property("SleepUntil"); + b.Property("SleepUntil") + .HasColumnType("datetime2"); - b.Property("StartTime"); + b.Property("StartTime") + .HasColumnType("datetime2"); - b.Property("Status"); + b.Property("Status") + .HasColumnType("int"); - b.Property("StepId"); + b.Property("StepId") + .HasColumnType("int"); b.Property("StepName") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); - b.Property("WorkflowId"); + b.Property("WorkflowId") + .HasColumnType("bigint"); b.HasKey("PersistenceId"); b.HasIndex("WorkflowId"); - b.ToTable("ExecutionPointer","wfc"); + b.ToTable("ExecutionPointer", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("AttributeKey") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); - b.Property("AttributeValue"); + b.Property("AttributeValue") + .HasColumnType("nvarchar(max)"); - b.Property("ExecutionPointerId"); + b.Property("ExecutionPointerId") + .HasColumnType("bigint"); b.HasKey("PersistenceId"); b.HasIndex("ExecutionPointerId"); - b.ToTable("ExtensionAttribute","wfc"); + b.ToTable("ExtensionAttribute", "wfc"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ExecuteTime") + .HasColumnType("bigint"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique() + .HasFilter("[CommandName] IS NOT NULL AND [Data] IS NOT NULL"); + + b.ToTable("ScheduledCommand", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("EventKey") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("EventName") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); - b.Property("StepId"); + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("datetime2"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); - b.Property("SubscribeAsOf"); + b.Property("StepId") + .HasColumnType("int"); + + b.Property("SubscribeAsOf") + .HasColumnType("datetime2"); + + b.Property("SubscriptionData") + .HasColumnType("nvarchar(max)"); b.Property("SubscriptionId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); b.Property("WorkflowId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.HasKey("PersistenceId"); @@ -185,38 +286,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("SubscriptionId") .IsUnique(); - b.ToTable("Subscription","wfc"); + b.ToTable("Subscription", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:IdentityIncrement", 1) + .HasAnnotation("SqlServer:IdentitySeed", 1) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - b.Property("CompleteTime"); + b.Property("CompleteTime") + .HasColumnType("datetime2"); - b.Property("CreateTime"); + b.Property("CreateTime") + .HasColumnType("datetime2"); - b.Property("Data"); + b.Property("Data") + .HasColumnType("nvarchar(max)"); b.Property("Description") - .HasMaxLength(500); + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); b.Property("InstanceId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); - b.Property("NextExecution"); + b.Property("NextExecution") + .HasColumnType("bigint"); b.Property("Reference") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); - b.Property("Status"); + b.Property("Status") + .HasColumnType("int"); - b.Property("Version"); + b.Property("Version") + .HasColumnType("int"); b.Property("WorkflowDefinitionId") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.HasKey("PersistenceId"); @@ -225,7 +339,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("NextExecution"); - b.ToTable("Workflow","wfc"); + b.ToTable("Workflow", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => @@ -233,7 +347,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", "Workflow") .WithMany("ExecutionPointers") .HasForeignKey("WorkflowId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Workflow"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -241,7 +358,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", "ExecutionPointer") .WithMany("ExtensionAttributes") .HasForeignKey("ExecutionPointerId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExecutionPointer"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Navigation("ExtensionAttributes"); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Navigation("ExecutionPointers"); }); #pragma warning restore 612, 618 } diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.SqlServer/Properties/AssemblyInfo.cs index afdd6ce15..26131f807 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/README.md b/src/providers/WorkflowCore.Persistence.SqlServer/README.md index 77c4ce347..a113eb894 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/README.md +++ b/src/providers/WorkflowCore.Persistence.SqlServer/README.md @@ -1,6 +1,6 @@ # SQL Server Persistence provider for Workflow Core -Provides support to persist workflows running on [Workflow Core](../../README.md) to a SQL Server database. +Provides support to persist workflows running on [Workflow Core](../../../README.md) to a SQL Server database. ## Installing diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs index e90451e25..0a54b1ea2 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs @@ -1,17 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Data.Common; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.SqlServer; namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static WorkflowOptions UseSqlServer(this WorkflowOptions options, string connectionString, bool canCreateDB, bool canMigrateDB) + public static WorkflowOptions UseSqlServer(this WorkflowOptions options, string connectionString, bool canCreateDB, bool canMigrateDB, Action initAction = null) { - options.UsePersistence(sp => new SqlServerPersistenceProvider(connectionString, canCreateDB, canMigrateDB)); + options.UsePersistence(sp => new EntityFrameworkPersistenceProvider(new SqlContextFactory(connectionString, initAction), canCreateDB, canMigrateDB)); + options.Services.AddTransient(sp => new WorkflowPurger(new SqlContextFactory(connectionString, initAction))); return options; } } diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs b/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs new file mode 100644 index 000000000..066aae73f --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs @@ -0,0 +1,28 @@ +using System; +using System.Data.Common; +using Microsoft.EntityFrameworkCore; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.SqlServer +{ + public class SqlContextFactory : IWorkflowDbContextFactory + { + private readonly string _connectionString; + private readonly Action _initAction; + + public SqlContextFactory(string connectionString, Action initAction = null) + { + _connectionString = connectionString; + _initAction = initAction; + } + + public WorkflowDbContext Build() + { + var result = new SqlServerContext(_connectionString); + _initAction?.Invoke(result.Database.GetDbConnection()); + + return result; + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerPersistenceprovider.cs b/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.cs similarity index 65% rename from src/providers/WorkflowCore.Persistence.SqlServer/SqlServerPersistenceprovider.cs rename to src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.cs index fde4f28e5..eb03f3647 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerPersistenceprovider.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using WorkflowCore.Persistence.EntityFramework.Models; @@ -9,19 +7,16 @@ namespace WorkflowCore.Persistence.SqlServer { - public class SqlServerPersistenceProvider : EntityFrameworkPersistenceProvider + public class SqlServerContext : WorkflowDbContext { private readonly string _connectionString; - public SqlServerPersistenceProvider(string connectionString, bool canCreateDB, bool canMigrateDB) - : base(canCreateDB, canMigrateDB) + public SqlServerContext(string connectionString) + : base() { - if (!connectionString.Contains("MultipleActiveResultSets")) - connectionString += ";MultipleActiveResultSets=True"; - _connectionString = connectionString; } - + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); @@ -31,37 +26,43 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void ConfigureSubscriptionStorage(EntityTypeBuilder builder) { builder.ToTable("Subscription", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } protected override void ConfigureWorkflowStorage(EntityTypeBuilder builder) { builder.ToTable("Workflow", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } protected override void ConfigureExecutionPointerStorage(EntityTypeBuilder builder) { builder.ToTable("ExecutionPointer", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } protected override void ConfigureExecutionErrorStorage(EntityTypeBuilder builder) { builder.ToTable("ExecutionError", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } protected override void ConfigureExetensionAttributeStorage(EntityTypeBuilder builder) { builder.ToTable("ExtensionAttribute", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } protected override void ConfigureEventStorage(EntityTypeBuilder builder) { builder.ToTable("Event", "wfc"); - builder.Property(x => x.PersistenceId).UseSqlServerIdentityColumn(); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); + } + + protected override void ConfigureScheduledCommandStorage(EntityTypeBuilder builder) + { + builder.ToTable("ScheduledCommand", "wfc"); + builder.Property(x => x.PersistenceId).UseIdentityColumn(); } } } diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj b/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj index 717973bbf..b6db766bd 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj +++ b/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj @@ -2,9 +2,9 @@ Workflow Core SQL Server Persistence Provider - 1.5.0 + 1.8.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0 WorkflowCore.Persistence.SqlServer WorkflowCore.Persistence.SqlServer workflow;.NET;Core;state machine;WorkflowCore @@ -15,10 +15,7 @@ false false false - 1.6.0 Provides support to persist workflows running on Workflow Core to a SQL Server database. - 1.6.0.0 - 1.6.0.0 @@ -26,12 +23,26 @@ - - - + + + All - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.Sqlite/Properties/AssemblyInfo.cs index 02fd59231..04f04aacc 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.cs index 476ab71c1..0d5dd5a8e 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.Sqlite; namespace Microsoft.Extensions.DependencyInjection @@ -11,7 +11,8 @@ public static class ServiceCollectionExtensions { public static WorkflowOptions UseSqlite(this WorkflowOptions options, string connectionString, bool canCreateDB) { - options.UsePersistence(sp => new SqlitePersistenceProvider(connectionString, canCreateDB)); + options.UsePersistence(sp => new EntityFrameworkPersistenceProvider(new SqliteContextFactory(connectionString), canCreateDB, false)); + options.Services.AddTransient(sp => new WorkflowPurger(new SqliteContextFactory(connectionString))); return options; } } diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/SqlitePersistenceProvider.cs b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs similarity index 84% rename from src/providers/WorkflowCore.Persistence.Sqlite/SqlitePersistenceProvider.cs rename to src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs index 4ad3b2730..0f2083933 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/SqlitePersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs @@ -1,20 +1,18 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Persistence.EntityFramework.Models; using WorkflowCore.Persistence.EntityFramework.Services; namespace WorkflowCore.Persistence.Sqlite { - public class SqlitePersistenceProvider : EntityFrameworkPersistenceProvider + public class SqliteContext : WorkflowDbContext { private readonly string _connectionString; - public SqlitePersistenceProvider(string connectionString, bool canCreateDB) - : base(canCreateDB, false) + public SqliteContext(string connectionString) + : base() { _connectionString = connectionString; } @@ -54,5 +52,10 @@ protected override void ConfigureEventStorage(EntityTypeBuilder { builder.ToTable("Event"); } + + protected override void ConfigureScheduledCommandStorage(EntityTypeBuilder builder) + { + builder.ToTable("ScheduledCommand"); + } } } diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContextFactory.cs b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContextFactory.cs new file mode 100644 index 000000000..9d24d688c --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContextFactory.cs @@ -0,0 +1,21 @@ +using System; +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.Sqlite +{ + public class SqliteContextFactory : IWorkflowDbContextFactory + { + private readonly string _connectionString; + + public SqliteContextFactory(string connectionString) + { + _connectionString = connectionString; + } + + public WorkflowDbContext Build() + { + return new SqliteContext(_connectionString); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj b/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj index 51964723b..88b9ceec9 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj +++ b/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj @@ -4,7 +4,7 @@ Workflow Core Sqlite Persistence Provider 1.5.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0 WorkflowCore.Persistence.Sqlite WorkflowCore.Persistence.Sqlite workflow;.NET;Core;state machine;WorkflowCore;Sqlite @@ -16,9 +16,6 @@ false false Provides support to persist workflows running on Workflow Core to a Sqlite database. - 1.6.0 - 1.6.0.0 - 1.6.0.0 @@ -26,8 +23,12 @@ - - + + + + + + diff --git a/src/providers/WorkflowCore.Providers.AWS/Interface/IDynamoDbProvisioner.cs b/src/providers/WorkflowCore.Providers.AWS/Interface/IDynamoDbProvisioner.cs new file mode 100644 index 000000000..0ec886ce3 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Interface/IDynamoDbProvisioner.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace WorkflowCore.Providers.AWS.Interface +{ + public interface IDynamoDbProvisioner + { + Task ProvisionTables(); + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs new file mode 100644 index 000000000..d4d66614f --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Amazon.Kinesis.Model; + +namespace WorkflowCore.Providers.AWS.Interface +{ + public interface IKinesisStreamConsumer + { + Task Subscribe(string appName, string stream, Action action); + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs new file mode 100644 index 000000000..f3cb37510 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace WorkflowCore.Providers.AWS.Interface +{ + public interface IKinesisTracker + { + Task GetNextShardIterator(string app, string stream, string shard); + Task GetNextLastSequenceNumber(string app, string stream, string shard); + Task IncrementShardIterator(string app, string stream, string shard, string iterator); + Task IncrementShardIteratorAndSequence(string app, string stream, string shard, string iterator, string sequence); + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs b/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs new file mode 100644 index 000000000..daf5b6889 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs @@ -0,0 +1,165 @@ +using Amazon.DynamoDBv2.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using WorkflowCore.Models; + +namespace WorkflowCore.Providers.AWS +{ + internal static class ModelExtensions + { + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + + public static Dictionary ToDynamoMap(this WorkflowInstance source) + { + var result = new Dictionary(); + + result["id"] = new AttributeValue(source.Id); + result["workflow_definition_id"] = new AttributeValue(source.WorkflowDefinitionId); + result["version"] = new AttributeValue(source.Version.ToString()); + result["next_execution"] = new AttributeValue { N = (source.NextExecution ?? 0).ToString() }; + result["create_time"] = new AttributeValue { N = source.CreateTime.Ticks.ToString() }; + result["data"] = new AttributeValue(JsonConvert.SerializeObject(source.Data, SerializerSettings)); + result["workflow_status"] = new AttributeValue { N = Convert.ToInt32(source.Status).ToString() }; + + if (!string.IsNullOrEmpty(source.Description)) + result["description"] = new AttributeValue(source.Description); + + if (!string.IsNullOrEmpty(source.Reference)) + result["reference"] = new AttributeValue(source.Reference); + + if (source.CompleteTime.HasValue) + result["complete_time"] = new AttributeValue { N = source.CompleteTime.Value.Ticks.ToString() }; + + var pointers = new List(); + foreach (var pointer in source.ExecutionPointers) + { + pointers.Add(new AttributeValue(JsonConvert.SerializeObject(pointer, SerializerSettings))); + } + + result["pointers"] = new AttributeValue { L = pointers }; + + if (source.Status == WorkflowStatus.Runnable) + result["runnable"] = new AttributeValue { N = 1.ToString() }; + + return result; + } + + public static WorkflowInstance ToWorkflowInstance(this Dictionary source) + { + var result = new WorkflowInstance + { + Id = source["id"].S, + WorkflowDefinitionId = source["workflow_definition_id"].S, + Version = Convert.ToInt32(source["version"].S), + Status = (WorkflowStatus)Convert.ToInt32(source["workflow_status"].N), + NextExecution = Convert.ToInt64(source["next_execution"].N), + CreateTime = new DateTime(Convert.ToInt64(source["create_time"].N)), + Data = JsonConvert.DeserializeObject(source["data"].S, SerializerSettings) + }; + + if (source.ContainsKey("description")) + result.Description = source["description"].S; + + if (source.ContainsKey("reference")) + result.Reference = source["reference"].S; + + if (source.ContainsKey("complete_time")) + result.CompleteTime = new DateTime(Int64.Parse(source["complete_time"].N)); + + foreach (var pointer in source["pointers"].L) + { + var ep = JsonConvert.DeserializeObject(pointer.S, SerializerSettings); + result.ExecutionPointers.Add(ep); + } + + return result; + } + + public static Dictionary ToDynamoMap(this EventSubscription source) + { + var result = new Dictionary + { + ["id"] = new AttributeValue(source.Id), + ["event_name"] = new AttributeValue(source.EventName), + ["event_key"] = new AttributeValue(source.EventKey), + ["workflow_id"] = new AttributeValue(source.WorkflowId), + ["execution_pointer_id"] = new AttributeValue(source.ExecutionPointerId), + ["step_id"] = new AttributeValue(source.StepId.ToString()), + ["subscribe_as_of"] = new AttributeValue { N = source.SubscribeAsOf.Ticks.ToString() }, + ["subscription_data"] = new AttributeValue(JsonConvert.SerializeObject(source.SubscriptionData, SerializerSettings)), + ["event_slug"] = new AttributeValue($"{source.EventName}:{source.EventKey}") + }; + if (!string.IsNullOrEmpty(source.ExternalToken)) + result["external_token"] = new AttributeValue(source.ExternalToken); + + if (!string.IsNullOrEmpty(source.ExternalWorkerId)) + result["external_worker_id"] = new AttributeValue(source.ExternalWorkerId); + + if (source.ExternalTokenExpiry.HasValue) + result["external_token_expiry"] = new AttributeValue { N = source.ExternalTokenExpiry.Value.Ticks.ToString()}; + + return result; + } + + public static EventSubscription ToEventSubscription(this Dictionary source) + { + var result = new EventSubscription + { + Id = source["id"].S, + EventName = source["event_name"].S, + EventKey = source["event_key"].S, + WorkflowId = source["workflow_id"].S, + ExecutionPointerId = source["execution_pointer_id"].S, + StepId = Convert.ToInt32(source["step_id"].S), + SubscribeAsOf = new DateTime(Convert.ToInt64(source["subscribe_as_of"].N)), + SubscriptionData = JsonConvert.DeserializeObject(source["subscription_data"].S, SerializerSettings), + }; + + if (source.ContainsKey("external_token")) + result.ExternalToken = source["external_token"].S; + + if (source.ContainsKey("external_worker_id")) + result.ExternalWorkerId = source["external_worker_id"].S; + + if (source.ContainsKey("external_token_expiry")) + result.ExternalTokenExpiry = new DateTime(Int64.Parse(source["external_token_expiry"].N)); + + return result; + } + + public static Dictionary ToDynamoMap(this Event source) + { + var result = new Dictionary + { + ["id"] = new AttributeValue(source.Id), + ["event_name"] = new AttributeValue(source.EventName), + ["event_key"] = new AttributeValue(source.EventKey), + ["event_data"] = new AttributeValue(JsonConvert.SerializeObject(source.EventData, SerializerSettings)), + ["event_time"] = new AttributeValue { N = source.EventTime.Ticks.ToString() }, + ["event_slug"] = new AttributeValue($"{source.EventName}:{source.EventKey}") + }; + + if (!source.IsProcessed) + result["not_processed"] = new AttributeValue { N = 1.ToString() }; + + return result; + } + + public static Event ToEvent(this Dictionary source) + { + var result = new Event + { + Id = source["id"].S, + EventName = source["event_name"].S, + EventKey = source["event_key"].S, + EventData = JsonConvert.DeserializeObject(source["event_data"].S, SerializerSettings), + EventTime = new DateTime(Convert.ToInt64(source["event_time"].N)), + IsProcessed = (!source.ContainsKey("not_processed")) + }; + + return result; + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.AWS/README.md b/src/providers/WorkflowCore.Providers.AWS/README.md new file mode 100644 index 000000000..0a9286f37 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/README.md @@ -0,0 +1,61 @@ +# AWS providers for Workflow Core + +* Provides persistence for [Workflow Core](../../README.md) using DynamoDB. +* Provides Queueing support on [Workflow Core](../../README.md) using AWS Simple Queue Service. +* Provides Distributed locking support on [Workflow Core](../../README.md) using DynamoDB. +* Provides event hub support on [Workflow Core](../../README.md) backed by AWS Kinesis. + +This makes it possible to have a cluster of nodes processing your workflows. + +## Installing + +Install the NuGet package "WorkflowCore.Providers.AWS" + +``` +PM> Install-Package WorkflowCore.Providers.AWS +``` + +## Usage (Persistence, Queueing and distributed locking) + +Use the `IServiceCollection` extension methods when building your service provider +* .UseAwsDynamoPersistence +* .UseAwsSimpleQueueService +* .UseAwsDynamoLocking + +```C# +services.AddWorkflow(cfg => +{ + cfg.UseAwsDynamoPersistence(new EnvironmentVariablesAWSCredentials(), new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }, "table-prefix"); + cfg.UseAwsDynamoLocking(new EnvironmentVariablesAWSCredentials(), new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }, "workflow-core-locks"); + cfg.UseAwsSimpleQueueService(new EnvironmentVariablesAWSCredentials(), new AmazonSQSConfig() { RegionEndpoint = RegionEndpoint.USWest2 }, "queues-prefix"); +}); +``` + +If any AWS resources do not exists, they will be automatcially created. By default, all DynamoDB tables and indexes will be provisioned with a throughput of 1, you can modify these values from the AWS console. +You may also specify a prefix for the dynamo table names. + +If you have a preconfigured dynamoClient, you can pass this in instead of the credentials and config +```C# +var client = new AmazonDynamoDBClient(); +var sqsClient = new AmazonSQSClient(); +services.AddWorkflow(cfg => +{ + cfg.UseAwsDynamoPersistenceWithProvisionedClient(client, "table-prefix"); + cfg.UseAwsDynamoLockingWithProvisionedClient(client, "workflow-core-locks"); + cfg.UseAwsSimpleQueueServiceWithProvisionedClient(sqsClient, "queues-prefix"); +}); +``` + + +## Usage (Kinesis) + +Use the the `.UseAwsKinesis` extension method on `IServiceCollection` when building your service provider + +```C# +services.AddWorkflow(cfg => +{ + cfg.UseAwsKinesis(new EnvironmentVariablesAWSCredentials(), RegionEndpoint.USWest2, "app-name", "stream-name"); +}); +``` +The Kinesis provider will also create a DynamoDB table to track the postion in each shard of the Kinesis stream. +A shard position will be tracked for each app name that you connect with. \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..c3c545f80 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs @@ -0,0 +1,71 @@ +using System; +using Amazon; +using Amazon.DynamoDBv2; +using Amazon.Kinesis; +using Amazon.Runtime; +using Amazon.SQS; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Providers.AWS.Interface; +using WorkflowCore.Providers.AWS.Services; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseAwsSimpleQueueService(this WorkflowOptions options, AWSCredentials credentials, AmazonSQSConfig config, string queuesPrefix = "workflowcore") + { + var sqsClient = new AmazonSQSClient(credentials, config); + return options.UseAwsSimpleQueueServiceWithProvisionedClient(sqsClient, queuesPrefix); + } + + public static WorkflowOptions UseAwsSimpleQueueServiceWithProvisionedClient(this WorkflowOptions options, AmazonSQSClient sqsClient, string queuesPrefix = "workflowcore") + { + options.UseQueueProvider(sp => new SQSQueueProvider(sqsClient, sp.GetService(), queuesPrefix)); + return options; + } + + public static WorkflowOptions UseAwsDynamoLocking(this WorkflowOptions options, AWSCredentials credentials, AmazonDynamoDBConfig config, string tableName) + { + var dbClient = new AmazonDynamoDBClient(credentials, config); + return options.UseAwsDynamoLockingWithProvisionedClient(dbClient, tableName); + } + + public static WorkflowOptions UseAwsDynamoLockingWithProvisionedClient (this WorkflowOptions options, AmazonDynamoDBClient dynamoClient, string tableName) + { + options.UseDistributedLockManager(sp => new DynamoLockProvider(dynamoClient, tableName, sp.GetService(), sp.GetService())); + return options; + } + + public static WorkflowOptions UseAwsDynamoPersistence(this WorkflowOptions options, AWSCredentials credentials, AmazonDynamoDBConfig config, string tablePrefix) + { + var dbClient = new AmazonDynamoDBClient(credentials, config); + return options.UseAwsDynamoPersistenceWithProvisionedClient(dbClient, tablePrefix); + } + + public static WorkflowOptions UseAwsDynamoPersistenceWithProvisionedClient(this WorkflowOptions options, AmazonDynamoDBClient dynamoClient, string tablePrefix) + { + options.Services.AddTransient(sp => new DynamoDbProvisioner(dynamoClient, tablePrefix, sp.GetService())); + options.UsePersistence(sp => new DynamoPersistenceProvider(dynamoClient, sp.GetService(), tablePrefix, sp.GetService())); + return options; + } + + public static WorkflowOptions UseAwsKinesis(this WorkflowOptions options, AWSCredentials credentials, RegionEndpoint region, string appName, string streamName) + { + var kinesisClient = new AmazonKinesisClient(credentials, region); + var dynamoClient = new AmazonDynamoDBClient(credentials, region); + + return options.UseAwsKinesisWithProvisionedClients(kinesisClient, dynamoClient,appName, streamName); + + } + + public static WorkflowOptions UseAwsKinesisWithProvisionedClients(this WorkflowOptions options, AmazonKinesisClient kinesisClient, AmazonDynamoDBClient dynamoDbClient, string appName, string streamName) + { + options.Services.AddTransient(sp => new KinesisTracker(dynamoDbClient, "workflowcore_kinesis", sp.GetService())); + options.Services.AddTransient(sp => new KinesisStreamConsumer(kinesisClient, sp.GetService(), sp.GetService(), sp.GetService(), sp.GetService())); + options.UseEventHub(sp => new KinesisProvider(kinesisClient, appName, streamName, sp.GetService(), sp.GetService())); + return options; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoDbProvisioner.cs b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoDbProvisioner.cs new file mode 100644 index 000000000..887d11a7a --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoDbProvisioner.cs @@ -0,0 +1,275 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using WorkflowCore.Providers.AWS.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class DynamoDbProvisioner : IDynamoDbProvisioner + { + private readonly ILogger _logger; + private readonly IAmazonDynamoDB _client; + private readonly string _tablePrefix; + + public DynamoDbProvisioner(AmazonDynamoDBClient dynamoDBClient, string tablePrefix, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(); + _client = dynamoDBClient; + _tablePrefix = tablePrefix; + } + + public Task ProvisionTables() + { + return Task.WhenAll( + EnsureTable($"{_tablePrefix}-{DynamoPersistenceProvider.WORKFLOW_TABLE}", CreateWorkflowTable), + EnsureTable($"{_tablePrefix}-{DynamoPersistenceProvider.SUBCRIPTION_TABLE}", CreateSubscriptionTable), + EnsureTable($"{_tablePrefix}-{DynamoPersistenceProvider.EVENT_TABLE}", CreateEventTable)); + } + + private async Task CreateWorkflowTable() + { + var runnableIndex = new GlobalSecondaryIndex + { + IndexName = "ix_runnable", + KeySchema = new List + { + { + new KeySchemaElement + { + AttributeName= "runnable", + KeyType = "HASH" //Partition key + } + }, + { + new KeySchemaElement + { + AttributeName = "next_execution", + KeyType = "RANGE" //Sort key + } + } + }, + Projection = new Projection + { + ProjectionType = ProjectionType.KEYS_ONLY + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.WORKFLOW_TABLE}", new List + { + new KeySchemaElement("id", KeyType.HASH) + }) + { + AttributeDefinitions = new List + { + new AttributeDefinition("id", ScalarAttributeType.S), + new AttributeDefinition("runnable", ScalarAttributeType.N), + new AttributeDefinition("next_execution", ScalarAttributeType.N), + }, + GlobalSecondaryIndexes = new List + { + runnableIndex + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + await CreateTable(createRequest); + } + + private async Task CreateSubscriptionTable() + { + var slugIndex = new GlobalSecondaryIndex + { + IndexName = "ix_slug", + KeySchema = new List + { + { + new KeySchemaElement + { + AttributeName = "event_slug", + KeyType = "HASH" //Partition key + } + }, + { + new KeySchemaElement + { + AttributeName = "subscribe_as_of", + KeyType = "RANGE" //Sort key + } + } + }, + Projection = new Projection + { + ProjectionType = ProjectionType.ALL + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.SUBCRIPTION_TABLE}", new List + { + new KeySchemaElement("id", KeyType.HASH) + }) + { + AttributeDefinitions = new List + { + new AttributeDefinition("id", ScalarAttributeType.S), + new AttributeDefinition("event_slug", ScalarAttributeType.S), + new AttributeDefinition("subscribe_as_of", ScalarAttributeType.N) + }, + GlobalSecondaryIndexes = new List + { + slugIndex + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + await CreateTable(createRequest); + } + + private async Task CreateEventTable() + { + var slugIndex = new GlobalSecondaryIndex + { + IndexName = "ix_slug", + KeySchema = new List + { + { + new KeySchemaElement + { + AttributeName= "event_slug", + KeyType = "HASH" //Partition key + } + }, + { + new KeySchemaElement + { + AttributeName = "event_time", + KeyType = "RANGE" //Sort key + } + } + }, + Projection = new Projection + { + ProjectionType = ProjectionType.KEYS_ONLY + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + var processedIndex = new GlobalSecondaryIndex + { + IndexName = "ix_not_processed", + KeySchema = new List + { + { + new KeySchemaElement + { + AttributeName = "not_processed", + KeyType = "HASH" //Partition key + } + }, + { + new KeySchemaElement + { + AttributeName = "event_time", + KeyType = "RANGE" //Sort key + } + } + }, + Projection = new Projection + { + ProjectionType = ProjectionType.KEYS_ONLY + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.EVENT_TABLE}", new List + { + new KeySchemaElement("id", KeyType.HASH) + }) + { + AttributeDefinitions = new List + { + new AttributeDefinition("id", ScalarAttributeType.S), + new AttributeDefinition("not_processed", ScalarAttributeType.N), + new AttributeDefinition("event_slug", ScalarAttributeType.S), + new AttributeDefinition("event_time", ScalarAttributeType.N) + }, + GlobalSecondaryIndexes = new List + { + slugIndex, + processedIndex + }, + ProvisionedThroughput = new ProvisionedThroughput + { + ReadCapacityUnits = 1, + WriteCapacityUnits = 1 + } + }; + + await CreateTable(createRequest); + } + + private async Task EnsureTable(string tableName, Func createTask) + { + try + { + await _client.DescribeTableAsync(tableName); + } + catch (ResourceNotFoundException) + { + _logger.LogWarning($"Provisioning DynamoDb table - {tableName}"); + await createTask(); + } + } + + private async Task CreateTable(CreateTableRequest createRequest) + { + var createResponse = await _client.CreateTableAsync(createRequest); + + int i = 0; + bool created = false; + while ((i < 30) && (!created)) + { + try + { + await Task.Delay(2000); + var poll = await _client.DescribeTableAsync(createRequest.TableName); + created = (poll.Table.TableStatus == TableStatus.ACTIVE); + i++; + } + catch (ResourceNotFoundException) + { + } + } + } + } +} + diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs new file mode 100644 index 000000000..0863f1393 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs @@ -0,0 +1,238 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WorkflowCore.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class DynamoLockProvider : IDistributedLockProvider + { + private readonly ILogger _logger; + private readonly IAmazonDynamoDB _client; + private readonly string _tableName; + private readonly string _nodeId; + private readonly long _ttl = 30000; + private readonly int _heartbeat = 10000; + private readonly long _jitter = 1000; + private readonly List _localLocks; + private Task _heartbeatTask; + private CancellationTokenSource _cancellationTokenSource; + private readonly AutoResetEvent _mutex = new AutoResetEvent(true); + private readonly IDateTimeProvider _dateTimeProvider; + + public DynamoLockProvider(AmazonDynamoDBClient dynamoDBClient, string tableName, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) + { + _logger = logFactory.CreateLogger(); + _client = dynamoDBClient; + _localLocks = new List(); + _tableName = tableName; + _nodeId = Guid.NewGuid().ToString(); + _dateTimeProvider = dateTimeProvider; + } + + public async Task AcquireLock(string Id, CancellationToken cancellationToken) + { + try + { + var req = new PutItemRequest + { + TableName = _tableName, + Item = new Dictionary + { + { "id", new AttributeValue(Id) }, + { "lock_owner", new AttributeValue(_nodeId) }, + { "expires", new AttributeValue + { + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _ttl) + } + } + }, + ConditionExpression = "attribute_not_exists(id) OR (expires < :expired)", + ExpressionAttributeValues = new Dictionary + { + { ":expired", new AttributeValue + { + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _jitter) + } + } + } + }; + + var response = await _client.PutItemAsync(req, _cancellationTokenSource.Token); + + if (response.HttpStatusCode == System.Net.HttpStatusCode.OK) + { + _localLocks.Add(Id); + return true; + } + } + catch (ConditionalCheckFailedException) + { + } + return false; + } + + public async Task ReleaseLock(string Id) + { + _mutex.WaitOne(); + try + { + _localLocks.Remove(Id); + } + finally + { + _mutex.Set(); + } + + try + { + var req = new DeleteItemRequest + { + TableName = _tableName, + Key = new Dictionary + { + { "id", new AttributeValue(Id) } + }, + ConditionExpression = "lock_owner = :nodeId", + ExpressionAttributeValues = new Dictionary + { + { ":nodeId", new AttributeValue(_nodeId) } + } + + }; + await _client.DeleteItemAsync(req); + } + catch (ConditionalCheckFailedException) + { + } + } + + public async Task Start() + { + await EnsureTable(); + if (_heartbeatTask != null) + { + throw new InvalidOperationException(); + } + + _cancellationTokenSource = new CancellationTokenSource(); + + _heartbeatTask = new Task(SendHeartbeat); + _heartbeatTask.Start(); + } + + public Task Stop() + { + _cancellationTokenSource.Cancel(); + _heartbeatTask.Wait(); + _heartbeatTask = null; + return Task.CompletedTask; + } + + private async void SendHeartbeat() + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + try + { + await Task.Delay(_heartbeat, _cancellationTokenSource.Token); + if (_mutex.WaitOne()) + { + try + { + foreach (var item in _localLocks.ToArray()) + { + var req = new PutItemRequest + { + TableName = _tableName, + Item = new Dictionary + { + { "id", new AttributeValue(item) }, + { "lock_owner", new AttributeValue(_nodeId) }, + { "expires", new AttributeValue + { + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _ttl) + } + } + }, + ConditionExpression = "lock_owner = :nodeId", + ExpressionAttributeValues = new Dictionary + { + { ":nodeId", new AttributeValue(_nodeId) } + } + }; + + try + { + await _client.PutItemAsync(req, _cancellationTokenSource.Token); + } + catch (ConditionalCheckFailedException) + { + _logger.LogWarning($"Lock not owned anymore when sending heartbeat for {item}"); + } + } + } + finally + { + _mutex.Set(); + } + } + } + catch (Exception ex) + { + _logger.LogError(default(EventId), ex, ex.Message); + } + } + } + + private async Task EnsureTable() + { + try + { + var poll = await _client.DescribeTableAsync(_tableName); + } + catch (ResourceNotFoundException) + { + await CreateTable(); + } + } + + private async Task CreateTable() + { + var createRequest = new CreateTableRequest(_tableName, new List + { + new KeySchemaElement("id", KeyType.HASH) + }) + { + AttributeDefinitions = new List + { + new AttributeDefinition("id", ScalarAttributeType.S) + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }; + + var createResponse = await _client.CreateTableAsync(createRequest); + + int i = 0; + bool created = false; + while ((i < 20) && (!created)) + { + try + { + await Task.Delay(1000); + var poll = await _client.DescribeTableAsync(_tableName); + created = (poll.Table.TableStatus == TableStatus.ACTIVE); + i++; + } + catch (ResourceNotFoundException) + { + } + } + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs new file mode 100644 index 000000000..01beaaabe --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs @@ -0,0 +1,522 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Providers.AWS.Interface; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using System.Threading; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class DynamoPersistenceProvider : IPersistenceProvider + { + private readonly ILogger _logger; + private readonly IAmazonDynamoDB _client; + private readonly string _tablePrefix; + private readonly IDynamoDbProvisioner _provisioner; + + public const string WORKFLOW_TABLE = "workflows"; + public const string SUBCRIPTION_TABLE = "subscriptions"; + public const string EVENT_TABLE = "events"; + + public bool SupportsScheduledCommands => false; + + public DynamoPersistenceProvider(AmazonDynamoDBClient dynamoDBClient, IDynamoDbProvisioner provisioner, string tablePrefix, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(); + _client = dynamoDBClient; + _tablePrefix = tablePrefix; + _provisioner = provisioner; + } + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) + { + workflow.Id = Guid.NewGuid().ToString(); + + var req = new PutItemRequest + { + TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", + Item = workflow.ToDynamoMap(), + ConditionExpression = "attribute_not_exists(id)" + }; + + var _ = await _client.PutItemAsync(req, cancellationToken); + + return workflow.Id; + } + + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) + { + var request = new PutItemRequest + { + TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", + Item = workflow.ToDynamoMap() + }; + + var response = await _client.PutItemAsync(request, cancellationToken); + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + var transactionWriteItemsRequest = new TransactWriteItemsRequest() + { + TransactItems = new List() + { + { + new TransactWriteItem() + { + Put = new Put() + { + TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", + Item = workflow.ToDynamoMap() + } + } + } + } + }; + + foreach(var subscription in subscriptions) + { + subscription.Id = Guid.NewGuid().ToString(); + + transactionWriteItemsRequest.TransactItems.Add(new TransactWriteItem() + { + Put = new Put() + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Item = subscription.ToDynamoMap(), + ConditionExpression = "attribute_not_exists(id)" + } + }); + } + + await _client.TransactWriteItemsAsync(transactionWriteItemsRequest, cancellationToken); + } + + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) + { + var result = new List(); + var now = asAt.ToUniversalTime().Ticks; + + var request = new QueryRequest + { + TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", + IndexName = "ix_runnable", + ProjectionExpression = "id", + KeyConditionExpression = "runnable = :r and next_execution <= :effective_date", + ExpressionAttributeValues = new Dictionary + { + { + ":r", new AttributeValue + { + N = 1.ToString() + } + }, + { + ":effective_date", new AttributeValue + { + N = Convert.ToString(now) + } + } + }, + ScanIndexForward = true + }; + + var response = await _client.QueryAsync(request, cancellationToken); + + foreach (var item in response.Items) + { + result.Add(item["id"].S); + } + + return result; + } + + public Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) + { + throw new NotImplementedException(); + } + + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) + { + var req = new GetItemRequest + { + TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(Id) } + } + }; + var response = await _client.GetItemAsync(req, cancellationToken); + + return response.Item.ToWorkflowInstance(); + } + + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) + { + if (ids == null) + { + return new List(); + } + + var keys = new KeysAndAttributes { Keys = new List>() }; + foreach (var id in ids) + { + var key = new Dictionary + { + { + "id", new AttributeValue { S = id } + } + }; + keys.Keys.Add(key); + } + + var request = new BatchGetItemRequest + { + RequestItems = new Dictionary + { + { + $"{_tablePrefix}-{WORKFLOW_TABLE}", keys + } + } + }; + + var result = new List>(); + BatchGetItemResponse response; + do + { + response = await _client.BatchGetItemAsync(request, cancellationToken); + foreach (var tableResponse in response.Responses) + result.AddRange(tableResponse.Value); + + request.RequestItems = response.UnprocessedKeys; + } while (response.UnprocessedKeys.Count > 0); + + return result.Select(i => i.ToWorkflowInstance()); + } + + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) + { + subscription.Id = Guid.NewGuid().ToString(); + + var req = new PutItemRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Item = subscription.ToDynamoMap(), + ConditionExpression = "attribute_not_exists(id)" + }; + + var response = await _client.PutItemAsync(req, cancellationToken); + + return subscription.Id; + } + + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + var result = new List(); + var asOfTicks = asOf.ToUniversalTime().Ticks; + + var request = new QueryRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + IndexName = "ix_slug", + Select = "ALL_PROJECTED_ATTRIBUTES", + KeyConditionExpression = "event_slug = :slug and subscribe_as_of <= :as_of", + ExpressionAttributeValues = new Dictionary + { + { + ":slug", new AttributeValue($"{eventName}:{eventKey}") + }, + { + ":as_of", new AttributeValue + { + N = Convert.ToString(asOfTicks) + } + } + }, + ScanIndexForward = true + }; + + var response = await _client.QueryAsync(request, cancellationToken); + + foreach (var item in response.Items) + { + result.Add(item.ToEventSubscription()); + } + + return result; + } + + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + var request = new DeleteItemRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(eventSubscriptionId) } + } + }; + await _client.DeleteItemAsync(request, cancellationToken); + } + + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) + { + newEvent.Id = Guid.NewGuid().ToString(); + + var req = new PutItemRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + Item = newEvent.ToDynamoMap(), + ConditionExpression = "attribute_not_exists(id)" + }; + + var _ = await _client.PutItemAsync(req, cancellationToken); + + return newEvent.Id; + } + + public async Task GetEvent(string id, CancellationToken cancellationToken = default) + { + var req = new GetItemRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(id) } + } + }; + var response = await _client.GetItemAsync(req, cancellationToken); + + return response.Item.ToEvent(); + } + + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) + { + var result = new List(); + var now = asAt.ToUniversalTime().Ticks; + + var request = new QueryRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + IndexName = "ix_not_processed", + ProjectionExpression = "id", + KeyConditionExpression = "not_processed = :n and event_time <= :effectiveDate", + ExpressionAttributeValues = new Dictionary + { + { ":n" , new AttributeValue { N = 1.ToString() } }, + { + ":effectiveDate", new AttributeValue + { + N = Convert.ToString(now) + } + } + }, + ScanIndexForward = true + }; + + var response = await _client.QueryAsync(request, cancellationToken); + + foreach (var item in response.Items) + { + result.Add(item["id"].S); + } + + return result; + } + + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) + { + var result = new List(); + var asOfTicks = asOf.ToUniversalTime().Ticks; + + var request = new QueryRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + IndexName = "ix_slug", + ProjectionExpression = "id", + KeyConditionExpression = "event_slug = :slug and event_time >= :effective_date", + ExpressionAttributeValues = new Dictionary + { + { + ":slug", new AttributeValue($"{eventName}:{eventKey}") + }, + { + ":effective_date", new AttributeValue + { + N = Convert.ToString(asOfTicks) + } + } + }, + ScanIndexForward = true + }; + + var response = await _client.QueryAsync(request, cancellationToken); + + foreach (var item in response.Items) + { + result.Add(item["id"].S); + } + + return result; + } + + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) + { + var request = new UpdateItemRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(id) } + }, + UpdateExpression = "REMOVE not_processed" + }; + await _client.UpdateItemAsync(request, cancellationToken); + } + + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) + { + var request = new UpdateItemRequest + { + TableName = $"{_tablePrefix}-{EVENT_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(id) } + }, + UpdateExpression = "ADD not_processed = :n", + ExpressionAttributeValues = new Dictionary + { + { ":n" , new AttributeValue { N = 1.ToString() } } + } + }; + await _client.UpdateItemAsync(request, cancellationToken); + } + + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) + { + //TODO + return Task.CompletedTask; + } + + public void EnsureStoreExists() + { + _provisioner.ProvisionTables().Wait(); + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) + { + var req = new GetItemRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(eventSubscriptionId) } + } + }; + var response = await _client.GetItemAsync(req, cancellationToken); + + return response.Item.ToEventSubscription(); + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + var result = new List(); + var asOfTicks = asOf.ToUniversalTime().Ticks; + + var request = new QueryRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + IndexName = "ix_slug", + Select = "ALL_PROJECTED_ATTRIBUTES", + KeyConditionExpression = "event_slug = :slug and subscribe_as_of <= :as_of", + FilterExpression = "attribute_not_exists(external_token)", + Limit = 1, + ExpressionAttributeValues = new Dictionary + { + { + ":slug", new AttributeValue($"{eventName}:{eventKey}") + }, + { + ":as_of", new AttributeValue + { + N = Convert.ToString(asOfTicks) + } + } + }, + ScanIndexForward = true + }; + + var response = await _client.QueryAsync(request, cancellationToken); + + foreach (var item in response.Items) + result.Add(item.ToEventSubscription()); + + return result.FirstOrDefault(); + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) + { + var request = new UpdateItemRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(eventSubscriptionId) } + }, + UpdateExpression = "SET external_token = :external_token, external_worker_id = :external_worker_id, external_token_expiry = :external_token_expiry", + ConditionExpression = "attribute_not_exists(external_token)", + ExpressionAttributeValues = new Dictionary + { + { ":external_token" , new AttributeValue { S = token } }, + { ":external_worker_id" , new AttributeValue { S = workerId } }, + { ":external_token_expiry" , new AttributeValue { N = expiry.Ticks.ToString() } } + } + }; + try + { + await _client.UpdateItemAsync(request, cancellationToken); + return true; + } + catch (ConditionalCheckFailedException) + { + return false; + } + } + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) + { + var request = new UpdateItemRequest + { + TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", + Key = new Dictionary + { + { "id", new AttributeValue(eventSubscriptionId) } + }, + UpdateExpression = "REMOVE external_token, external_worker_id, external_token_expiry", + ConditionExpression = "external_token = :external_token", + ExpressionAttributeValues = new Dictionary + { + { ":external_token" , new AttributeValue { S = token } }, + } + }; + + await _client.UpdateItemAsync(request, cancellationToken); + } + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisProvider.cs new file mode 100644 index 000000000..d8aa519bd --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisProvider.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Amazon; +using Amazon.Kinesis; +using Amazon.Kinesis.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using WorkflowCore.Interface; +using WorkflowCore.Models.LifeCycleEvents; +using WorkflowCore.Providers.AWS.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class KinesisProvider : ILifeCycleEventHub + { + private readonly ILogger _logger; + private Queue> _deferredSubscribers = new Queue>(); + private readonly string _streamName; + private readonly string _appName; + private readonly JsonSerializer _serializer; + private readonly IKinesisStreamConsumer _consumer; + private readonly AmazonKinesisClient _client; + private readonly int _defaultShardCount = 1; + private bool _started = false; + + public KinesisProvider(AmazonKinesisClient kinesisClient, string appName, string streamName, IKinesisStreamConsumer consumer, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(GetType()); + _appName = appName; + _streamName = streamName; + _consumer = consumer; + _serializer = new JsonSerializer(); + _serializer.TypeNameHandling = TypeNameHandling.All; + _client = kinesisClient; + } + + public async Task PublishNotification(LifeCycleEvent evt) + { + using (var stream = new MemoryStream()) + { + var writer = new StreamWriter(stream); + _serializer.Serialize(writer, evt); + writer.Flush(); + + var response = await _client.PutRecordAsync(new PutRecordRequest + { + StreamName = _streamName, + PartitionKey = evt.WorkflowInstanceId, + Data = stream + }); + + //if (response.HttpStatusCode != System.Net.HttpStatusCode.OK) + //{ + // _logger.LogWarning($"Failed to send event to Kinesis {response.HttpStatusCode}"); + //} + } + } + + public void Subscribe(Action action) + { + if (_started) + { + _consumer.Subscribe(_appName, _streamName, record => Consume(record, action)); + } + else + { + _deferredSubscribers.Enqueue(action); + } + } + + public async Task Start() + { + await EnsureStream(); + _started = true; + while (_deferredSubscribers.Count > 0) + { + var action = _deferredSubscribers.Dequeue(); + await _consumer.Subscribe(_appName, _streamName, record => Consume(record, action)); + } + } + + public Task Stop() + { + _started = false; + return Task.CompletedTask; + } + + private async Task EnsureStream() + { + try + { + await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest + { + StreamName = _streamName + }); + } + catch (ResourceNotFoundException) + { + await CreateStream(); + } + } + + private async Task CreateStream() + { + await _client.CreateStreamAsync(new CreateStreamRequest + { + StreamName = _streamName, + ShardCount = _defaultShardCount + }); + + var i = 0; + while (i < 20) + { + i++; + await Task.Delay(3000); + var poll = await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest + { + StreamName = _streamName + }); + + if (poll.StreamDescriptionSummary.StreamStatus == StreamStatus.ACTIVE) + return poll.StreamDescriptionSummary.StreamARN; + } + + throw new TimeoutException(); + } + + private void Consume(Record record, Action action) + { + using (var strm = new StreamReader(record.Data)) + { + var evt = _serializer.Deserialize(new JsonTextReader(strm)); + action(evt as LifeCycleEvent); + } + } + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs new file mode 100644 index 000000000..5c89f7837 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon; +using Amazon.Kinesis; +using Amazon.Kinesis.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Providers.AWS.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class KinesisStreamConsumer : IKinesisStreamConsumer, IDisposable + { + private readonly ILogger _logger; + private readonly IKinesisTracker _tracker; + private readonly IDistributedLockProvider _lockManager; + private readonly AmazonKinesisClient _client; + private readonly CancellationTokenSource _cancelToken = new CancellationTokenSource(); + private readonly Task _processTask; + private readonly int _batchSize = 100; + private ICollection _subscribers = new HashSet(); + private readonly IDateTimeProvider _dateTimeProvider; + + public KinesisStreamConsumer(AmazonKinesisClient kinesisClient, IKinesisTracker tracker, IDistributedLockProvider lockManager, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) + { + _logger = logFactory.CreateLogger(GetType()); + _tracker = tracker; + _lockManager = lockManager; + _client = kinesisClient; + _processTask = new Task(Process); + _processTask.Start(); + _dateTimeProvider = dateTimeProvider; + } + + public async Task Subscribe(string appName, string stream, Action action) + { + var shards = await _client.ListShardsAsync(new ListShardsRequest + { + StreamName = stream + }); + + foreach (var shard in shards.Shards) + { + _subscribers.Add(new ShardSubscription + { + AppName = appName, + Stream = stream, + Shard = shard, + Action = action + }); + } + } + + private async void Process() + { + while (!_cancelToken.IsCancellationRequested) + { + try + { + var todo = _subscribers.Where(x => x.Snooze < _dateTimeProvider.Now).ToList(); + foreach (var sub in todo) + { + if (!await _lockManager.AcquireLock($"{sub.AppName}.{sub.Stream}.{sub.Shard.ShardId}", + _cancelToken.Token)) + continue; + + try + { + var records = await GetBatch(sub); + + if (records.Records.Count == 0) + sub.Snooze = _dateTimeProvider.Now.AddSeconds(5); + + var lastSequence = string.Empty; + + foreach (var rec in records.Records) + { + lastSequence = rec.SequenceNumber; + try + { + sub.Action(rec); + } + catch (Exception ex) + { + _logger.LogError(default(EventId), ex, ex.Message); + } + } + + if (lastSequence != string.Empty) + await _tracker.IncrementShardIteratorAndSequence(sub.AppName, sub.Stream, sub.Shard.ShardId, records.NextShardIterator, lastSequence); + else + await _tracker.IncrementShardIterator(sub.AppName, sub.Stream, sub.Shard.ShardId, records.NextShardIterator); + } + finally + { + await _lockManager.ReleaseLock($"{sub.AppName}.{sub.Stream}.{sub.Shard.ShardId}"); + } + } + + if (todo.Count == 0) + await Task.Delay(2000); + } + catch (Exception ex) + { + _logger.LogError(default(EventId), ex, ex.Message); + } + } + } + + private async Task GetBatch(ShardSubscription sub) + { + var iterator = await _tracker.GetNextShardIterator(sub.AppName, sub.Stream, sub.Shard.ShardId); + + if (iterator == null) + { + var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest + { + ShardId = sub.Shard.ShardId, + StreamName = sub.Stream, + ShardIteratorType = ShardIteratorType.AT_SEQUENCE_NUMBER, + StartingSequenceNumber = sub.Shard.SequenceNumberRange.StartingSequenceNumber + }); + iterator = iterResp.ShardIterator; + } + + try + { + var result = await _client.GetRecordsAsync(new GetRecordsRequest + { + ShardIterator = iterator, + Limit = _batchSize + }); + + return result; + } + catch (ExpiredIteratorException) + { + var lastSequence = await _tracker.GetNextLastSequenceNumber(sub.AppName, sub.Stream, sub.Shard.ShardId); + var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest + { + ShardId = sub.Shard.ShardId, + StreamName = sub.Stream, + ShardIteratorType = ShardIteratorType.AFTER_SEQUENCE_NUMBER, + StartingSequenceNumber = lastSequence + }); + iterator = iterResp.ShardIterator; + + var result = await _client.GetRecordsAsync(new GetRecordsRequest + { + ShardIterator = iterator, + Limit = _batchSize + }); + + return result; + } + } + + public void Dispose() + { + _cancelToken.Cancel(); + _processTask.Wait(5000); + } + + class ShardSubscription + { + public string AppName { get; set; } + public string Stream { get; set; } + public Shard Shard { get; set; } + public Action Action { get; set; } + public DateTime Snooze { get; set; } = DateTime.Now; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs new file mode 100644 index 000000000..d7c028c46 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Microsoft.Extensions.Logging; +using WorkflowCore.Providers.AWS.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class KinesisTracker : IKinesisTracker + { + private readonly ILogger _logger; + private readonly AmazonDynamoDBClient _client; + private readonly string _tableName; + private bool _tableConfirmed = false; + + public KinesisTracker(AmazonDynamoDBClient client, string tableName, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(GetType()); + _client = client; + _tableName = tableName; + } + + public async Task GetNextShardIterator(string app, string stream, string shard) + { + if (!_tableConfirmed) + await EnsureTable(); + + var response = await _client.GetItemAsync(new GetItemRequest + { + TableName = _tableName, + Key = new Dictionary + { + { "id", new AttributeValue(FormatId(app, stream, shard)) } + } + }); + + if (!response.Item.ContainsKey("next_iterator")) + return null; + + return response.Item["next_iterator"].S; + } + + public async Task GetNextLastSequenceNumber(string app, string stream, string shard) + { + if (!_tableConfirmed) + await EnsureTable(); + + var response = await _client.GetItemAsync(new GetItemRequest + { + TableName = _tableName, + Key = new Dictionary + { + { "id", new AttributeValue(FormatId(app, stream, shard)) } + } + }); + + if (!response.Item.ContainsKey("last_sequence")) + return null; + + return response.Item["last_sequence"].S; + } + + public async Task IncrementShardIterator(string app, string stream, string shard, string iterator) + { + if (!_tableConfirmed) + await EnsureTable(); + + await _client.UpdateItemAsync(new UpdateItemRequest + { + TableName = _tableName, + Key = new Dictionary + { + {"id", new AttributeValue(FormatId(app, stream, shard))} + }, + UpdateExpression = "SET next_iterator = :n", + ExpressionAttributeValues = new Dictionary + { + { ":n" , new AttributeValue(iterator) } + } + }); + } + + public async Task IncrementShardIteratorAndSequence(string app, string stream, string shard, string iterator, string sequence) + { + if (!_tableConfirmed) + await EnsureTable(); + + await _client.PutItemAsync(new PutItemRequest + { + TableName = _tableName, + Item = new Dictionary + { + {"id", new AttributeValue(FormatId(app, stream, shard))}, + {"next_iterator", new AttributeValue(iterator)}, + {"last_sequence", new AttributeValue(sequence)} + } + }); + } + + private async Task EnsureTable() + { + try + { + var poll = await _client.DescribeTableAsync(_tableName); + _tableConfirmed = true; + } + catch (ResourceNotFoundException) + { + await CreateTable(); + } + } + + private async Task CreateTable() + { + var createRequest = new CreateTableRequest(_tableName, new List + { + new KeySchemaElement("id", KeyType.HASH) + }) + { + AttributeDefinitions = new List + { + new AttributeDefinition("id", ScalarAttributeType.S) + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }; + + await _client.CreateTableAsync(createRequest); + + int i = 0; + while (i < 20) + { + try + { + i++; + await Task.Delay(1000); + var poll = await _client.DescribeTableAsync(_tableName); + if (poll.Table.TableStatus == TableStatus.ACTIVE) + { + _tableConfirmed = true; + return; + } + } + catch (ResourceNotFoundException) + { + } + } + } + + private string FormatId(string app, string stream, string shard) => $"{app}.{stream}.{shard}"; + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs new file mode 100644 index 000000000..c15fb02af --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Amazon.Runtime; +using Amazon.SQS; +using Amazon.SQS.Model; +using WorkflowCore.Interface; + +namespace WorkflowCore.Providers.AWS.Services +{ + public class SQSQueueProvider : IQueueProvider + { + private const int WaitTime = 5; + private readonly ILogger _logger; + private readonly IAmazonSQS _client; + private readonly Dictionary _queues = new Dictionary(); + private readonly string _queuesPrefix; + + public bool IsDequeueBlocking => true; + + public SQSQueueProvider(AmazonSQSClient sqsClient, ILoggerFactory logFactory, string queuesPrefix) + { + _logger = logFactory.CreateLogger(); + _client = sqsClient; + _queuesPrefix = queuesPrefix; + } + + public async Task QueueWork(string id, QueueType queue) + { + var queueUrl = _queues[queue]; + + await _client.SendMessageAsync(new SendMessageRequest(queueUrl, id)); + } + + public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) + { + var queueUrl = _queues[queue]; + + var result = await _client.ReceiveMessageAsync(new ReceiveMessageRequest(queueUrl) + { + MaxNumberOfMessages = 1, + WaitTimeSeconds = WaitTime + }); + + if (result.Messages.Count == 0) + return null; + + var msg = result.Messages.First(); + + await _client.DeleteMessageAsync(new DeleteMessageRequest(queueUrl, msg.ReceiptHandle)); + return msg.Body; + } + + public async Task Start() + { + var workflowQueue = await _client.CreateQueueAsync(new CreateQueueRequest($"{_queuesPrefix}-workflows")); + var eventQueue = await _client.CreateQueueAsync(new CreateQueueRequest($"{_queuesPrefix}-events")); + var indexQueue = await _client.CreateQueueAsync(new CreateQueueRequest($"{_queuesPrefix}-index")); + + _queues[QueueType.Workflow] = workflowQueue.QueueUrl; + _queues[QueueType.Event] = eventQueue.QueueUrl; + _queues[QueueType.Index] = indexQueue.QueueUrl; + } + + public Task Stop() => Task.CompletedTask; + + public void Dispose() + { + } + } +} diff --git a/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj b/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj new file mode 100644 index 000000000..4ea4a1ed8 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0 + Daniel Gerlag + AWS providers for Workflow Core + +- Provides Queueing support on Workflow Core +- Provides distributed locking support on Workflow Core + + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core.git + git + + + + + + + + + + + + + + + diff --git a/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosClientFactory.cs b/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosClientFactory.cs new file mode 100644 index 000000000..cf8b44c47 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosClientFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.Azure.Cosmos; + +namespace WorkflowCore.Providers.Azure.Interface +{ + public interface ICosmosClientFactory + { + CosmosClient GetCosmosClient(); + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosDbProvisioner.cs b/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosDbProvisioner.cs new file mode 100644 index 000000000..d24cca4d8 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Interface/ICosmosDbProvisioner.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace WorkflowCore.Providers.Azure.Interface +{ + public interface ICosmosDbProvisioner + { + Task Provision(string dbId, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs b/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs index f32a1b0cd..5b0343456 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.WindowsAzure.Storage.Blob; namespace WorkflowCore.Providers.Azure.Models diff --git a/src/providers/WorkflowCore.Providers.Azure/Models/PersistedEvent.cs b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedEvent.cs new file mode 100644 index 000000000..57e43fd40 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedEvent.cs @@ -0,0 +1,49 @@ +using Newtonsoft.Json; +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.Providers.Azure.Models +{ + public class PersistedEvent + { + public string id { get; set; } + + public string EventName { get; set; } + + public string EventKey { get; set; } + + public string EventData { get; set; } + + public DateTime EventTime { get; set; } + + public bool IsProcessed { get; set; } + + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + + public static PersistedEvent FromInstance(Event instance) + { + return new PersistedEvent + { + id = instance.Id, + EventKey = instance.EventKey, + EventName = instance.EventName, + EventTime = instance.EventTime, + IsProcessed = instance.IsProcessed, + EventData = JsonConvert.SerializeObject(instance.EventData, SerializerSettings), + }; + } + + public static Event ToInstance(PersistedEvent instance) + { + return new Event + { + Id = instance.id, + EventKey = instance.EventKey, + EventName = instance.EventName, + EventTime = instance.EventTime, + IsProcessed = instance.IsProcessed, + EventData = JsonConvert.DeserializeObject(instance.EventData, SerializerSettings), + }; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Models/PersistedSubscription.cs b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedSubscription.cs new file mode 100644 index 000000000..d1966eea6 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedSubscription.cs @@ -0,0 +1,69 @@ +using System; +using Newtonsoft.Json; +using WorkflowCore.Models; + +namespace WorkflowCore.Providers.Azure.Models +{ + public class PersistedSubscription + { + public string id { get; set; } + + public string WorkflowId { get; set; } + + public int StepId { get; set; } + + public string ExecutionPointerId { get; set; } + + public string EventName { get; set; } + + public string EventKey { get; set; } + + public DateTime SubscribeAsOf { get; set; } + + public string SubscriptionData { get; set; } + + public string ExternalToken { get; set; } + + public string ExternalWorkerId { get; set; } + + public DateTime? ExternalTokenExpiry { get; set; } + + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + + public static PersistedSubscription FromInstance(EventSubscription instance) + { + return new PersistedSubscription + { + id = instance.Id, + EventKey = instance.EventKey, + EventName = instance.EventName, + ExecutionPointerId = instance.ExecutionPointerId, + ExternalToken = instance.ExternalToken, + ExternalTokenExpiry = instance.ExternalTokenExpiry, + ExternalWorkerId = instance.ExternalWorkerId, + StepId = instance.StepId, + SubscribeAsOf = instance.SubscribeAsOf, + WorkflowId = instance.WorkflowId, + SubscriptionData = JsonConvert.SerializeObject(instance.SubscriptionData, SerializerSettings), + }; + } + + public static EventSubscription ToInstance(PersistedSubscription instance) + { + return new EventSubscription + { + Id = instance.id, + EventKey = instance.EventKey, + EventName = instance.EventName, + ExecutionPointerId = instance.ExecutionPointerId, + ExternalToken = instance.ExternalToken, + ExternalTokenExpiry = instance.ExternalTokenExpiry, + ExternalWorkerId = instance.ExternalWorkerId, + StepId = instance.StepId, + SubscribeAsOf = instance.SubscribeAsOf, + WorkflowId = instance.WorkflowId, + SubscriptionData = JsonConvert.DeserializeObject(instance.SubscriptionData, SerializerSettings), + }; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Models/PersistedWorkflow.cs b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedWorkflow.cs new file mode 100644 index 000000000..f8af8228d --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Models/PersistedWorkflow.cs @@ -0,0 +1,74 @@ +using Newtonsoft.Json; +using System; +using WorkflowCore.Models; + +namespace WorkflowCore.Providers.Azure.Models +{ + public class PersistedWorkflow + { + public string id { get; set; } + + public string WorkflowDefinitionId { get; set; } + + public int Version { get; set; } + + public string Description { get; set; } + + public string Reference { get; set; } + + public string ExecutionPointers { get; set; } + + public long? NextExecution { get; set; } + + public WorkflowStatus Status { get; set; } + + public string Data { get; set; } + + public DateTime CreateTime { get; set; } + + public DateTime? CompleteTime { get; set; } + + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + + public static PersistedWorkflow FromInstance(WorkflowInstance instance) + { + var result = new PersistedWorkflow + { + id = instance.Id, + CompleteTime = instance.CompleteTime, + CreateTime = instance.CreateTime, + Description = instance.Description, + NextExecution = instance.NextExecution, + Reference = instance.Reference, + Status = instance.Status, + Version = instance.Version, + WorkflowDefinitionId = instance.WorkflowDefinitionId, + Data = JsonConvert.SerializeObject(instance.Data, SerializerSettings), + ExecutionPointers = JsonConvert.SerializeObject(instance.ExecutionPointers, SerializerSettings), + }; + + return result; + } + + public static WorkflowInstance ToInstance(PersistedWorkflow instance) + { + var result = new WorkflowInstance + { + Id = instance.id, + CompleteTime = instance.CompleteTime, + CreateTime = instance.CreateTime, + Description = instance.Description, + NextExecution = instance.NextExecution, + Reference = instance.Reference, + Status = instance.Status, + Version = instance.Version, + WorkflowDefinitionId = instance.WorkflowDefinitionId, + Data = JsonConvert.DeserializeObject(instance.Data, SerializerSettings), + ExecutionPointers = JsonConvert.DeserializeObject(instance.ExecutionPointers, SerializerSettings), + }; + + return result; + } + + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/README.md b/src/providers/WorkflowCore.Providers.Azure/README.md index e27e2df41..10c17bfbe 100644 --- a/src/providers/WorkflowCore.Providers.Azure/README.md +++ b/src/providers/WorkflowCore.Providers.Azure/README.md @@ -2,6 +2,8 @@ * Provides [DLM](https://en.wikipedia.org/wiki/Distributed_lock_manager) support on [Workflow Core](../../README.md) using Azure Blob Storage leases. * Provides Queueing support on [Workflow Core](../../README.md) using Azure Storage queues. +* Provides event hub support on [Workflow Core](../../README.md) backed by Azure Service Bus. +* Provides persistence on [Workflow Core](../../README.md) backed by Azure Cosmos DB. This makes it possible to have a cluster of nodes processing your workflows. @@ -9,14 +11,26 @@ This makes it possible to have a cluster of nodes processing your workflows. Install the NuGet package "WorkflowCore.Providers.Azure" +Using Nuget package console ``` PM> Install-Package WorkflowCore.Providers.Azure ``` +Using .NET CLI +``` +dotnet add package WorkflowCore.Providers.Azure +``` ## Usage -Use the .UseAzureSyncronization extension method when building your service provider. +Use the `IServiceCollection` extension methods when building your service provider +* .UseAzureSynchronization +* .UseAzureServiceBusEventHub ```C# -services.AddWorkflow(x => x.UseAzureSyncronization("azure storage connection string")); +services.AddWorkflow(options => +{ + options.UseAzureSynchronization("azure storage connection string"); + options.UseAzureServiceBusEventHub("service bus connection string", "topic name", "subscription name"); + options.UseCosmosDbPersistence("connection string"); +}); ``` \ No newline at end of file diff --git a/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs index 95ae04031..0aa1963e4 100644 --- a/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs @@ -1,20 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Providers.Azure.Interface; using WorkflowCore.Providers.Azure.Services; namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static WorkflowOptions UseAzureSyncronization(this WorkflowOptions options, string connectionString) + public static WorkflowOptions UseAzureSynchronization(this WorkflowOptions options, string connectionString) { options.UseQueueProvider(sp => new AzureStorageQueueProvider(connectionString, sp.GetService())); options.UseDistributedLockManager(sp => new AzureLockManager(connectionString, sp.GetService())); return options; } + + public static WorkflowOptions UseAzureServiceBusEventHub( + this WorkflowOptions options, + string connectionString, + string topicName, + string subscriptionName) + { + options.UseEventHub(sp => new ServiceBusLifeCycleEventHub( + connectionString, topicName, subscriptionName, sp.GetService())); + + return options; + } + + public static WorkflowOptions UseCosmosDbPersistence( + this WorkflowOptions options, + string connectionString, + string databaseId, + CosmosDbStorageOptions cosmosDbStorageOptions = null) + { + if (cosmosDbStorageOptions == null) + { + cosmosDbStorageOptions = new CosmosDbStorageOptions(); + } + + options.Services.AddSingleton(sp => new CosmosClientFactory(connectionString)); + options.Services.AddTransient(sp => new CosmosDbProvisioner(sp.GetService(), cosmosDbStorageOptions)); + options.Services.AddSingleton(sp => new WorkflowPurger(sp.GetService(), databaseId, cosmosDbStorageOptions)); + options.UsePersistence(sp => new CosmosDbPersistenceProvider(sp.GetService(), databaseId, sp.GetService(), cosmosDbStorageOptions)); + return options; + } } } diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs b/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs index 2bc97ec67..c52e823c7 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -74,7 +73,7 @@ public async Task ReleaseLock(string Id) } catch (Exception ex) { - _logger.LogError($"Error releasing lock - {ex.Message}"); + _logger.LogError(ex, $"Error releasing lock - {ex.Message}"); } _locks.Remove(entry); } @@ -93,13 +92,15 @@ public async Task Start() _renewTimer = new Timer(RenewLeases, null, RenewInterval, RenewInterval); } - public async Task Stop() + public Task Stop() { if (_renewTimer == null) - return; + return Task.CompletedTask; _renewTimer.Dispose(); _renewTimer = null; + + return Task.CompletedTask; } private async void RenewLeases(object state) @@ -114,7 +115,7 @@ private async void RenewLeases(object state) } catch (Exception ex) { - _logger.LogError($"Error renewing leases - {ex.Message}"); + _logger.LogError(ex, $"Error renewing leases - {ex.Message}"); } finally { @@ -131,7 +132,7 @@ private async Task RenewLock(ControlledLock entry) } catch (Exception ex) { - _logger.LogError($"Error renewing lease - {ex.Message}"); + _logger.LogError(ex, $"Error renewing lease - {ex.Message}"); } } } diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs b/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs index 75be01fd1..cbd76d1f7 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -13,8 +12,8 @@ namespace WorkflowCore.Providers.Azure.Services public class AzureStorageQueueProvider : IQueueProvider { private readonly ILogger _logger; - private readonly CloudQueue _workflowQueue; - private readonly CloudQueue _eventQueue; + + private readonly Dictionary _queues = new Dictionary(); public bool IsDequeueBlocking => false; @@ -24,37 +23,20 @@ public AzureStorageQueueProvider(string connectionString, ILoggerFactory logFact var account = CloudStorageAccount.Parse(connectionString); var client = account.CreateCloudQueueClient(); - _workflowQueue = client.GetQueueReference("workflowcore-workflows"); - _eventQueue = client.GetQueueReference("workflowcore-events"); + _queues[QueueType.Workflow] = client.GetQueueReference("workflowcore-workflows"); + _queues[QueueType.Event] = client.GetQueueReference("workflowcore-events"); + _queues[QueueType.Index] = client.GetQueueReference("workflowcore-index"); } public async Task QueueWork(string id, QueueType queue) { var msg = new CloudQueueMessage(id); - - switch (queue) - { - case QueueType.Workflow: - await _workflowQueue.AddMessageAsync(msg); - break; - case QueueType.Event: - await _eventQueue.AddMessageAsync(msg); - break; - } + await _queues[queue].AddMessageAsync(msg); } public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) { - CloudQueue cloudQueue = null; - switch (queue) - { - case QueueType.Workflow: - cloudQueue = _workflowQueue; - break; - case QueueType.Event: - cloudQueue = _eventQueue; - break; - } + CloudQueue cloudQueue = _queues[queue]; if (cloudQueue == null) return null; @@ -70,13 +52,13 @@ public async Task DequeueWork(QueueType queue, CancellationToken cancell public async Task Start() { - await _workflowQueue.CreateIfNotExistsAsync(); - await _eventQueue.CreateIfNotExistsAsync(); + foreach (var queue in _queues.Values) + { + await queue.CreateIfNotExistsAsync(); + } } - public async Task Stop() - { - } + public Task Stop() => Task.CompletedTask; public void Dispose() { diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/CosmosClientFactory.cs b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosClientFactory.cs new file mode 100644 index 000000000..9cb4cc572 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosClientFactory.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Azure.Cosmos; +using WorkflowCore.Providers.Azure.Interface; + +namespace WorkflowCore.Providers.Azure.Services +{ + public class CosmosClientFactory : ICosmosClientFactory, IDisposable + { + private bool isDisposed = false; + + private CosmosClient _client; + + public CosmosClientFactory(string connectionString) + { + _client = new CosmosClient(connectionString); + } + + public CosmosClient GetCosmosClient() + { + return this._client; + } + + /// + /// Dispose of cosmos client + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Dispose of cosmos client + /// + /// True if disposing + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this._client.Dispose(); + } + + this.isDisposed = true; + } + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbPersistenceProvider.cs b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbPersistenceProvider.cs new file mode 100644 index 000000000..008136494 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbPersistenceProvider.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Cosmos.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Providers.Azure.Interface; +using WorkflowCore.Providers.Azure.Models; + +namespace WorkflowCore.Providers.Azure.Services +{ + public class CosmosDbPersistenceProvider : IPersistenceProvider + { + private readonly ICosmosDbProvisioner _provisioner; + private readonly string _dbId; + private readonly ICosmosClientFactory _clientFactory; + private readonly Lazy _workflowContainer; + private readonly Lazy _eventContainer; + private readonly Lazy _subscriptionContainer; + + public CosmosDbPersistenceProvider( + ICosmosClientFactory clientFactory, + string dbId, + ICosmosDbProvisioner provisioner, + CosmosDbStorageOptions cosmosDbStorageOptions) + { + _provisioner = provisioner; + _dbId = dbId; + _clientFactory = clientFactory; + _workflowContainer = new Lazy(() => _clientFactory.GetCosmosClient().GetDatabase(_dbId).GetContainer(cosmosDbStorageOptions.WorkflowContainerName)); + _eventContainer = new Lazy(() => _clientFactory.GetCosmosClient().GetDatabase(_dbId).GetContainer(cosmosDbStorageOptions.EventContainerName)); + _subscriptionContainer = new Lazy(() => _clientFactory.GetCosmosClient().GetDatabase(_dbId).GetContainer(cosmosDbStorageOptions.SubscriptionContainerName)); + } + + public bool SupportsScheduledCommands => false; + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) + { + var existing = await _subscriptionContainer.Value.ReadItemAsync(eventSubscriptionId, new PartitionKey(eventSubscriptionId)); + + if (existing.Resource.ExternalToken != token) + throw new InvalidOperationException(); + existing.Resource.ExternalToken = null; + existing.Resource.ExternalWorkerId = null; + existing.Resource.ExternalTokenExpiry = null; + + await _subscriptionContainer.Value.ReplaceItemAsync(existing.Resource, eventSubscriptionId, cancellationToken: cancellationToken); + } + + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken) + { + newEvent.Id = Guid.NewGuid().ToString(); + var result = await _eventContainer.Value.CreateItemAsync(PersistedEvent.FromInstance(newEvent), cancellationToken: cancellationToken); + return result.Resource.id; + } + + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken) + { + subscription.Id = Guid.NewGuid().ToString(); + var result = await _subscriptionContainer.Value.CreateItemAsync(PersistedSubscription.FromInstance(subscription), cancellationToken: cancellationToken); + return result.Resource.id; + } + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken) + { + workflow.Id = Guid.NewGuid().ToString(); + var result = await _workflowContainer.Value.CreateItemAsync(PersistedWorkflow.FromInstance(workflow), cancellationToken: cancellationToken); + return result.Resource.id; + } + + public void EnsureStoreExists() + { + _provisioner.Provision(_dbId).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public async Task GetEvent(string id, CancellationToken cancellationToken) + { + var resp = await _eventContainer.Value.ReadItemAsync(id, new PartitionKey(id), cancellationToken: cancellationToken); + return PersistedEvent.ToInstance(resp.Resource); + } + + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) + { + var events = new List(); + using (FeedIterator feedIterator = _eventContainer.Value.GetItemLinqQueryable() + .Where(x => x.EventName == eventName && x.EventKey == eventKey) + .Where(x => x.EventTime >= asOf) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + events.Add(item.id); + } + } + } + + return events; + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) + { + EventSubscription eventSubscription = null; + using (FeedIterator feedIterator = _subscriptionContainer.Value.GetItemLinqQueryable() + .Where(x => x.ExternalToken == null && x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults && eventSubscription == null) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + eventSubscription = PersistedSubscription.ToInstance(item); + } + } + } + + return eventSubscription; + } + + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken) + { + var events = new List(); + using (FeedIterator feedIterator = _eventContainer.Value.GetItemLinqQueryable() + .Where(x => !x.IsProcessed) + .Where(x => x.EventTime <= asAt.ToUniversalTime()) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + events.Add(item.id); + } + } + } + + return events; + } + + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken) + { + var now = asAt.ToUniversalTime().Ticks; + + var instances = new List(); + using (FeedIterator feedIterator = _workflowContainer.Value.GetItemLinqQueryable() + .Where(x => x.NextExecution.HasValue && (x.NextExecution <= now) && (x.Status == WorkflowStatus.Runnable)) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + instances.Add(item.id); + } + } + } + + return instances; + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken) + { + var resp = await _subscriptionContainer.Value.ReadItemAsync(eventSubscriptionId, new PartitionKey(eventSubscriptionId), cancellationToken: cancellationToken); + return PersistedSubscription.ToInstance(resp.Resource); + } + + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken) + { + var subscriptions = new List(); + using (FeedIterator feedIterator = _subscriptionContainer.Value.GetItemLinqQueryable() + .Where(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + subscriptions.Add(PersistedSubscription.ToInstance(item)); + } + } + } + + return subscriptions; + } + + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken) + { + var result = await _workflowContainer.Value.ReadItemAsync(Id, new PartitionKey(Id), cancellationToken: cancellationToken); + return PersistedWorkflow.ToInstance(result.Resource); + } + + public Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) + { + throw new NotImplementedException(); + } + + public Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken) + { + var evt = await _eventContainer.Value.ReadItemAsync(id, new PartitionKey(id), cancellationToken: cancellationToken); + evt.Resource.IsProcessed = true; + await _eventContainer.Value.ReplaceItemAsync(evt.Resource, id, cancellationToken: cancellationToken); + } + + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken) + { + var evt = await _eventContainer.Value.ReadItemAsync(id, new PartitionKey(id), cancellationToken: cancellationToken); + evt.Resource.IsProcessed = false; + await _eventContainer.Value.ReplaceItemAsync(evt.Resource, id, cancellationToken: cancellationToken); + } + + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) + { + return Task.CompletedTask; + } + + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken) + { + await _workflowContainer.Value.UpsertItemAsync(PersistedWorkflow.FromInstance(workflow), cancellationToken: cancellationToken); + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + await PersistWorkflow(workflow, cancellationToken); + + foreach(var subscription in subscriptions) + { + await CreateEventSubscription(subscription, cancellationToken); + } + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken) + { + var sub = await _subscriptionContainer.Value.ReadItemAsync(eventSubscriptionId, new PartitionKey(eventSubscriptionId), cancellationToken: cancellationToken); + var existingEntity = sub.Resource; + existingEntity.ExternalToken = token; + existingEntity.ExternalWorkerId = workerId; + existingEntity.ExternalTokenExpiry = expiry; + + await _subscriptionContainer.Value.ReplaceItemAsync(existingEntity, eventSubscriptionId, cancellationToken: cancellationToken); + + return true; + } + + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken) + { + await _subscriptionContainer.Value.DeleteItemAsync(eventSubscriptionId, new PartitionKey(eventSubscriptionId), cancellationToken: cancellationToken); + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbProvisioner.cs b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbProvisioner.cs new file mode 100644 index 000000000..54aa21435 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbProvisioner.cs @@ -0,0 +1,38 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using WorkflowCore.Providers.Azure.Interface; + +namespace WorkflowCore.Providers.Azure.Services +{ + public class CosmosDbProvisioner : ICosmosDbProvisioner + { + private readonly ICosmosClientFactory _clientFactory; + private readonly CosmosDbStorageOptions _cosmosDbStorageOptions; + + public CosmosDbProvisioner( + ICosmosClientFactory clientFactory, + CosmosDbStorageOptions cosmosDbStorageOptions) + { + _clientFactory = clientFactory; + _cosmosDbStorageOptions = cosmosDbStorageOptions; + } + + public async Task Provision(string dbId, CancellationToken cancellationToken = default) + { + var dbResp = await _clientFactory.GetCosmosClient().CreateDatabaseIfNotExistsAsync(dbId, cancellationToken: cancellationToken); + var wfIndexPolicy = new IndexingPolicy(); + wfIndexPolicy.IncludedPaths.Add(new IncludedPath { Path = @"/*" }); + wfIndexPolicy.ExcludedPaths.Add(new ExcludedPath { Path = @"/ExecutionPointers/?" }); + + Task.WaitAll( + dbResp.Database.CreateContainerIfNotExistsAsync(new ContainerProperties(_cosmosDbStorageOptions.WorkflowContainerName, @"/id") + { + IndexingPolicy = wfIndexPolicy + }), + dbResp.Database.CreateContainerIfNotExistsAsync(new ContainerProperties(_cosmosDbStorageOptions.EventContainerName, @"/id")), + dbResp.Database.CreateContainerIfNotExistsAsync(new ContainerProperties(_cosmosDbStorageOptions.SubscriptionContainerName, @"/id")) + ); + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbStorageOptions.cs b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbStorageOptions.cs new file mode 100644 index 000000000..fc89c89b0 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosDbStorageOptions.cs @@ -0,0 +1,35 @@ +namespace WorkflowCore.Providers.Azure.Services +{ + public sealed class CosmosDbStorageOptions + { + /// + /// The default name of workflow container. + /// + public const string DefaultWorkflowContainerName = "workflows"; + + /// + /// The name of Workflow container in Cosmos DB. + /// + public string WorkflowContainerName { get; set; } = DefaultWorkflowContainerName; + + /// + /// The default name of event container. + /// + public const string DefaultEventContainerName = "events"; + + /// + /// The name of Event container in Cosmos DB. + /// + public string EventContainerName { get; set; } = DefaultEventContainerName; + + /// + /// The default name of subscription container. + /// + public const string DefaultSubscriptionContainerName = "subscriptions"; + + /// + /// The name of Subscription container in Cosmos DB. + /// + public string SubscriptionContainerName { get; set; } = DefaultSubscriptionContainerName; + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/ServiceBusLifeCycleEventHub.cs b/src/providers/WorkflowCore.Providers.Azure/Services/ServiceBusLifeCycleEventHub.cs new file mode 100644 index 000000000..85f7a4245 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/ServiceBusLifeCycleEventHub.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.ServiceBus; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using WorkflowCore.Interface; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Providers.Azure.Services +{ + public class ServiceBusLifeCycleEventHub : ILifeCycleEventHub + { + private readonly ITopicClient _topicClient; + private readonly ILogger _logger; + private readonly ISubscriptionClient _subscriptionClient; + private readonly ICollection> _subscribers = new HashSet>(); + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All, + ReferenceLoopHandling = ReferenceLoopHandling.Error, + }; + + public ServiceBusLifeCycleEventHub( + string connectionString, + string topicName, + string subscriptionName, + ILoggerFactory logFactory) + { + _subscriptionClient = new SubscriptionClient(connectionString, topicName, subscriptionName); + _topicClient = new TopicClient(connectionString, topicName); + _logger = logFactory.CreateLogger(GetType()); + } + + public async Task PublishNotification(LifeCycleEvent evt) + { + var payload = JsonConvert.SerializeObject(evt, _serializerSettings); + var message = new Message(Encoding.Default.GetBytes(payload)) + { + Label = evt.Reference + }; + + await _topicClient.SendAsync(message); + } + + public void Subscribe(Action action) + { + _subscribers.Add(action); + } + + public Task Start() + { + var messageHandlerOptions = new MessageHandlerOptions(ExceptionHandler) + { + AutoComplete = false + }; + + _subscriptionClient.RegisterMessageHandler(MessageHandler, messageHandlerOptions); + + return Task.CompletedTask; + } + + public async Task Stop() + { + await _topicClient.CloseAsync(); + await _subscriptionClient.CloseAsync(); + } + + private async Task MessageHandler(Message message, CancellationToken cancellationToken) + { + try + { + var payload = Encoding.Default.GetString(message.Body); + var evt = JsonConvert.DeserializeObject( + payload, _serializerSettings); + + NotifySubscribers(evt); + + await _subscriptionClient + .CompleteAsync(message.SystemProperties.LockToken) + .ConfigureAwait(false); + } + catch + { + await _subscriptionClient.AbandonAsync(message.SystemProperties.LockToken); + } + } + + private Task ExceptionHandler(ExceptionReceivedEventArgs arg) + { + _logger.LogWarning(default, arg.Exception, "Error on receiving events"); + + return Task.CompletedTask; + } + + private void NotifySubscribers(LifeCycleEvent evt) + { + foreach (var subscriber in _subscribers) + { + try + { + subscriber(evt); + } + catch (Exception ex) + { + _logger.LogWarning( + default, ex, $"Error on event subscriber: {ex.Message}"); + } + } + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/WorkflowPurger.cs b/src/providers/WorkflowCore.Providers.Azure/Services/WorkflowPurger.cs new file mode 100644 index 000000000..64050f9f8 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/WorkflowPurger.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Cosmos.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Providers.Azure.Interface; +using WorkflowCore.Providers.Azure.Models; + +namespace WorkflowCore.Providers.Azure.Services +{ + public class WorkflowPurger : IWorkflowPurger + { + private readonly Lazy _workflowContainer; + + public WorkflowPurger(ICosmosClientFactory clientFactory, string dbId, CosmosDbStorageOptions cosmosDbStorageOptions) + { + _workflowContainer = new Lazy(() => clientFactory.GetCosmosClient() + .GetDatabase(dbId) + .GetContainer(cosmosDbStorageOptions.WorkflowContainerName)); + } + + public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan, CancellationToken cancellationToken = default) + { + var olderThanUtc = olderThan.ToUniversalTime(); + using (FeedIterator feedIterator = _workflowContainer.Value.GetItemLinqQueryable() + .Where(x => x.Status == status && x.CompleteTime < olderThanUtc) + .ToFeedIterator()) + { + while (feedIterator.HasMoreResults) + { + foreach (var item in await feedIterator.ReadNextAsync(cancellationToken)) + { + await _workflowContainer.Value.DeleteItemAsync(item.id, new PartitionKey(item.id), cancellationToken: cancellationToken); + } + } + } + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj b/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj index a31afecd3..cedd72024 100644 --- a/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj +++ b/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj @@ -1,25 +1,25 @@  - netstandard1.3 + netstandard2.0 Azure providers for Workflow Core - Provides distributed lock management on Workflow Core - Provides Queueing support on Workflow Core workflow workflowcore dlm - 1.3.0 $(PackageTargetFallback);dnxcore50 https://github.com/danielgerlag/workflow-core https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git https://github.com/danielgerlag/workflow-core.git Daniel Gerlag - 1.3.0.0 - 1.3.0.0 - + + + + diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs b/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs new file mode 100644 index 000000000..1399bdd23 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; + +namespace WorkflowCore.Providers.Elasticsearch.Models +{ + public class WorkflowSearchModel + { + public string Id { get; set; } + + public string WorkflowDefinitionId { get; set; } + + public int Version { get; set; } + + public string Description { get; set; } + + public string Reference { get; set; } + + public DateTime? NextExecutionUtc { get; set; } + + public string Status { get; set; } + + public Dictionary Data { get; set; } = new Dictionary(); + + public IEnumerable DataTokens { get; set; } + + public DateTime CreateTime { get; set; } + + public DateTime? CompleteTime { get; set; } + + public ICollection WaitingSteps { get; set; } = new HashSet(); + + public ICollection SleepingSteps { get; set; } = new HashSet(); + + public ICollection FailedSteps { get; set; } = new HashSet(); + + public WorkflowSearchResult ToSearchResult() + { + var result = new WorkflowSearchResult + { + Id = Id, + CompleteTime = CompleteTime, + CreateTime = CreateTime, + Description = Description, + NextExecutionUtc = NextExecutionUtc, + Reference = Reference, + Status = (WorkflowStatus) Enum.Parse(typeof(WorkflowStatus), Status, true), + Version = Version, + WorkflowDefinitionId = WorkflowDefinitionId, + FailedSteps = FailedSteps, + SleepingSteps = SleepingSteps, + WaitingSteps = WaitingSteps + }; + + if (Data.Count > 0) + result.Data = Data.First().Value; + + return result; + } + + public static WorkflowSearchModel FromWorkflowInstance(WorkflowInstance workflow) + { + var result = new WorkflowSearchModel(); + + result.Id = workflow.Id; + result.WorkflowDefinitionId = workflow.WorkflowDefinitionId; + result.Description = workflow.Description; + result.Reference = workflow.Reference; + + if (workflow.Data != null) + result.Data.Add(workflow.Data.GetType().FullName, workflow.Data); + + result.CompleteTime = workflow.CompleteTime; + result.CreateTime = workflow.CreateTime; + result.Version = workflow.Version; + result.Status = workflow.Status.ToString(); + + if (workflow.NextExecution.HasValue) + result.NextExecutionUtc = new DateTime(workflow.NextExecution.Value); + + if (workflow.Data is ISearchable) + result.DataTokens = (workflow.Data as ISearchable).GetSearchTokens(); + + foreach (var ep in workflow.ExecutionPointers) + { + if (ep.Status == PointerStatus.Sleeping) + { + result.SleepingSteps.Add(new StepInfo + { + StepId = ep.StepId, + Name = ep.StepName + }); + } + + if (ep.Status == PointerStatus.WaitingForEvent) + { + result.WaitingSteps.Add(new StepInfo + { + StepId = ep.StepId, + Name = ep.StepName + }); + } + + if (ep.Status == PointerStatus.Failed) + { + result.FailedSteps.Add(new StepInfo + { + StepId = ep.StepId, + Name = ep.StepName + }); + } + } + + return result; + } + + } + + public class TypedWorkflowSearchModel : WorkflowSearchModel + { + public new Dictionary Data { get; set; } + } +} diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/README.md b/src/providers/WorkflowCore.Providers.Elasticsearch/README.md new file mode 100644 index 000000000..c75aa34d8 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/README.md @@ -0,0 +1,132 @@ +# 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. + +## Installing + +Install the NuGet package "WorkflowCore.Providers.Elasticsearch" + +Using Nuget package console +``` +PM> Install-Package WorkflowCore.Providers.Elasticsearch +``` + +Using .NET CLI +``` +dotnet add package WorkflowCore.Providers.Elasticsearch +``` + + +## Configuration + +Use the `.UseElasticsearch` extension method on `IServiceCollection` when building your service provider + +```C# +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` + + ```c# + using WorkflowCore.Interfaces; + ... + public class MyData : ISearchable +{ + public string StrValue1 { get; set; } + public string StrValue2 { get; set; } + + public IEnumerable GetSearchTokens() + { + return new List() + { + StrValue1, + StrValue2 + }; + } +} + ``` + + ##### Examples + + Search all fields for "puppies" + ```c# + 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 + ```c# + using WorkflowCore.Models.Search; + ... + + searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Reference, "My Reference")); + ``` + + 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 + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Value1, "blue moon")); + searchIndex.Search("", 0, 10, NumericRangeFilter.LessThan(x => x.Value2, 5)) + ``` diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.Elasticsearch/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..0fc834630 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Extensions.Logging; +using Nest; +using WorkflowCore.Models; +using WorkflowCore.Providers.Elasticsearch.Services; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseElasticsearch(this WorkflowOptions options, ConnectionSettings settings, string indexName) + { + options.UseSearchIndex(sp => new ElasticsearchIndexer(settings, indexName, sp.GetService())); + return options; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs b/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs new file mode 100644 index 000000000..8195bd53a --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs @@ -0,0 +1,135 @@ +using Microsoft.Extensions.Logging; +using Nest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; +using WorkflowCore.Providers.Elasticsearch.Models; + +namespace WorkflowCore.Providers.Elasticsearch.Services +{ + public class ElasticsearchIndexer : ISearchIndex + { + private readonly ConnectionSettings _settings; + private readonly string _indexName; + private readonly ILogger _logger; + private IElasticClient _client; + + public ElasticsearchIndexer(ConnectionSettings settings, string indexName, ILoggerFactory loggerFactory) + { + _settings = settings; + _indexName = indexName.ToLower(); + _logger = loggerFactory.CreateLogger(GetType()); + } + + public async Task IndexWorkflow(WorkflowInstance workflow) + { + if (_client == null) + throw new InvalidOperationException("Not started"); + + var denormModel = WorkflowSearchModel.FromWorkflowInstance(workflow); + + var result = await _client.IndexAsync(denormModel, idx => idx + .Index(_indexName) + ); + + if (!result.ApiCall.Success) + { + _logger.LogError(default(EventId), result.ApiCall.OriginalException, $"Failed to index workflow {workflow.Id}"); + throw new ApplicationException($"Failed to index workflow {workflow.Id}", result.ApiCall.OriginalException); + } + } + + public async Task> Search(string terms, int skip, int take, params SearchFilter[] filters) + { + if (_client == null) + throw new InvalidOperationException("Not started"); + + var result = await _client.SearchAsync(s => s + .Index(_indexName) + .Skip(skip) + .Take(take) + .MinScore(!string.IsNullOrEmpty(terms) ? 0.1 : 0) + .Query(query => query + .Bool(b => b + .Filter(BuildFilterQuery(filters)) + .Should( + should => should.Match(t => t.Field(f => f.Reference).Query(terms).Boost(1.2)), + should => should.Match(t => t.Field(f => f.DataTokens).Query(terms).Boost(1.1)), + should => should.Match(t => t.Field(f => f.WorkflowDefinitionId).Query(terms).Boost(0.9)), + should => should.Match(t => t.Field(f => f.Status).Query(terms).Boost(0.9)), + should => should.Match(t => t.Field(f => f.Description).Query(terms)) + ) + ) + ) + ); + + return new Page + { + Total = result.Total, + Data = result.Hits.Select(x => x.Source).Select(x => x.ToSearchResult()).ToList() + }; + } + + public async Task Start() + { + _client = new ElasticClient(_settings); + var nodeInfo = await _client.Nodes.InfoAsync(); + if (nodeInfo.Nodes.Values.Any(x => Convert.ToUInt32(x.Version.Split('.')[0]) < 6)) + throw new NotSupportedException("Elasticsearch verison 6 or greater is required"); + + var exists = await _client.Indices.ExistsAsync(_indexName); + if (!exists.Exists) + { + await _client.Indices.CreateAsync(_indexName); + } + } + + public Task Stop() + { + _client = null; + return Task.CompletedTask; + } + + private List, QueryContainer>> BuildFilterQuery(SearchFilter[] filters) + { + var result = new List, QueryContainer>>(); + + foreach (var filter in filters) + { + var field = new Field(filter.Property); + if (filter.IsData) + { + Expression> dataExpr = x => x.Data[filter.DataType.FullName]; + var fieldExpr = Expression.Convert(filter.Property, typeof(Func)); + field = new Field(Expression.Lambda(Expression.Invoke(fieldExpr, dataExpr), Expression.Parameter(typeof(WorkflowSearchModel)))); + } + + switch (filter) + { + case ScalarFilter f: + result.Add(x => x.Match(t => t.Field(field).Query(Convert.ToString(f.Value)))); + break; + case DateRangeFilter f: + if (f.BeforeValue.HasValue) + result.Add(x => x.DateRange(t => t.Field(field).LessThan(f.BeforeValue))); + if (f.AfterValue.HasValue) + result.Add(x => x.DateRange(t => t.Field(field).GreaterThan(f.AfterValue))); + break; + case NumericRangeFilter f: + if (f.LessValue.HasValue) + result.Add(x => x.Range(t => t.Field(field).LessThan(f.LessValue))); + if (f.GreaterValue.HasValue) + result.Add(x => x.Range(t => t.Field(field).GreaterThan(f.GreaterValue))); + break; + } + } + + return result; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj b/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj new file mode 100644 index 000000000..7bf5600b8 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core.git + git + Daniel Gerlag + WorkflowCore + 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. + + + + + + + + + + + + diff --git a/src/providers/WorkflowCore.Providers.Redis/README.md b/src/providers/WorkflowCore.Providers.Redis/README.md new file mode 100644 index 000000000..9edc8f31e --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/README.md @@ -0,0 +1,40 @@ +# Redis providers for Workflow Core + +* Provides Persistence support on [Workflow Core](../../README.md) backed by Redis. +* Provides Queueing support on [Workflow Core](../../README.md) backed by Redis. +* Provides Distributed locking support on [Workflow Core](../../README.md) backed by Redis. +* Provides event hub support on [Workflow Core](../../README.md) backed by Redis. + +This makes it possible to have a cluster of nodes processing your workflows. + +## Installing + +Install the NuGet package "WorkflowCore.Providers.Redis" + +Using Nuget package console +``` +PM> Install-Package WorkflowCore.Providers.Redis +``` +Using .NET CLI +``` +dotnet add package WorkflowCore.Providers.Redis +``` + + +## Usage + +Use the `IServiceCollection` extension methods when building your service provider +* .UseRedisPersistence +* .UseRedisQueues +* .UseRedisLocking +* .UseRedisEventHub + +```C# +services.AddWorkflow(cfg => +{ + cfg.UseRedisPersistence("localhost:6379", "app-name"); + cfg.UseRedisLocking("localhost:6379"); + cfg.UseRedisQueues("localhost:6379", "app-name"); + cfg.UseRedisEventHub("localhost:6379", "channel-name") +}); +``` diff --git a/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..27594cfac --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.Logging; +using WorkflowCore.Models; +using WorkflowCore.Providers.Redis.Services; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseRedisQueues(this WorkflowOptions options, string connectionString, string prefix) + { + options.UseQueueProvider(sp => new RedisQueueProvider(connectionString, prefix, sp.GetService())); + return options; + } + + public static WorkflowOptions UseRedisLocking(this WorkflowOptions options, string connectionString, string prefix = null) + { + options.UseDistributedLockManager(sp => new RedisLockProvider(connectionString, prefix, sp.GetService())); + return options; + } + + public static WorkflowOptions UseRedisPersistence(this WorkflowOptions options, string connectionString, string prefix, bool deleteComplete = false) + { + options.UsePersistence(sp => new RedisPersistenceProvider(connectionString, prefix, deleteComplete, sp.GetService())); + return options; + } + + public static WorkflowOptions UseRedisEventHub(this WorkflowOptions options, string connectionString, string channel) + { + options.UseEventHub(sp => new RedisLifeCycleEventHub(connectionString, channel, sp.GetService())); + return options; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs new file mode 100644 index 000000000..bb595309d --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using StackExchange.Redis; +using WorkflowCore.Interface; +using WorkflowCore.Models.LifeCycleEvents; + +namespace WorkflowCore.Providers.Redis.Services +{ + public class RedisLifeCycleEventHub : ILifeCycleEventHub + { + private readonly ILogger _logger; + private readonly string _connectionString; + private readonly string _channel; + private ICollection> _subscribers = new HashSet>(); + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + private IConnectionMultiplexer _multiplexer; + private ISubscriber _subscriber; + + public RedisLifeCycleEventHub(string connectionString, string channel, ILoggerFactory logFactory) + { + _connectionString = connectionString; + _channel = channel; + _logger = logFactory.CreateLogger(GetType()); + } + + public async Task PublishNotification(LifeCycleEvent evt) + { + if (_subscriber == null) + throw new InvalidOperationException(); + + var data = JsonConvert.SerializeObject(evt, _serializerSettings); + await _subscriber.PublishAsync(_channel, data); + } + + public void Subscribe(Action action) + { + _subscribers.Add(action); + } + + public async Task Start() + { + _multiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString); + _subscriber = _multiplexer.GetSubscriber(); + _subscriber.Subscribe(_channel, (channel, message) => { + var evt = JsonConvert.DeserializeObject(message, _serializerSettings); + NotifySubscribers((LifeCycleEvent)evt); + }); + } + + public async Task Stop() + { + await _subscriber.UnsubscribeAllAsync(); + await _multiplexer.CloseAsync(); + _subscriber = null; + _multiplexer = null; + } + + private void NotifySubscribers(LifeCycleEvent evt) + { + foreach (var subscriber in _subscribers) + { + try + { + subscriber(evt); + } + catch (Exception ex) + { + _logger.LogWarning(default(EventId), ex, $"Error on event subscriber: {ex.Message}"); + } + } + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs new file mode 100644 index 000000000..f71a1c323 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using RedLockNet; +using RedLockNet.SERedis; +using RedLockNet.SERedis.Configuration; +using StackExchange.Redis; +using WorkflowCore.Interface; + +namespace WorkflowCore.Providers.Redis.Services +{ + public class RedisLockProvider : IDistributedLockProvider + { + private readonly ILogger _logger; + private readonly string _connectionString; + private readonly string _prefix; + private IConnectionMultiplexer _multiplexer; + private RedLockFactory _redlockFactory; + private readonly TimeSpan _lockTimeout = TimeSpan.FromMinutes(1); + private readonly List ManagedLocks = new List(); + + public RedisLockProvider(string connectionString, string prefix, ILoggerFactory logFactory) + { + _connectionString = connectionString; + _prefix = prefix; + _logger = logFactory.CreateLogger(GetType()); + } + + public async Task AcquireLock(string Id, CancellationToken cancellationToken) + { + if (_redlockFactory == null) + throw new InvalidOperationException(); + + var redLock = await _redlockFactory.CreateLockAsync(GetResource(Id), _lockTimeout); + + if (redLock.IsAcquired) + { + lock (ManagedLocks) + { + ManagedLocks.Add(redLock); + } + return true; + } + + return false; + } + + public Task ReleaseLock(string Id) + { + if (_redlockFactory == null) + throw new InvalidOperationException(); + + var resource = GetResource(Id); + + lock (ManagedLocks) + { + foreach (var redLock in ManagedLocks) + { + if (redLock.Resource == resource) + { + redLock.Dispose(); + ManagedLocks.Remove(redLock); + break; + } + } + } + + return Task.CompletedTask; + } + + public async Task Start() + { + _multiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString); + _redlockFactory = RedLockFactory.Create(new List { new RedLockMultiplexer(_multiplexer) }); + } + + public async Task Stop() + { + _redlockFactory?.Dispose(); + await _multiplexer.CloseAsync(); + _multiplexer = null; + + } + + private string GetResource(string key) + { + if (string.IsNullOrEmpty(_prefix)) + return key; + + return $"{_prefix}:{key}"; + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisPersistenceProvider.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisPersistenceProvider.cs new file mode 100644 index 000000000..6bf8df875 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisPersistenceProvider.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using StackExchange.Redis; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Providers.Redis.Services +{ + public class RedisPersistenceProvider : IPersistenceProvider + { + private readonly ILogger _logger; + private readonly string _connectionString; + private readonly string _prefix; + private const string WORKFLOW_SET = "workflows"; + private const string SUBSCRIPTION_SET = "subscriptions"; + private const string EVENT_SET = "events"; + private const string RUNNABLE_INDEX = "runnable"; + private const string EVENTSLUG_INDEX = "eventslug"; + private readonly IConnectionMultiplexer _multiplexer; + private readonly IDatabase _redis; + + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + private readonly bool _removeComplete; + + public bool SupportsScheduledCommands => false; + + public RedisPersistenceProvider(string connectionString, string prefix, bool removeComplete, ILoggerFactory logFactory) + { + _connectionString = connectionString; + _prefix = prefix; + _logger = logFactory.CreateLogger(GetType()); + _multiplexer = ConnectionMultiplexer.Connect(_connectionString); + _redis = _multiplexer.GetDatabase(); + _removeComplete = removeComplete; + } + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken _ = default) + { + workflow.Id = Guid.NewGuid().ToString(); + await PersistWorkflow(workflow); + return workflow.Id; + } + + public async Task PersistWorkflow(WorkflowInstance workflow, List subscriptions, CancellationToken cancellationToken = default) + { + await PersistWorkflow(workflow, cancellationToken); + + foreach (var subscription in subscriptions) + { + await CreateEventSubscription(subscription, cancellationToken); + } + } + + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken _ = default) + { + var str = JsonConvert.SerializeObject(workflow, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{WORKFLOW_SET}", workflow.Id, str); + + if ((workflow.Status == WorkflowStatus.Runnable) && (workflow.NextExecution.HasValue)) + await _redis.SortedSetAddAsync($"{_prefix}.{WORKFLOW_SET}.{RUNNABLE_INDEX}", workflow.Id, workflow.NextExecution.Value); + else + { + await _redis.SortedSetRemoveAsync($"{_prefix}.{WORKFLOW_SET}.{RUNNABLE_INDEX}", workflow.Id); + if (_removeComplete && workflow.Status == WorkflowStatus.Complete) + await _redis.HashDeleteAsync($"{_prefix}.{WORKFLOW_SET}", workflow.Id); + } + } + + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken _ = default) + { + var result = new List(); + var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{WORKFLOW_SET}.{RUNNABLE_INDEX}", -1, asAt.ToUniversalTime().Ticks); + + foreach (var item in data) + result.Add(item); + + return result; + } + + public Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, + int take) + { + throw new NotImplementedException(); + } + + public async Task GetWorkflowInstance(string Id, CancellationToken _ = default) + { + var raw = await _redis.HashGetAsync($"{_prefix}.{WORKFLOW_SET}", Id); + return JsonConvert.DeserializeObject(raw, _serializerSettings); + } + + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken _ = default) + { + if (ids == null) + { + return new List(); + } + + var raw = await _redis.HashGetAsync($"{_prefix}.{WORKFLOW_SET}", Array.ConvertAll(ids.ToArray(), x => (RedisValue)x)); + return raw.Select(r => JsonConvert.DeserializeObject(r, _serializerSettings)); + } + + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) + { + subscription.Id = Guid.NewGuid().ToString(); + var str = JsonConvert.SerializeObject(subscription, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{SUBSCRIPTION_SET}", subscription.Id, str); + await _redis.SortedSetAddAsync($"{_prefix}.{SUBSCRIPTION_SET}.{EVENTSLUG_INDEX}.{subscription.EventName}-{subscription.EventKey}", subscription.Id, subscription.SubscribeAsOf.Ticks); + + return subscription.Id; + } + + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) + { + var result = new List(); + var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{SUBSCRIPTION_SET}.{EVENTSLUG_INDEX}.{eventName}-{eventKey}", -1, asOf.Ticks); + + foreach (var id in data) + { + var raw = await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", id); + if (raw.HasValue) + result.Add(JsonConvert.DeserializeObject(raw, _serializerSettings)); + } + + return result; + } + + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) + { + var existingRaw = await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId); + var existing = JsonConvert.DeserializeObject(existingRaw, _serializerSettings); + await _redis.HashDeleteAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId); + await _redis.SortedSetRemoveAsync($"{_prefix}.{SUBSCRIPTION_SET}.{EVENTSLUG_INDEX}.{existing.EventName}-{existing.EventKey}", eventSubscriptionId); + } + + public async Task GetSubscription(string eventSubscriptionId, CancellationToken _ = default) + { + var raw = await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId); + return JsonConvert.DeserializeObject(raw, _serializerSettings); + } + + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) + { + return (await GetSubscriptions(eventName, eventKey, asOf, cancellationToken)).FirstOrDefault(sub => string.IsNullOrEmpty(sub.ExternalToken)); + } + + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken _ = default) + { + var item = JsonConvert.DeserializeObject(await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId), _serializerSettings); + if (item.ExternalToken != null) + return false; + item.ExternalToken = token; + item.ExternalWorkerId = workerId; + item.ExternalTokenExpiry = expiry; + var str = JsonConvert.SerializeObject(item, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId, str); + return true; + } + + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken _ = default) + { + var item = JsonConvert.DeserializeObject(await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId), _serializerSettings); + if (item.ExternalToken != token) + return; + item.ExternalToken = null; + item.ExternalWorkerId = null; + item.ExternalTokenExpiry = null; + var str = JsonConvert.SerializeObject(item, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId, str); + } + + public async Task CreateEvent(Event newEvent, CancellationToken _ = default) + { + newEvent.Id = Guid.NewGuid().ToString(); + var str = JsonConvert.SerializeObject(newEvent, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{EVENT_SET}", newEvent.Id, str); + await _redis.SortedSetAddAsync($"{_prefix}.{EVENT_SET}.{EVENTSLUG_INDEX}.{newEvent.EventName}-{newEvent.EventKey}", newEvent.Id, newEvent.EventTime.Ticks); + + if (newEvent.IsProcessed) + await _redis.SortedSetRemoveAsync($"{_prefix}.{EVENT_SET}.{RUNNABLE_INDEX}", newEvent.Id); + else + await _redis.SortedSetAddAsync($"{_prefix}.{EVENT_SET}.{RUNNABLE_INDEX}", newEvent.Id, newEvent.EventTime.Ticks); + + return newEvent.Id; + } + + public async Task GetEvent(string id, CancellationToken _ = default) + { + var raw = await _redis.HashGetAsync($"{_prefix}.{EVENT_SET}", id); + return JsonConvert.DeserializeObject(raw, _serializerSettings); + } + + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken _ = default) + { + var result = new List(); + var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{EVENT_SET}.{RUNNABLE_INDEX}", -1, asAt.Ticks); + + foreach (var item in data) + result.Add(item); + + return result; + } + + public async Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) + { + var result = new List(); + var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{EVENT_SET}.{EVENTSLUG_INDEX}.{eventName}-{eventKey}", asOf.Ticks); + + foreach (var id in data) + result.Add(id); + + return result; + } + + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) + { + var evt = await GetEvent(id, cancellationToken); + evt.IsProcessed = true; + var str = JsonConvert.SerializeObject(evt, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{EVENT_SET}", evt.Id, str); + await _redis.SortedSetRemoveAsync($"{_prefix}.{EVENT_SET}.{RUNNABLE_INDEX}", id); + } + + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) + { + var evt = await GetEvent(id, cancellationToken); + evt.IsProcessed = false; + var str = JsonConvert.SerializeObject(evt, _serializerSettings); + await _redis.HashSetAsync($"{_prefix}.{EVENT_SET}", evt.Id, str); + await _redis.SortedSetAddAsync($"{_prefix}.{EVENT_SET}.{RUNNABLE_INDEX}", evt.Id, evt.EventTime.Ticks); + } + + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) + { + return Task.CompletedTask; + } + + public void EnsureStoreExists() + { + } + + public Task ScheduleCommand(ScheduledCommand command) + { + throw new NotImplementedException(); + } + + public Task ProcessCommands(DateTimeOffset asOf, Func action, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisQueueProvider.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisQueueProvider.cs new file mode 100644 index 000000000..347c699b6 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisQueueProvider.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using WorkflowCore.Interface; + +namespace WorkflowCore.Providers.Redis.Services +{ + public class RedisQueueProvider : IQueueProvider + { + private readonly ILogger _logger; + private readonly string _connectionString; + private readonly string _prefix; + + private IConnectionMultiplexer _multiplexer; + private IDatabase _redis; + + private readonly Dictionary _queues = new Dictionary + { + [QueueType.Workflow] = "workflows", + [QueueType.Event] = "events", + [QueueType.Index] = "index" + }; + + public RedisQueueProvider(string connectionString, string prefix, ILoggerFactory logFactory) + { + _connectionString = connectionString; + _prefix = prefix; + _logger = logFactory.CreateLogger(GetType()); + } + + public async Task QueueWork(string id, QueueType queue) + { + if (_redis == null) + throw new InvalidOperationException(); + + var queueName = GetQueueName(queue); + + var insertResult = await _redis.ListInsertBeforeAsync(queueName, id, id); + if (insertResult == -1 || insertResult == 0) + await _redis.ListRightPushAsync(queueName, id, When.Always); + else + await _redis.ListRemoveAsync(queueName, id, 1); + } + + public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) + { + if (_redis == null) + throw new InvalidOperationException(); + + var result = await _redis.ListLeftPopAsync(GetQueueName(queue)); + + if (result.IsNull) + return null; + + return result; + } + + public bool IsDequeueBlocking => false; + + public async Task Start() + { + _multiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString); + _redis = _multiplexer.GetDatabase(); + } + + public async Task Stop() + { + await _multiplexer.CloseAsync(); + _redis = null; + _multiplexer = null; + } + + public void Dispose() + { + } + + private string GetQueueName(QueueType queue) => $"{_prefix}-{_queues[queue]}"; + } +} diff --git a/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj b/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj new file mode 100644 index 000000000..b1cc0aa97 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + https://github.com/danielgerlag/workflow-core.git + git + https://github.com/danielgerlag/workflow-core + Redis providers for Workflow Core (Persistence, queueing, distributed locking and event hubs) + + + + + + + + + + + + + + diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Interfaces/IRabbitMqQueueNameProvider.cs b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Interfaces/IRabbitMqQueueNameProvider.cs new file mode 100644 index 000000000..f39d75b52 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Interfaces/IRabbitMqQueueNameProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; + +namespace WorkflowCore.QueueProviders.RabbitMQ.Interfaces +{ + public interface IRabbitMqQueueNameProvider + { + string GetQueueName(QueueType queue); + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Properties/AssemblyInfo.cs index 4a1926468..53e1beb8e 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs index b148a0044..4474e403a 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs @@ -2,18 +2,54 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection.Extensions; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.QueueProviders.RabbitMQ.Interfaces; using WorkflowCore.QueueProviders.RabbitMQ.Services; namespace Microsoft.Extensions.DependencyInjection { + public delegate IConnection RabbitMqConnectionFactory(IServiceProvider sp, string clientProvidedName); + public static class ServiceCollectionExtensions { public static WorkflowOptions UseRabbitMQ(this WorkflowOptions options, IConnectionFactory connectionFactory) { - options.UseQueueProvider(sp => new RabbitMQProvider(connectionFactory)); + if (options == null) throw new ArgumentNullException(nameof(options)); + if (connectionFactory == null) throw new ArgumentNullException(nameof(connectionFactory)); + + return options + .UseRabbitMQ((sp, name) => connectionFactory.CreateConnection(name)); + } + + public static WorkflowOptions UseRabbitMQ(this WorkflowOptions options, + IConnectionFactory connectionFactory, + IEnumerable hostnames) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (connectionFactory == null) throw new ArgumentNullException(nameof(connectionFactory)); + if (hostnames == null) throw new ArgumentNullException(nameof(hostnames)); + + return options + .UseRabbitMQ((sp, name) => connectionFactory.CreateConnection(hostnames.ToList(), name)); + } + + public static WorkflowOptions UseRabbitMQ(this WorkflowOptions options, RabbitMqConnectionFactory rabbitMqConnectionFactory) + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (rabbitMqConnectionFactory == null) throw new ArgumentNullException(nameof(rabbitMqConnectionFactory)); + + options.Services.AddSingleton(rabbitMqConnectionFactory); + options.Services.TryAddSingleton(); + options.UseQueueProvider(RabbitMqQueueProviderFactory); + return options; } + + private static IQueueProvider RabbitMqQueueProviderFactory(IServiceProvider sp) + => new RabbitMQProvider(sp, + sp.GetRequiredService(), + sp.GetRequiredService()); } } diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/DefaultRabbitMqQueueNameProvider.cs b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/DefaultRabbitMqQueueNameProvider.cs new file mode 100644 index 000000000..120de752e --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/DefaultRabbitMqQueueNameProvider.cs @@ -0,0 +1,23 @@ +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.RabbitMQ.Interfaces; + +namespace WorkflowCore.QueueProviders.RabbitMQ.Services +{ + public class DefaultRabbitMqQueueNameProvider : IRabbitMqQueueNameProvider + { + public string GetQueueName(QueueType queue) + { + switch (queue) + { + case QueueType.Workflow: + return "wfc.workflow_queue"; + case QueueType.Event: + return "wfc.event_queue"; + case QueueType.Index: + return "wfc.index_queue"; + default: + return null; + } + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/RabbitMQProvider.cs b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/RabbitMQProvider.cs index c09c63166..64322ad50 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/RabbitMQProvider.cs +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/Services/RabbitMQProvider.cs @@ -1,29 +1,35 @@ using Newtonsoft.Json; using RabbitMQ.Client; -using RabbitMQ.Client.Events; using System; -using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; -using WorkflowCore.Models; +using WorkflowCore.QueueProviders.RabbitMQ.Interfaces; namespace WorkflowCore.QueueProviders.RabbitMQ.Services { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public class RabbitMQProvider : IQueueProvider { - private readonly IConnectionFactory _connectionFactory; + private readonly IRabbitMqQueueNameProvider _queueNameProvider; + private readonly RabbitMqConnectionFactory _rabbitMqConnectionFactory; + private readonly IServiceProvider _serviceProvider; + private IConnection _connection = null; - private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; public bool IsDequeueBlocking => false; - public RabbitMQProvider(IConnectionFactory connectionFactory) + public RabbitMQProvider(IServiceProvider serviceProvider, + IRabbitMqQueueNameProvider queueNameProvider, + RabbitMqConnectionFactory connectionFactory) { - _connectionFactory = connectionFactory; + _serviceProvider = serviceProvider; + _queueNameProvider = queueNameProvider; + _rabbitMqConnectionFactory = connectionFactory; } public async Task QueueWork(string id, QueueType queue) @@ -33,9 +39,9 @@ public async Task QueueWork(string id, QueueType queue) using (var channel = _connection.CreateModel()) { - channel.QueueDeclare(queue: GetQueueName(queue), durable: true, exclusive: false, autoDelete: false, arguments: null); + channel.QueueDeclare(queue: _queueNameProvider.GetQueueName(queue), durable: true, exclusive: false, autoDelete: false, arguments: null); var body = Encoding.UTF8.GetBytes(id); - channel.BasicPublish(exchange: "", routingKey: GetQueueName(queue), basicProperties: null, body: body); + channel.BasicPublish(exchange: "", routingKey: _queueNameProvider.GetQueueName(queue), basicProperties: null, body: body); } } @@ -46,7 +52,7 @@ public async Task DequeueWork(QueueType queue, CancellationToken cancell using (var channel = _connection.CreateModel()) { - channel.QueueDeclare(queue: GetQueueName(queue), + channel.QueueDeclare(queue: _queueNameProvider.GetQueueName(queue), durable: true, exclusive: false, autoDelete: false, @@ -54,7 +60,7 @@ public async Task DequeueWork(QueueType queue, CancellationToken cancell channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); - var msg = channel.BasicGet(GetQueueName(queue), false); + var msg = channel.BasicGet(_queueNameProvider.GetQueueName(queue), false); if (msg != null) { var data = Encoding.UTF8.GetString(msg.Body); @@ -76,7 +82,7 @@ public void Dispose() public async Task Start() { - _connection = _connectionFactory.CreateConnection("Workflow-Core"); + _connection = _rabbitMqConnectionFactory(_serviceProvider, "Workflow-Core"); } public async Task Stop() @@ -88,18 +94,6 @@ public async Task Stop() } } - private string GetQueueName(QueueType queue) - { - switch (queue) - { - case QueueType.Workflow: - return "wfc.workflow_queue"; - case QueueType.Event: - return "wfc.event_queue"; - } - return null; - } - } #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } diff --git a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj index 5c75e91fb..3ef1064ae 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj @@ -2,9 +2,8 @@ Workflow Core RabbitMQ queue provider - 1.1.0 Daniel Gerlag - netstandard1.3 + netstandard2.0 WorkflowCore.QueueProviders.RabbitMQ WorkflowCore.QueueProviders.RabbitMQ workflow;.NET;Core;state machine;WorkflowCore;RabbitMQ @@ -12,15 +11,10 @@ https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git https://github.com/danielgerlag/workflow-core.git - 1.6.1 - $(PackageTargetFallback);dnxcore50 false false false - 1.3.0 Queue provider for Workflow-core using RabbitMQ - 1.3.0.0 - 1.3.0.0 @@ -28,7 +22,7 @@ - + diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/IQueueConfigProvider.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/IQueueConfigProvider.cs new file mode 100644 index 000000000..0f69f5355 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/IQueueConfigProvider.cs @@ -0,0 +1,16 @@ +#region using + +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.SqlServer.Models; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Interfaces +{ + public interface IQueueConfigProvider + { + QueueConfig GetByQueue(QueueType queue); + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlCommandExecutor.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlCommandExecutor.cs new file mode 100644 index 000000000..68996f348 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlCommandExecutor.cs @@ -0,0 +1,18 @@ +#region using + +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Interfaces +{ + public interface ISqlCommandExecutor + { + Task ExecuteScalarAsync(SqlConnection cn, SqlTransaction tx, string cmdtext, params DbParameter[] parameters); + Task ExecuteCommandAsync(SqlConnection cn, SqlTransaction tx, string cmdtext, params DbParameter[] parameters); + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs new file mode 100644 index 000000000..19b173d5c --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace WorkflowCore.QueueProviders.SqlServer.Interfaces +{ + public interface ISqlServerQueueProviderMigrator + { + Task MigrateDbAsync(); + Task CreateDbAsync(); + } +} diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs new file mode 100644 index 000000000..c323b9d03 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs @@ -0,0 +1,22 @@ +using System; + +namespace WorkflowCore.QueueProviders.SqlServer.Models +{ + public class QueueConfig + { + public QueueConfig(string name) + { + MsgType = $"//workflow-core/{name}"; + InitiatorService = $"//workflow-core/initiator{name}Service"; + TargetService = $"//workflow-core/target{name}Service"; + ContractName = $"//workflow-core/{name}Contract"; + QueueName = $"//workflow-core/{name}Queue"; + } + + public string MsgType { get; } + public string InitiatorService { get; } + public string TargetService { get; } + public string ContractName { get; } + public string QueueName { get; } + } +} diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/README.md b/src/providers/WorkflowCore.QueueProviders.SqlServer/README.md new file mode 100644 index 000000000..2cdd0ba14 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/README.md @@ -0,0 +1,25 @@ +# SQL Server Service Broker queue provider for Workflow Core + +*Thank you to Roberto Paterlini for contributing this provider* + +Provides distributed worker support on [Workflow Core](../../../README.md) using [SQL Server Service Broker](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-service-broker). + +This makes it possible to have a cluster of nodes processing your workflows, along with a distributed lock manager. + +## Installing + +Install the NuGet package "WorkflowCore.QueueProviders.SqlServer" + +``` +PM> Install-Package WorkflowCore.QueueProviders.SqlServer -Pre +``` + +## Usage + +Use the .UseSqlServerBroker extension method when building your service provider. + +```C# +services.AddWorkflow(x => x.UseSqlServerBroker(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;", true, true)); + +``` + diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..56f4f7fc2 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +#region using + +using System; +using WorkflowCore.Models; +using WorkflowCore.QueueProviders.SqlServer; +using WorkflowCore.QueueProviders.SqlServer.Interfaces; +using WorkflowCore.QueueProviders.SqlServer.Services; + +#endregion + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + /// + /// Use SQL Server as a queue provider + /// + /// + /// + /// + public static WorkflowOptions UseSqlServerBroker(this WorkflowOptions options, string connectionString, bool canCreateDb, bool canMigrateDb) + { + options.Services.AddTransient(); + options.Services.AddTransient(); + options.Services.AddTransient(sp => new SqlServerQueueProviderMigrator(connectionString, sp.GetService(), sp.GetService())); + + var sqlOptions = new SqlServerQueueProviderOptions + { + ConnectionString = connectionString, + CanCreateDb = canCreateDb, + CanMigrateDb = canMigrateDb + }; + + options.UseQueueProvider(sp => + { + return new SqlServerQueueProvider(sqlOptions, sp.GetService(), sp.GetService(), sp.GetService()); + }); + + return options; + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs new file mode 100644 index 000000000..4cbcd7dac --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs @@ -0,0 +1,28 @@ +#region using + +using System; +using System.Collections.Generic; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.SqlServer.Interfaces; +using WorkflowCore.QueueProviders.SqlServer.Models; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Services +{ + /// + /// Build names for SSSB objects + /// + public class QueueConfigProvider : IQueueConfigProvider + { + private readonly Dictionary _queues = new Dictionary + { + [QueueType.Workflow] = new QueueConfig("workflow"), + [QueueType.Event] = new QueueConfig("event"), + [QueueType.Index] = new QueueConfig("indexq") + }; + + public QueueConfig GetByQueue(QueueType queue) => _queues[queue]; + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs new file mode 100644 index 000000000..5c77342fe --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs @@ -0,0 +1,44 @@ +#region using + +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.QueueProviders.SqlServer.Interfaces; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Services +{ + public class SqlCommandExecutor : ISqlCommandExecutor + { + public async Task ExecuteScalarAsync(SqlConnection cn, SqlTransaction tx, string cmdtext, params DbParameter[] parameters) + { + using (var cmd = cn.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = cmdtext; + + foreach (var param in parameters) + cmd.Parameters.Add(param); + + return (TResult)await cmd.ExecuteScalarAsync(); + } + } + + public async Task ExecuteCommandAsync(SqlConnection cn, SqlTransaction tx, string cmdtext, params DbParameter[] parameters) + { + using (var cmd = cn.CreateCommand()) + { + cmd.Transaction = tx; + cmd.CommandText = cmdtext; + + foreach (var param in parameters) + cmd.Parameters.Add(param); + + return await cmd.ExecuteNonQueryAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProvider.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProvider.cs new file mode 100644 index 000000000..c07ed2b76 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProvider.cs @@ -0,0 +1,138 @@ +#region using + +using System; +using System.Data.SqlClient; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.SqlServer.Interfaces; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Services +{ + public class SqlServerQueueProvider : IQueueProvider + { + private readonly string _connectionString; + + private readonly bool _canMigrateDb; + private readonly bool _canCreateDb; + + private readonly IQueueConfigProvider _config; + private readonly ISqlServerQueueProviderMigrator _migrator; + private readonly ISqlCommandExecutor _sqlCommandExecutor; + + private readonly string _queueWorkCommand; + private readonly string _dequeueWorkCommand; + + public SqlServerQueueProvider(SqlServerQueueProviderOptions opt, IQueueConfigProvider names, ISqlServerQueueProviderMigrator migrator, ISqlCommandExecutor sqlCommandExecutor) + { + _config = names; + _migrator = migrator; + _sqlCommandExecutor = sqlCommandExecutor; + _connectionString = opt.ConnectionString; + _canMigrateDb = opt.CanMigrateDb; + _canCreateDb = opt.CanCreateDb; + + IsDequeueBlocking = true; + + _queueWorkCommand = GetFromResource("QueueWork"); + _dequeueWorkCommand = GetFromResource("DequeueWork"); + } + + private static string GetFromResource(string file) + { + var resName = $"WorkflowCore.QueueProviders.SqlServer.SqlCommands.{file}.sql"; + + using (var reader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream(resName))) + { + return reader.ReadToEnd(); + } + } + + + public bool IsDequeueBlocking { get; } + +#pragma warning disable CS1998 + + public async Task Start() + { + if (_canCreateDb) await _migrator.CreateDbAsync(); + if (_canMigrateDb) await _migrator.MigrateDbAsync(); + } + + public async Task Stop() + { + // Do nothing + } + +#pragma warning restore CS1998 + + public void Dispose() + { + Stop().Wait(); + } + + /// + /// + /// Write a new id to the specified queue + /// + /// + /// + /// + public async Task QueueWork(string id, QueueType queue) + { + if (string.IsNullOrEmpty(id)) + throw new ArgumentNullException(nameof(id), "Param id must not be null"); + + SqlConnection cn = new SqlConnection(_connectionString); + try + { + await cn.OpenAsync(); + var par = _config.GetByQueue(queue); + + await _sqlCommandExecutor.ExecuteCommandAsync(cn, null, _queueWorkCommand, + new SqlParameter("@initiatorService", par.InitiatorService), + new SqlParameter("@targetService", par.TargetService), + new SqlParameter("@contractName", par.ContractName), + new SqlParameter("@msgType", par.MsgType), + new SqlParameter("@RequestMessage", id) + ); + } + finally + { + cn.Close(); + } + } + + /// + /// + /// Get an id from the specified queue. + /// + /// + /// cancellationToken + /// Next id from queue, null if no message arrives in one second. + public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) + { + SqlConnection cn = new SqlConnection(_connectionString); + try + { + await cn.OpenAsync(cancellationToken); + + var par = _config.GetByQueue(queue); + var sql = _dequeueWorkCommand.Replace("{queueName}", par.QueueName); + var msg = await _sqlCommandExecutor.ExecuteScalarAsync(cn, null, sql); + return msg is DBNull ? null : (string)msg; + + } + finally + { + cn.Close(); + } + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs new file mode 100644 index 000000000..18a27b920 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs @@ -0,0 +1,178 @@ +#region using + +using System; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.SqlServer.Interfaces; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer.Services +{ + + public class SqlServerQueueProviderMigrator : ISqlServerQueueProviderMigrator + { + private readonly string _connectionString; + + private readonly IQueueConfigProvider _configProvider; + private readonly ISqlCommandExecutor _sqlCommandExecutor; + + public SqlServerQueueProviderMigrator(string connectionString, IQueueConfigProvider configProvider, ISqlCommandExecutor sqlCommandExecutor) + { + _connectionString = connectionString; + _configProvider = configProvider; + _sqlCommandExecutor = sqlCommandExecutor; + } + + + #region Migrate + + public async Task MigrateDbAsync() + { + var cn = new SqlConnection(_connectionString); + await cn.OpenAsync(); + var tx = cn.BeginTransaction(); + try + { + var queueConfigurations = new[] + { + _configProvider.GetByQueue(QueueType.Workflow), + _configProvider.GetByQueue(QueueType.Event), + _configProvider.GetByQueue(QueueType.Index) + }; + + foreach (var item in queueConfigurations) + { + await CreateMessageType(cn, tx, item.MsgType); + + await CreateContract(cn, tx, item.ContractName, item.MsgType); + + await CreateQueue(cn, tx, item.QueueName); + + await CreateService(cn, tx, item.InitiatorService, item.QueueName, item.ContractName); + await CreateService(cn, tx, item.TargetService, item.QueueName, item.ContractName); + } + + tx.Commit(); + } + catch + { + tx.Rollback(); + throw; + } + finally + { + cn.Close(); + } + } + + private async Task CreateService(SqlConnection cn, SqlTransaction tx, string name, string queueName, string contractName) + { + var cmdtext = @"select name from sys.services where name=@name"; + var existing = await _sqlCommandExecutor.ExecuteScalarAsync(cn, tx, cmdtext, new SqlParameter("@name", name)); + + if (!string.IsNullOrEmpty(existing)) + return; + + await _sqlCommandExecutor.ExecuteCommandAsync(cn, tx, $"CREATE SERVICE [{name}] ON QUEUE [{queueName}]([{contractName}]);"); + } + + private async Task CreateQueue(SqlConnection cn, SqlTransaction tx, string queueName) + { + var cmdtext = @"select name from sys.service_queues where name=@name"; + var existing = await _sqlCommandExecutor.ExecuteScalarAsync(cn, tx, cmdtext, new SqlParameter("@name", queueName)); + + if (!string.IsNullOrEmpty(existing)) + return; + + await _sqlCommandExecutor.ExecuteCommandAsync(cn, tx, $"CREATE QUEUE [{queueName}];"); + } + + private async Task CreateContract(SqlConnection cn, SqlTransaction tx, string contractName, string messageName) + { + var cmdtext = @"select name from sys.service_contracts where name=@name"; + var existing = await _sqlCommandExecutor.ExecuteScalarAsync(cn, tx, cmdtext, new SqlParameter("@name", contractName)); + + if (!string.IsNullOrEmpty(existing)) + return; + + await _sqlCommandExecutor.ExecuteCommandAsync(cn, tx, $"CREATE CONTRACT [{contractName}] ( [{messageName}] SENT BY INITIATOR);"); + } + + private async Task CreateMessageType(SqlConnection cn, SqlTransaction tx, string message) + { + var cmdtext = @"select name from sys.service_message_types where name=@name"; + var existing = await _sqlCommandExecutor.ExecuteScalarAsync(cn, tx, cmdtext, new SqlParameter("@name", message)); + + if (!string.IsNullOrEmpty(existing)) + return; + + await _sqlCommandExecutor.ExecuteCommandAsync(cn, tx, $"CREATE MESSAGE TYPE [{message}] VALIDATION = NONE;"); + } + + #endregion + + public async Task CreateDbAsync() + { + var builder = new SqlConnectionStringBuilder(_connectionString); + var masterBuilder = new SqlConnectionStringBuilder(_connectionString); + masterBuilder.InitialCatalog = "master"; + + var masterCnStr = masterBuilder.ToString(); + + bool dbPresente; + var cn = new SqlConnection(masterCnStr); + await cn.OpenAsync(); + try + { + var cmd = cn.CreateCommand(); + cmd.CommandText = "select name from sys.databases where name = @dbname"; + cmd.Parameters.AddWithValue("@dbname", builder.InitialCatalog); + var found = await cmd.ExecuteScalarAsync(); + dbPresente = (found != null); + + if (!dbPresente) + { + var createCmd = cn.CreateCommand(); + createCmd.CommandText = "create database [" + builder.InitialCatalog + "]"; + await createCmd.ExecuteNonQueryAsync(); + } + } + finally + { + cn.Close(); + } + + await EnableBroker(masterCnStr, builder.InitialCatalog); + } + + private async Task EnableBroker(string masterCn, string db) + { + var cn = new SqlConnection(masterCn); + await cn.OpenAsync(); + + var isBrokerEnabled = await _sqlCommandExecutor.ExecuteScalarAsync(cn, null, @"select is_broker_enabled from sys.databases where name = @name", new SqlParameter("@name", db)); + + if (isBrokerEnabled) + return; + + var tx = cn.BeginTransaction(); + try + { + await _sqlCommandExecutor.ExecuteCommandAsync(cn, tx, $"ALTER DATABASE [{db}] SET ENABLE_BROKER;"); + tx.Commit(); + } + catch + { + tx.Rollback(); + throw; + } + finally + { + cn.Close(); + } + } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/DequeueWork.sql b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/DequeueWork.sql new file mode 100644 index 000000000..04d061d86 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/DequeueWork.sql @@ -0,0 +1,16 @@ +DECLARE @TargetDlgHandle UNIQUEIDENTIFIER +DECLARE @Message varbinary(max) +DECLARE @MessageName Sysname + +BEGIN TRAN; + +WAITFOR ( + RECEIVE TOP(1) + @TargetDlgHandle=Conversation_Handle + ,@Message=Message_Body + ,@MessageName=Message_Type_Name + FROM [{queueName}]), +TIMEOUT 1000; + +SELECT cast(@Message as nvarchar(max)) +COMMIT TRAN \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/QueueWork.sql b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/QueueWork.sql new file mode 100644 index 000000000..82f176e93 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlCommands/QueueWork.sql @@ -0,0 +1,14 @@ +DECLARE @InitDlgHandle UNIQUEIDENTIFIER +BEGIN TRAN + +BEGIN DIALOG @InitDlgHandle +FROM SERVICE @initiatorService +TO SERVICE @targetService +ON CONTRACT @contractName +WITH ENCRYPTION=OFF; + +SEND ON CONVERSATION @InitDlgHandle +MESSAGE TYPE @msgType +(@RequestMessage); + +COMMIT TRAN \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlServerQueueProviderOptions.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlServerQueueProviderOptions.cs new file mode 100644 index 000000000..1717739a8 --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/SqlServerQueueProviderOptions.cs @@ -0,0 +1,16 @@ +#region using + +using System; +using System.Linq; + +#endregion + +namespace WorkflowCore.QueueProviders.SqlServer +{ + public class SqlServerQueueProviderOptions + { + public string ConnectionString { get; set; } + public bool CanMigrateDb { get; set; } + public bool CanCreateDb { get; set; } + } +} \ No newline at end of file diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj b/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj new file mode 100644 index 000000000..c24b7d89e --- /dev/null +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj @@ -0,0 +1,32 @@ + + + + netstandard2.0 + Roberto Paterlini + Queue provider for Workflow-core using SQL Server Service Broker + + alpha + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Directory.Build.props b/src/samples/Directory.Build.props new file mode 100644 index 000000000..0f8bd594d --- /dev/null +++ b/src/samples/Directory.Build.props @@ -0,0 +1,7 @@ + + + net6.0;netcoreapp3.1 + latest + false + + \ No newline at end of file diff --git a/src/samples/WebApiSample/.dockerignore b/src/samples/WebApiSample/.dockerignore new file mode 100644 index 000000000..43e8ab1e3 --- /dev/null +++ b/src/samples/WebApiSample/.dockerignore @@ -0,0 +1,10 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +docker-compose.yml +docker-compose.*.yml +*/bin +*/obj diff --git a/src/samples/WebApiSample/Directory.Build.props b/src/samples/WebApiSample/Directory.Build.props new file mode 100644 index 000000000..b312f7757 --- /dev/null +++ b/src/samples/WebApiSample/Directory.Build.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/samples/WebApiSample/README.md b/src/samples/WebApiSample/README.md new file mode 100644 index 000000000..cbe406b03 --- /dev/null +++ b/src/samples/WebApiSample/README.md @@ -0,0 +1,76 @@ + +# Using with ASP.NET Core + +This sample will use `docker-compose` to fire up instances of MongoDB and Elasticsearch to which the sample application will connect. + +## How to configure within an ASP.NET Core application + +In your startup class, use the `AddWorkflow` extension method to configure workflow core services, and then register your workflows and start the host when you configure the app. +```c# +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + services.AddWorkflow(cfg => + { + cfg.UseMongoDB(@"mongodb://mongo:27017", "workflow"); + cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://elastic:9200")), "workflows"); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + + var host = app.ApplicationServices.GetService(); + host.RegisterWorkflow(); + host.Start(); + } +} +``` + +## Usage + +Now simply inject the services you require into your controllers +* IWorkflowController +* IWorkflowHost +* ISearchIndex +* IPersistenceProvider + +```c# +public class WorkflowsController : Controller +{ + private readonly IWorkflowController _workflowService; + private readonly IWorkflowRegistry _registry; + private readonly IPersistenceProvider _workflowStore; + private readonly ISearchIndex _searchService; + + public WorkflowsController(IWorkflowController workflowService, ISearchIndex searchService, IWorkflowRegistry registry, IPersistenceProvider workflowStore) + { + _workflowService = workflowService; + _workflowStore = workflowStore; + _registry = registry; + _searchService = searchService; + } + + public Task Suspend(string id) + { + return _workflowService.SuspendWorkflow(id); + } + + ... +} +``` \ No newline at end of file diff --git a/src/samples/WebApiSample/WebApiSample.sln b/src/samples/WebApiSample/WebApiSample.sln new file mode 100644 index 000000000..f2e46941d --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiSample", "WebApiSample\WebApiSample.csproj", "{14489389-A65D-4993-8DE2-F51701A5AF60}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{2D5D708D-7EA1-48A9-ABF0-64CCC6026435}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6545FD2F-EBA5-4C65-8257-2C1E34AD2FAA}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {14489389-A65D-4993-8DE2-F51701A5AF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14489389-A65D-4993-8DE2-F51701A5AF60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14489389-A65D-4993-8DE2-F51701A5AF60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14489389-A65D-4993-8DE2-F51701A5AF60}.Release|Any CPU.Build.0 = Release|Any CPU + {2D5D708D-7EA1-48A9-ABF0-64CCC6026435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D5D708D-7EA1-48A9-ABF0-64CCC6026435}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D5D708D-7EA1-48A9-ABF0-64CCC6026435}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D5D708D-7EA1-48A9-ABF0-64CCC6026435}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EEC447B3-434E-4573-A280-D4A6CC652274} + EndGlobalSection +EndGlobal diff --git a/src/samples/WebApiSample/WebApiSample/Controllers/EventsController.cs b/src/samples/WebApiSample/WebApiSample/Controllers/EventsController.cs new file mode 100644 index 000000000..68486e735 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Controllers/EventsController.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using WebApiSample.Workflows; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WebApiSample.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class EventsController : Controller + { + private readonly IWorkflowController _workflowService; + + public EventsController(IWorkflowController workflowService) + { + _workflowService = workflowService; + } + + [HttpPost("{eventName}/{eventKey}")] + public async Task Post(string eventName, string eventKey, [FromBody]MyDataClass eventData) + { + await _workflowService.PublishEvent(eventName, eventKey, eventData.Value1); + return Ok(); + } + + } +} diff --git a/src/samples/WebApiSample/WebApiSample/Controllers/WorkflowsController.cs b/src/samples/WebApiSample/WebApiSample/Controllers/WorkflowsController.cs new file mode 100644 index 000000000..b61d50d76 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Controllers/WorkflowsController.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; + +namespace WebApiSample.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class WorkflowsController : Controller + { + private readonly IWorkflowController _workflowService; + private readonly IWorkflowRegistry _registry; + private readonly IPersistenceProvider _workflowStore; + private readonly ISearchIndex _searchService; + + public WorkflowsController(IWorkflowController workflowService, ISearchIndex searchService, IWorkflowRegistry registry, IPersistenceProvider workflowStore) + { + _workflowService = workflowService; + _workflowStore = workflowStore; + _registry = registry; + _searchService = searchService; + } + + [HttpGet] + public async Task Get(string terms, WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take = 10) + { + var filters = new List(); + + if (status.HasValue) + filters.Add(StatusFilter.Equals(status.Value)); + + if (createdFrom.HasValue) + filters.Add(DateRangeFilter.After(x => x.CreateTime, createdFrom.Value)); + + if (createdTo.HasValue) + filters.Add(DateRangeFilter.Before(x => x.CreateTime, createdTo.Value)); + + if (!string.IsNullOrEmpty(type)) + filters.Add(ScalarFilter.Equals(x => x.WorkflowDefinitionId, type)); + + var result = await _searchService.Search(terms, skip, take, filters.ToArray()); + + return Json(result); + } + + [HttpGet("{id}")] + public async Task Get(string id) + { + var result = await _workflowStore.GetWorkflowInstance(id); + return Json(result); + } + + [HttpPost("{id}")] + [HttpPost("{id}/{version}")] + public async Task Post(string id, int? version, string reference, [FromBody]JObject data) + { + string workflowId = null; + var def = _registry.GetDefinition(id, version); + if (def == null) + return BadRequest(String.Format("Workflow defintion {0} for version {1} not found", id, version)); + + if ((data != null) && (def.DataType != null)) + { + var dataStr = JsonConvert.SerializeObject(data); + var dataObj = JsonConvert.DeserializeObject(dataStr, def.DataType); + workflowId = await _workflowService.StartWorkflow(id, version, dataObj, reference); + } + else + { + workflowId = await _workflowService.StartWorkflow(id, version, null, reference); + } + + return Ok(workflowId); + } + + [HttpPut("{id}/suspend")] + public Task Suspend(string id) + { + return _workflowService.SuspendWorkflow(id); + } + + [HttpPut("{id}/resume")] + public Task Resume(string id) + { + return _workflowService.ResumeWorkflow(id); + } + + [HttpDelete("{id}")] + public Task Terminate(string id) + { + return _workflowService.TerminateWorkflow(id); + } + } +} diff --git a/src/samples/WebApiSample/WebApiSample/Dockerfile b/src/samples/WebApiSample/WebApiSample/Dockerfile new file mode 100644 index 000000000..f3bec7528 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Dockerfile @@ -0,0 +1,19 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY WebApiSample/WebApiSample.csproj WebApiSample/ +RUN dotnet restore WebApiSample/WebApiSample.csproj +COPY . . +WORKDIR /src/WebApiSample +RUN dotnet build WebApiSample.csproj -c Release -o /app + +FROM build AS publish +RUN dotnet publish WebApiSample.csproj -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "WebApiSample.dll"] diff --git a/src/samples/WebApiSample/WebApiSample/Dockerfile.original b/src/samples/WebApiSample/WebApiSample/Dockerfile.original new file mode 100644 index 000000000..8c834bb3b --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Dockerfile.original @@ -0,0 +1,19 @@ +FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +WORKDIR /app +EXPOSE 80 + +FROM microsoft/dotnet:2.1-sdk AS build +WORKDIR /src +COPY ["WebApiSample/WebApiSample.csproj", "WebApiSample/"] +RUN dotnet restore "WebApiSample/WebApiSample.csproj" +COPY . . +WORKDIR "/src/WebApiSample" +RUN dotnet build "WebApiSample.csproj" -c Release -o /app + +FROM build AS publish +RUN dotnet publish "WebApiSample.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "WebApiSample.dll"] \ No newline at end of file diff --git a/src/samples/WebApiSample/WebApiSample/Program.cs b/src/samples/WebApiSample/WebApiSample/Program.cs new file mode 100644 index 000000000..df2e76437 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace WebApiSample +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/src/samples/WebApiSample/WebApiSample/Properties/launchSettings.json b/src/samples/WebApiSample/WebApiSample/Properties/launchSettings.json new file mode 100644 index 000000000..83c2f62d6 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52884", + "sslPort": 0 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApiSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger" + } + } +} \ No newline at end of file diff --git a/src/samples/WebApiSample/WebApiSample/Startup.cs b/src/samples/WebApiSample/WebApiSample/Startup.cs new file mode 100644 index 000000000..13a19ddd8 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Startup.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Nest; +using Swashbuckle.AspNetCore.Swagger; +using WebApiSample.Workflows; +using WorkflowCore.Interface; + +namespace WebApiSample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + + services.AddWorkflow(cfg => + { + cfg.UseMongoDB(@"mongodb://mongo:27017", "workflow"); + cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://elastic:9200")), "workflows"); + }); + + services.AddSwaggerGen(c => c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" })); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1")); + + app.UseMvc(); + + var host = app.ApplicationServices.GetService(); + host.RegisterWorkflow(); + host.Start(); + } + } +} diff --git a/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj b/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj new file mode 100644 index 000000000..e1454eb7d --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj @@ -0,0 +1,24 @@ + + + + Linux + ..\docker-compose.dcproj + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/WebApiSample/WebApiSample/Workflows/TestWorkflow.cs b/src/samples/WebApiSample/WebApiSample/Workflows/TestWorkflow.cs new file mode 100644 index 000000000..df01ae156 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/Workflows/TestWorkflow.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WebApiSample.Workflows +{ + public class TestWorkflow : IWorkflow + { + public string Id => "TestWorkflow"; + + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .WaitFor("MyEvent", (data, context) => context.Workflow.Id, data => DateTime.Now) + .Output(data => data.Value1, step => step.EventData) + .Then(context => Console.WriteLine("workflow complete")); + } + } + + public class MyDataClass + { + public string Value1 { get; set; } + } +} diff --git a/src/samples/WebApiSample/WebApiSample/appsettings.Development.json b/src/samples/WebApiSample/WebApiSample/appsettings.Development.json new file mode 100644 index 000000000..e203e9407 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/samples/WebApiSample/WebApiSample/appsettings.json b/src/samples/WebApiSample/WebApiSample/appsettings.json new file mode 100644 index 000000000..def9159a7 --- /dev/null +++ b/src/samples/WebApiSample/WebApiSample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/samples/WebApiSample/docker-compose.dcproj b/src/samples/WebApiSample/docker-compose.dcproj new file mode 100644 index 000000000..b3385d19e --- /dev/null +++ b/src/samples/WebApiSample/docker-compose.dcproj @@ -0,0 +1,18 @@ + + + + 2.1 + Linux + 2d5d708d-7ea1-48a9-abf0-64ccc6026435 + LaunchBrowser + {Scheme}://localhost:{ServicePort}/api/workflows + webapisample + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/src/samples/WebApiSample/docker-compose.override.yml b/src/samples/WebApiSample/docker-compose.override.yml new file mode 100644 index 000000000..3dcfad473 --- /dev/null +++ b/src/samples/WebApiSample/docker-compose.override.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + webapisample: + environment: + - ASPNETCORE_ENVIRONMENT=Development + ports: + - "80" diff --git a/src/samples/WebApiSample/docker-compose.yml b/src/samples/WebApiSample/docker-compose.yml new file mode 100644 index 000000000..4f46b919a --- /dev/null +++ b/src/samples/WebApiSample/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.4' + +services: + webapisample: + image: ${DOCKER_REGISTRY-}webapisample + build: + context: . + dockerfile: WebApiSample/Dockerfile + + elastic: + image: elasticsearch:6.5.4 + expose: + - 9200 + + mongo: + image: mongo:3.6 + expose: + - 27017 diff --git a/src/samples/WorkflowCore.Sample01/HelloWorldWorkflow.cs b/src/samples/WorkflowCore.Sample01/HelloWorldWorkflow.cs index 74887e1e5..157966f18 100644 --- a/src/samples/WorkflowCore.Sample01/HelloWorldWorkflow.cs +++ b/src/samples/WorkflowCore.Sample01/HelloWorldWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Sample01.Steps; @@ -13,6 +11,7 @@ public class HelloWorldWorkflow : IWorkflow public void Build(IWorkflowBuilder builder) { builder + .UseDefaultErrorBehavior(WorkflowErrorHandling.Suspend) .StartWith() .Then(); } diff --git a/src/samples/WorkflowCore.Sample01/Program.cs b/src/samples/WorkflowCore.Sample01/Program.cs index 2f1e6105c..6577f5ba0 100644 --- a/src/samples/WorkflowCore.Sample01/Program.cs +++ b/src/samples/WorkflowCore.Sample01/Program.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Sample01.Steps; -using WorkflowCore.Services; namespace WorkflowCore.Sample01 { @@ -22,7 +18,7 @@ public static void Main(string[] args) host.RegisterWorkflow(); host.Start(); - host.StartWorkflow("HelloWorld", 1, null); + host.StartWorkflow("HelloWorld"); Console.ReadLine(); host.Stop(); @@ -39,9 +35,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } diff --git a/src/samples/WorkflowCore.Sample01/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample01/Properties/AssemblyInfo.cs index 32a0f2b49..be2722eae 100644 --- a/src/samples/WorkflowCore.Sample01/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample01/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample01/Steps/GoodbyeWorld.cs b/src/samples/WorkflowCore.Sample01/Steps/GoodbyeWorld.cs index 8c94778f7..048b57bb1 100644 --- a/src/samples/WorkflowCore.Sample01/Steps/GoodbyeWorld.cs +++ b/src/samples/WorkflowCore.Sample01/Steps/GoodbyeWorld.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample01/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample01/Steps/HelloWorld.cs index a5a09534a..415ad08f2 100644 --- a/src/samples/WorkflowCore.Sample01/Steps/HelloWorld.cs +++ b/src/samples/WorkflowCore.Sample01/Steps/HelloWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj b/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj index 152ccf433..d20a6bc51 100644 --- a/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj +++ b/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj @@ -1,12 +1,9 @@  - netcoreapp1.0 WorkflowCore.Sample01 Exe WorkflowCore.Sample01 - 1.0.3 - $(PackageTargetFallback);dnxcore50 false false false @@ -18,12 +15,12 @@ - - - - - - + + + + + + diff --git a/src/samples/WorkflowCore.Sample02/Program.cs b/src/samples/WorkflowCore.Sample02/Program.cs index 49153381f..22dd3c196 100644 --- a/src/samples/WorkflowCore.Sample02/Program.cs +++ b/src/samples/WorkflowCore.Sample02/Program.cs @@ -1,11 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Services; namespace WorkflowCore.Sample02 @@ -21,7 +17,7 @@ public static void Main(string[] args) host.RegisterWorkflow(); host.Start(); - host.StartWorkflow("Simple Decision Workflow", 1, null); + host.StartWorkflow("Simple Decision Workflow"); Console.ReadLine(); host.Stop(); @@ -35,9 +31,6 @@ private static IServiceProvider ConfigureServices() services.AddWorkflow(); var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample02/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample02/Properties/AssemblyInfo.cs index 5b02eba4c..f677ae82a 100644 --- a/src/samples/WorkflowCore.Sample02/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample02/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample02/SimpleDecisionWorkflow.cs b/src/samples/WorkflowCore.Sample02/SimpleDecisionWorkflow.cs index b32fa3c55..91a0323f2 100644 --- a/src/samples/WorkflowCore.Sample02/SimpleDecisionWorkflow.cs +++ b/src/samples/WorkflowCore.Sample02/SimpleDecisionWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Sample02.Steps; diff --git a/src/samples/WorkflowCore.Sample02/Steps/CustomMessage.cs b/src/samples/WorkflowCore.Sample02/Steps/CustomMessage.cs index 2514a7384..efb3fb662 100644 --- a/src/samples/WorkflowCore.Sample02/Steps/CustomMessage.cs +++ b/src/samples/WorkflowCore.Sample02/Steps/CustomMessage.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample02/Steps/GoodbyeWorld.cs b/src/samples/WorkflowCore.Sample02/Steps/GoodbyeWorld.cs index 8e09fed44..c082e57c9 100644 --- a/src/samples/WorkflowCore.Sample02/Steps/GoodbyeWorld.cs +++ b/src/samples/WorkflowCore.Sample02/Steps/GoodbyeWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample02/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample02/Steps/HelloWorld.cs index 151d92a3e..884b01525 100644 --- a/src/samples/WorkflowCore.Sample02/Steps/HelloWorld.cs +++ b/src/samples/WorkflowCore.Sample02/Steps/HelloWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample02/Steps/RandomOutput.cs b/src/samples/WorkflowCore.Sample02/Steps/RandomOutput.cs index 90bbdbddf..80c285161 100644 --- a/src/samples/WorkflowCore.Sample02/Steps/RandomOutput.cs +++ b/src/samples/WorkflowCore.Sample02/Steps/RandomOutput.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj b/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj index af734aba9..be4e85c2d 100644 --- a/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj +++ b/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj @@ -1,28 +1,21 @@  - netcoreapp1.0 WorkflowCore.Sample02 Exe WorkflowCore.Sample02 - 1.0.3 - $(PackageTargetFallback);dnxcore50 false false false - + + - - - - - - + diff --git a/src/samples/WorkflowCore.Sample03/MyDataClass.cs b/src/samples/WorkflowCore.Sample03/MyDataClass.cs index de0f99f81..3f0cead60 100644 --- a/src/samples/WorkflowCore.Sample03/MyDataClass.cs +++ b/src/samples/WorkflowCore.Sample03/MyDataClass.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Sample03 { @@ -12,5 +10,6 @@ public class MyDataClass public int Value2 { get; set; } public int Value3 { get; set; } + } } diff --git a/src/samples/WorkflowCore.Sample03/PassingDataWorkflow.cs b/src/samples/WorkflowCore.Sample03/PassingDataWorkflow.cs index c96f3dc03..56307b42e 100644 --- a/src/samples/WorkflowCore.Sample03/PassingDataWorkflow.cs +++ b/src/samples/WorkflowCore.Sample03/PassingDataWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Sample03.Steps; diff --git a/src/samples/WorkflowCore.Sample03/PassingDataWorkflow2.cs b/src/samples/WorkflowCore.Sample03/PassingDataWorkflow2.cs new file mode 100644 index 000000000..500c8ad74 --- /dev/null +++ b/src/samples/WorkflowCore.Sample03/PassingDataWorkflow2.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Sample03.Steps; + +namespace WorkflowCore.Sample03 +{ + public class PassingDataWorkflow2 : IWorkflow> + { + public void Build(IWorkflowBuilder> builder) + { + builder + .StartWith(context => + { + Console.WriteLine("Starting workflow..."); + return ExecutionResult.Next(); + }) + .Then() + .Input(step => step.Input1, data => data["Value1"]) + .Input(step => step.Input2, data => data["Value2"]) + .Output((step, data) => data["Value3"] = step.Output) + .Then() + .Name("Print custom message") + .Input(step => step.Message, data => "The answer is " + data["Value3"].ToString()) + .Then(context => + { + Console.WriteLine("Workflow complete"); + return ExecutionResult.Next(); + }); + } + + public string Id => "PassingDataWorkflow2"; + + public int Version => 1; + + } +} diff --git a/src/samples/WorkflowCore.Sample03/Program.cs b/src/samples/WorkflowCore.Sample03/Program.cs index d778ba6ba..813e5dfd7 100644 --- a/src/samples/WorkflowCore.Sample03/Program.cs +++ b/src/samples/WorkflowCore.Sample03/Program.cs @@ -1,11 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Services; namespace WorkflowCore.Sample03 @@ -19,6 +16,7 @@ public static void Main(string[] args) //start the workflow host var host = serviceProvider.GetService(); host.RegisterWorkflow(); + host.RegisterWorkflow>(); host.Start(); var initialData = new MyDataClass @@ -27,7 +25,16 @@ public static void Main(string[] args) Value2 = 3 }; - host.StartWorkflow("PassingDataWorkflow", 1, initialData); + //host.StartWorkflow("PassingDataWorkflow", 1, initialData); + + + var initialData2 = new Dictionary + { + ["Value1"] = 7, + ["Value2"] = 2 + }; + + host.StartWorkflow("PassingDataWorkflow2", 1, initialData2); Console.ReadLine(); host.Stop(); @@ -42,9 +49,6 @@ private static IServiceProvider ConfigureServices() //services.AddWorkflow(x => x.UseSqlServer(@"Server=.\SQLEXPRESS;Database=WorkflowCore;Trusted_Connection=True;", true, true)); var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample03/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample03/Properties/AssemblyInfo.cs index 324e13bc7..3965ce4cd 100644 --- a/src/samples/WorkflowCore.Sample03/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample03/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample03/README.md b/src/samples/WorkflowCore.Sample03/README.md index d75950de6..02e8f730a 100644 --- a/src/samples/WorkflowCore.Sample03/README.md +++ b/src/samples/WorkflowCore.Sample03/README.md @@ -50,7 +50,7 @@ public class PassingDataWorkflow : IWorkflow .Input(step => step.Message, data => "The answer is " + data.Value3.ToString()) .Then(context => { - Console.WriteLine("Workflow comeplete"); + Console.WriteLine("Workflow complete"); return ExecutionResult.Next(); }); } diff --git a/src/samples/WorkflowCore.Sample03/Steps/AddNumbers.cs b/src/samples/WorkflowCore.Sample03/Steps/AddNumbers.cs index 209229812..a8209d227 100644 --- a/src/samples/WorkflowCore.Sample03/Steps/AddNumbers.cs +++ b/src/samples/WorkflowCore.Sample03/Steps/AddNumbers.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using WorkflowCore.Interface; diff --git a/src/samples/WorkflowCore.Sample03/Steps/CustomMessage.cs b/src/samples/WorkflowCore.Sample03/Steps/CustomMessage.cs index ce83fcdc0..810092b30 100644 --- a/src/samples/WorkflowCore.Sample03/Steps/CustomMessage.cs +++ b/src/samples/WorkflowCore.Sample03/Steps/CustomMessage.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample03/Steps/GoodbyeWorld.cs b/src/samples/WorkflowCore.Sample03/Steps/GoodbyeWorld.cs index 4a700c768..c7f985288 100644 --- a/src/samples/WorkflowCore.Sample03/Steps/GoodbyeWorld.cs +++ b/src/samples/WorkflowCore.Sample03/Steps/GoodbyeWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample03/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample03/Steps/HelloWorld.cs index ccd93045c..e156b13ba 100644 --- a/src/samples/WorkflowCore.Sample03/Steps/HelloWorld.cs +++ b/src/samples/WorkflowCore.Sample03/Steps/HelloWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample03/WorkflowCore.Sample03.csproj b/src/samples/WorkflowCore.Sample03/WorkflowCore.Sample03.csproj index 5c8dcfaa8..180c03454 100644 --- a/src/samples/WorkflowCore.Sample03/WorkflowCore.Sample03.csproj +++ b/src/samples/WorkflowCore.Sample03/WorkflowCore.Sample03.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Sample03 Exe WorkflowCore.Sample03 @@ -16,12 +15,8 @@ - - - - - - + + diff --git a/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs b/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs index 20fd48f69..b78eaa751 100644 --- a/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs +++ b/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Sample04.Steps; @@ -19,9 +17,9 @@ public void Build(IWorkflowBuilder builder) builder .StartWith(context => ExecutionResult.Next()) .WaitFor("MyEvent", (data, context) => context.Workflow.Id, data => DateTime.Now) - .Output(data => data.StrValue, step => step.EventData) + .Output(data => data.Value1, step => step.EventData) .Then() - .Input(step => step.Message, data => "The data from the event is " + data.StrValue) + .Input(step => step.Message, data => "The data from the event is " + data.Value1) .Then(context => Console.WriteLine("workflow complete")); } } diff --git a/src/samples/WorkflowCore.Sample04/MyDataClass.cs b/src/samples/WorkflowCore.Sample04/MyDataClass.cs index dda008f2c..719459c33 100644 --- a/src/samples/WorkflowCore.Sample04/MyDataClass.cs +++ b/src/samples/WorkflowCore.Sample04/MyDataClass.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Sample04 { public class MyDataClass { - public string StrValue { get; set; } + public string Value1 { get; set; } } } diff --git a/src/samples/WorkflowCore.Sample04/Program.cs b/src/samples/WorkflowCore.Sample04/Program.cs index db44c287a..0e1895d44 100644 --- a/src/samples/WorkflowCore.Sample04/Program.cs +++ b/src/samples/WorkflowCore.Sample04/Program.cs @@ -1,15 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MongoDB.Driver; -//using RabbitMQ.Client; using StackExchange.Redis; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Persistence.MongoDB.Services; -using WorkflowCore.Services; namespace WorkflowCore.Sample04 { @@ -48,7 +41,7 @@ private static IServiceProvider ConfigureServices() //services.AddWorkflow(x => //{ - // x.UseAzureSyncronization(@"UseDevelopmentStorage=true"); + // x.UseAzureSynchronization(@"UseDevelopmentStorage=true"); // x.UseMongoDB(@"mongodb://localhost:27017", "workflow9999"); //}); @@ -58,7 +51,23 @@ private static IServiceProvider ConfigureServices() // x.UseSqlServerLocking(@"Server=.\SQLEXPRESS;Database=WorkflowCore;Trusted_Connection=True;"); //}); - //redis = ConnectionMultiplexer.Connect("127.0.0.1"); + //services.AddWorkflow(cfg => + //{ + // var ddbConfig = new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }; + + // cfg.UseAwsDynamoPersistence(new EnvironmentVariablesAWSCredentials(), ddbConfig, "sample4"); + // cfg.UseAwsDynamoLocking(new EnvironmentVariablesAWSCredentials(), ddbConfig, "workflow-core-locks"); + // cfg.UseAwsSimpleQueueService(new EnvironmentVariablesAWSCredentials(), new AmazonSQSConfig() { RegionEndpoint = RegionEndpoint.USWest2 }); + //}); + + //services.AddWorkflow(cfg => + //{ + // cfg.UseRedisPersistence("localhost:6379", "sample4"); + // cfg.UseRedisLocking("localhost:6379"); + // cfg.UseRedisQueues("localhost:6379", "sample4"); + // cfg.UseRedisEventHub("localhost:6379", "channel1"); + //}); + //services.AddWorkflow(x => //{ // x.UseMongoDB(@"mongodb://192.168.0.12:27017", "workflow"); @@ -69,9 +78,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } diff --git a/src/samples/WorkflowCore.Sample04/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample04/Properties/AssemblyInfo.cs index 2bc13d42e..e51bcce10 100644 --- a/src/samples/WorkflowCore.Sample04/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample04/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample04/Steps/CustomMessage.cs b/src/samples/WorkflowCore.Sample04/Steps/CustomMessage.cs index 4d96503fb..678f22731 100644 --- a/src/samples/WorkflowCore.Sample04/Steps/CustomMessage.cs +++ b/src/samples/WorkflowCore.Sample04/Steps/CustomMessage.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj b/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj index df6381285..5940a8e00 100644 --- a/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj +++ b/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj @@ -1,10 +1,9 @@  - netcoreapp2.0 WorkflowCore.Sample04 Exe - WorkflowCore.Sample04 + WorkflowCore.Sample04 false false false @@ -12,22 +11,21 @@ + + + - - - - - - - + + + diff --git a/src/samples/WorkflowCore.Sample05/DeferSampleWorkflow.cs b/src/samples/WorkflowCore.Sample05/DeferSampleWorkflow.cs index 08380429c..ff9223ae9 100644 --- a/src/samples/WorkflowCore.Sample05/DeferSampleWorkflow.cs +++ b/src/samples/WorkflowCore.Sample05/DeferSampleWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Sample05.Steps; diff --git a/src/samples/WorkflowCore.Sample05/Program.cs b/src/samples/WorkflowCore.Sample05/Program.cs index ef9f0d265..a0e727633 100644 --- a/src/samples/WorkflowCore.Sample05/Program.cs +++ b/src/samples/WorkflowCore.Sample05/Program.cs @@ -1,13 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MongoDB.Driver; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Persistence.MongoDB.Services; -using WorkflowCore.Services; namespace WorkflowCore.Sample05 { @@ -22,7 +16,7 @@ public static void Main(string[] args) host.RegisterWorkflow(); host.Start(); - host.StartWorkflow("DeferSampleWorkflow", 1, null); + host.StartWorkflow("DeferSampleWorkflow", 1, null, null); Console.ReadLine(); host.Stop(); @@ -38,9 +32,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample05/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample05/Properties/AssemblyInfo.cs index 660ed42d2..a510661da 100644 --- a/src/samples/WorkflowCore.Sample05/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample05/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample05/Steps/SleepStep.cs b/src/samples/WorkflowCore.Sample05/Steps/SleepStep.cs index f2d1ef94c..c11cfb6f7 100644 --- a/src/samples/WorkflowCore.Sample05/Steps/SleepStep.cs +++ b/src/samples/WorkflowCore.Sample05/Steps/SleepStep.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj b/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj index c4a14a0e1..bbfd7f7c7 100644 --- a/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj +++ b/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj @@ -1,7 +1,6 @@  - netcoreapp1.0 WorkflowCore.Sample05 Exe WorkflowCore.Sample05 @@ -16,12 +15,7 @@ - - - - - - + diff --git a/src/samples/WorkflowCore.Sample06/MultipleOutcomeWorkflow.cs b/src/samples/WorkflowCore.Sample06/MultipleOutcomeWorkflow.cs index 8b792d968..5f7661455 100644 --- a/src/samples/WorkflowCore.Sample06/MultipleOutcomeWorkflow.cs +++ b/src/samples/WorkflowCore.Sample06/MultipleOutcomeWorkflow.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Sample06.Steps; diff --git a/src/samples/WorkflowCore.Sample06/Program.cs b/src/samples/WorkflowCore.Sample06/Program.cs index 1805377fb..4612f92cc 100644 --- a/src/samples/WorkflowCore.Sample06/Program.cs +++ b/src/samples/WorkflowCore.Sample06/Program.cs @@ -1,13 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MongoDB.Driver; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Persistence.MongoDB.Services; -using WorkflowCore.Services; namespace WorkflowCore.Sample06 { @@ -22,7 +16,7 @@ public static void Main(string[] args) host.RegisterWorkflow(); host.Start(); - host.StartWorkflow("MultipleOutcomeWorkflow", 1, null); + host.StartWorkflow("MultipleOutcomeWorkflow", 1, null, null); Console.ReadLine(); host.Stop(); @@ -38,9 +32,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample06/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample06/Properties/AssemblyInfo.cs index 20a134e87..abc7e334f 100644 --- a/src/samples/WorkflowCore.Sample06/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample06/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample06/Steps/RandomOutput.cs b/src/samples/WorkflowCore.Sample06/Steps/RandomOutput.cs index 79587280d..dc686abec 100644 --- a/src/samples/WorkflowCore.Sample06/Steps/RandomOutput.cs +++ b/src/samples/WorkflowCore.Sample06/Steps/RandomOutput.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample06/Steps/TaskA.cs b/src/samples/WorkflowCore.Sample06/Steps/TaskA.cs index 67d20cf76..17b8cd6d3 100644 --- a/src/samples/WorkflowCore.Sample06/Steps/TaskA.cs +++ b/src/samples/WorkflowCore.Sample06/Steps/TaskA.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample06/Steps/TaskB.cs b/src/samples/WorkflowCore.Sample06/Steps/TaskB.cs index cd2554eae..c230fb836 100644 --- a/src/samples/WorkflowCore.Sample06/Steps/TaskB.cs +++ b/src/samples/WorkflowCore.Sample06/Steps/TaskB.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample06/Steps/TaskC.cs b/src/samples/WorkflowCore.Sample06/Steps/TaskC.cs index 39770f8e4..ec840b843 100644 --- a/src/samples/WorkflowCore.Sample06/Steps/TaskC.cs +++ b/src/samples/WorkflowCore.Sample06/Steps/TaskC.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample06/Steps/TaskD.cs b/src/samples/WorkflowCore.Sample06/Steps/TaskD.cs index 2f40005ea..cd4ff1940 100644 --- a/src/samples/WorkflowCore.Sample06/Steps/TaskD.cs +++ b/src/samples/WorkflowCore.Sample06/Steps/TaskD.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj b/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj index 12160f810..47e03b0ee 100644 --- a/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj +++ b/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj @@ -1,7 +1,6 @@  - netcoreapp1.0 WorkflowCore.Sample06 Exe WorkflowCore.Sample06 @@ -16,12 +15,7 @@ - - - - - - + diff --git a/src/samples/WorkflowCore.Sample07/Program.cs b/src/samples/WorkflowCore.Sample07/Program.cs index 57b417b72..b89645580 100644 --- a/src/samples/WorkflowCore.Sample07/Program.cs +++ b/src/samples/WorkflowCore.Sample07/Program.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; namespace WorkflowCore.Sample07 diff --git a/src/samples/WorkflowCore.Sample07/Startup.cs b/src/samples/WorkflowCore.Sample07/Startup.cs index 6c8f619b8..80264f6d2 100644 --- a/src/samples/WorkflowCore.Sample07/Startup.cs +++ b/src/samples/WorkflowCore.Sample07/Startup.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; @@ -24,7 +21,7 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(); + //loggerFactory.AddConsole(); //start the workflow host var host = app.ApplicationServices.GetService(); diff --git a/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj b/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj index 1fd3f91b6..1cc78f66f 100644 --- a/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj +++ b/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 true WorkflowCore.Sample07 Exe @@ -23,10 +22,9 @@ - - - - + + + diff --git a/src/samples/WorkflowCore.Sample07/web.config b/src/samples/WorkflowCore.Sample07/web.config index dc0514fca..b56a4065f 100644 --- a/src/samples/WorkflowCore.Sample07/web.config +++ b/src/samples/WorkflowCore.Sample07/web.config @@ -1,14 +1,14 @@  - - - + - + + + - + \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample08/HumanWorkflow.cs b/src/samples/WorkflowCore.Sample08/HumanWorkflow.cs index b6e00849d..b2b48b1c8 100644 --- a/src/samples/WorkflowCore.Sample08/HumanWorkflow.cs +++ b/src/samples/WorkflowCore.Sample08/HumanWorkflow.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Users.Models; namespace WorkflowCore.Sample08 { diff --git a/src/samples/WorkflowCore.Sample08/Program.cs b/src/samples/WorkflowCore.Sample08/Program.cs index 1c4ba2036..970834c88 100644 --- a/src/samples/WorkflowCore.Sample08/Program.cs +++ b/src/samples/WorkflowCore.Sample08/Program.cs @@ -1,10 +1,7 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using WorkflowCore.Interface; namespace WorkflowCore.Sample08 @@ -40,14 +37,22 @@ public static void Main(string[] args) } //Thread.Sleep(500); - - Console.ReadLine(); + + var input = Console.ReadLine(); Console.WriteLine(); - Console.WriteLine("Choosing " + item.Options.First().Key); - host.PublishUserAction(openItems.First().Key, @"domain\john", item.Options.First().Value).Wait(); - } + string key = item.Key; + string value = item.Options.Single(x => x.Value == input).Value; + + Console.WriteLine("Choosing key:" + key + " value:" + value); + host.PublishUserAction(key, @"domain\john", value).Wait(); + } + Thread.Sleep(1000); + Console.WriteLine("Open user actions left:" + host.GetOpenUserActions(workflowId).Count().ToString()); + timer.Dispose(); + timer = null; + Console.WriteLine("Workflow ended."); Console.ReadLine(); host.Stop(); } @@ -79,12 +84,9 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } - + } } diff --git a/src/samples/WorkflowCore.Sample08/Properties/AssemblyInfo.cs b/src/samples/WorkflowCore.Sample08/Properties/AssemblyInfo.cs index 079de7d9e..2f6266e9f 100644 --- a/src/samples/WorkflowCore.Sample08/Properties/AssemblyInfo.cs +++ b/src/samples/WorkflowCore.Sample08/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj b/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj index a70da3b23..f6ae3bcba 100644 --- a/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj +++ b/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Sample08 Exe WorkflowCore.Sample08 @@ -21,12 +20,7 @@ - - - - - - + diff --git a/src/samples/WorkflowCore.Sample09/ForEachWorkflow.cs b/src/samples/WorkflowCore.Sample09/ForEachWorkflow.cs index 4e525c3de..ba462bac1 100644 --- a/src/samples/WorkflowCore.Sample09/ForEachWorkflow.cs +++ b/src/samples/WorkflowCore.Sample09/ForEachWorkflow.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample09 { @@ -15,7 +13,7 @@ public void Build(IWorkflowBuilder builder) { builder .StartWith() - .ForEach(data => new List() { 1, 2, 3, 4 }) + .ForEach(data => new List { 1, 2, 3, 4 }) .Do(x => x .StartWith() .Input(step => step.Item, (data, context) => context.Item) diff --git a/src/samples/WorkflowCore.Sample09/Program.cs b/src/samples/WorkflowCore.Sample09/Program.cs index 4455f86aa..f98fc1547 100644 --- a/src/samples/WorkflowCore.Sample09/Program.cs +++ b/src/samples/WorkflowCore.Sample09/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using WorkflowCore.Interface; @@ -38,9 +37,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } diff --git a/src/samples/WorkflowCore.Sample09/README.md b/src/samples/WorkflowCore.Sample09/README.md index 9c7b54bc8..a9b2fbe2a 100644 --- a/src/samples/WorkflowCore.Sample09/README.md +++ b/src/samples/WorkflowCore.Sample09/README.md @@ -1,4 +1,4 @@ -# Foreach sample +# Foreach Parallel sample Illustrates how to implement a parallel foreach within your workflow. @@ -14,7 +14,7 @@ builder .Then(); ``` -or get the collectioin from workflow data. +or get the collection from workflow data. ```c# builder diff --git a/src/samples/WorkflowCore.Sample09/Steps/DisplayContext.cs b/src/samples/WorkflowCore.Sample09/Steps/DisplayContext.cs index 9782a32bc..0d9815038 100644 --- a/src/samples/WorkflowCore.Sample09/Steps/DisplayContext.cs +++ b/src/samples/WorkflowCore.Sample09/Steps/DisplayContext.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample09/Steps/DoSomething.cs b/src/samples/WorkflowCore.Sample09/Steps/DoSomething.cs index 57d0f556a..9636c486c 100644 --- a/src/samples/WorkflowCore.Sample09/Steps/DoSomething.cs +++ b/src/samples/WorkflowCore.Sample09/Steps/DoSomething.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample09/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample09/Steps/SayGoodbye.cs index 9836d5f44..c3aaec908 100644 --- a/src/samples/WorkflowCore.Sample09/Steps/SayGoodbye.cs +++ b/src/samples/WorkflowCore.Sample09/Steps/SayGoodbye.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample09/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample09/Steps/SayHello.cs index 94ba8768a..653b28af1 100644 --- a/src/samples/WorkflowCore.Sample09/Steps/SayHello.cs +++ b/src/samples/WorkflowCore.Sample09/Steps/SayHello.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj b/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj index 0a50fc7c5..e7c7206ec 100644 --- a/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj +++ b/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj @@ -2,13 +2,10 @@ Exe - netcoreapp2.0 - - - + diff --git a/src/samples/WorkflowCore.Sample09s/ForEachSyncWorkflow.cs b/src/samples/WorkflowCore.Sample09s/ForEachSyncWorkflow.cs new file mode 100644 index 000000000..2c1a335a7 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/ForEachSyncWorkflow.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; + +namespace WorkflowCore.Sample09s +{ + public class ForEachSyncWorkflow : IWorkflow + { + public string Id => "ForeachSync"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .ForEach(data => new List { 1, 2, 3, 4 }, data => false) + .Do(x => x + .StartWith() + .Input(step => step.Item, (data, context) => context.Item) + .Then()) + .Then(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample09s/Program.cs b/src/samples/WorkflowCore.Sample09s/Program.cs new file mode 100644 index 000000000..09e04abb8 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/Program.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using WorkflowCore.Interface; + +namespace WorkflowCore.Sample09s +{ + class Program + { + public static void Main(string[] args) + { + IServiceProvider serviceProvider = ConfigureServices(); + + //start the workflow host + var host = serviceProvider.GetService(); + host.RegisterWorkflow(); + host.Start(); + + Console.WriteLine("Starting workflow..."); + string workflowId = host.StartWorkflow("Foreach").Result; + + + Console.ReadLine(); + host.Stop(); + } + + private static IServiceProvider ConfigureServices() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + services.AddWorkflow(); + //services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow-test002")); + //services.AddWorkflow(x => x.UseSqlServer(@"Server=.;Database=WorkflowCore3;Trusted_Connection=True;", true, true)); + //services.AddWorkflow(x => x.UseSqlite(@"Data Source=database2.db;", true)); + //services.AddWorkflow(x => x.UsePostgreSQL(@"Server=127.0.0.1;Port=32768;Database=workflow_test002;User Id=postgres;", true, true)); + + + var serviceProvider = services.BuildServiceProvider(); + + return serviceProvider; + } + + } +} \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample09s/README.md b/src/samples/WorkflowCore.Sample09s/README.md new file mode 100644 index 000000000..ea6efae02 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/README.md @@ -0,0 +1,29 @@ +# Foreach Sync sample + +Illustrates how to implement a synchronous foreach within your workflow. + + +```c# +builder + .StartWith() + .ForEach(data => new List() { 1, 2, 3, 4 }, data => false) + .Do(x => x + .StartWith() + .Input(step => step.Item, (data, context) => context.Item) + .Then()) + .Then(); +``` + +or get the collection from workflow data. + +```c# +builder + .StartWith() + .ForEach(data => data.MyCollection, data => false) + .Do(x => x + .StartWith() + .Input(step => step.Item, (data, context) => context.Item) + .Then()) + .Then(); + +``` diff --git a/src/samples/WorkflowCore.Sample09s/Steps/DisplayContext.cs b/src/samples/WorkflowCore.Sample09s/Steps/DisplayContext.cs new file mode 100644 index 000000000..012cda0c6 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/Steps/DisplayContext.cs @@ -0,0 +1,18 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample09s +{ + public class DisplayContext : StepBody + { + + public object Item { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine($"Working on item {Item}"); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample09s/Steps/DoSomething.cs b/src/samples/WorkflowCore.Sample09s/Steps/DoSomething.cs new file mode 100644 index 000000000..859a13eab --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/Steps/DoSomething.cs @@ -0,0 +1,15 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample09s +{ + public class DoSomething : StepBody + { + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Doing something..."); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample09s/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample09s/Steps/SayGoodbye.cs new file mode 100644 index 000000000..1789b74b2 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/Steps/SayGoodbye.cs @@ -0,0 +1,15 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample09s +{ + public class SayGoodbye : StepBody + { + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Goodbye"); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample09s/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample09s/Steps/SayHello.cs new file mode 100644 index 000000000..ebec30eed --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/Steps/SayHello.cs @@ -0,0 +1,15 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample09s +{ + public class SayHello : StepBody + { + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Hello"); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample09s/WorkflowCore.Sample09s.csproj b/src/samples/WorkflowCore.Sample09s/WorkflowCore.Sample09s.csproj new file mode 100644 index 000000000..67c94bc64 --- /dev/null +++ b/src/samples/WorkflowCore.Sample09s/WorkflowCore.Sample09s.csproj @@ -0,0 +1,18 @@ + + + + Exe + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample10/Program.cs b/src/samples/WorkflowCore.Sample10/Program.cs index c57970e22..5fa612f8e 100644 --- a/src/samples/WorkflowCore.Sample10/Program.cs +++ b/src/samples/WorkflowCore.Sample10/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using WorkflowCore.Interface; @@ -17,7 +16,7 @@ public static void Main(string[] args) host.Start(); Console.WriteLine("Starting workflow..."); - string workflowId = host.StartWorkflow("While", new MyData() { Counter = 0 }).Result; + string workflowId = host.StartWorkflow("While", new MyData { Counter = 0 }).Result; Console.ReadLine(); @@ -37,9 +36,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample10/Steps/DoSomething.cs b/src/samples/WorkflowCore.Sample10/Steps/DoSomething.cs index 646b929a6..389bd86e3 100644 --- a/src/samples/WorkflowCore.Sample10/Steps/DoSomething.cs +++ b/src/samples/WorkflowCore.Sample10/Steps/DoSomething.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample10/Steps/IncrementStep.cs b/src/samples/WorkflowCore.Sample10/Steps/IncrementStep.cs index cb7177bba..2e000956c 100644 --- a/src/samples/WorkflowCore.Sample10/Steps/IncrementStep.cs +++ b/src/samples/WorkflowCore.Sample10/Steps/IncrementStep.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample10/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample10/Steps/SayGoodbye.cs index 48ec493ef..f28c50dcf 100644 --- a/src/samples/WorkflowCore.Sample10/Steps/SayGoodbye.cs +++ b/src/samples/WorkflowCore.Sample10/Steps/SayGoodbye.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample10/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample10/Steps/SayHello.cs index d0e96745b..0b552ea83 100644 --- a/src/samples/WorkflowCore.Sample10/Steps/SayHello.cs +++ b/src/samples/WorkflowCore.Sample10/Steps/SayHello.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample10/WhileWorkflow.cs b/src/samples/WorkflowCore.Sample10/WhileWorkflow.cs index b65156edc..bf4ce8ccc 100644 --- a/src/samples/WorkflowCore.Sample10/WhileWorkflow.cs +++ b/src/samples/WorkflowCore.Sample10/WhileWorkflow.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample10 { diff --git a/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj b/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj index 0a50fc7c5..e7c7206ec 100644 --- a/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj +++ b/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj @@ -2,13 +2,10 @@ Exe - netcoreapp2.0 - - - + diff --git a/src/samples/WorkflowCore.Sample11/IfWorkflow.cs b/src/samples/WorkflowCore.Sample11/IfWorkflow.cs index 64b31fb56..2b9ef2265 100644 --- a/src/samples/WorkflowCore.Sample11/IfWorkflow.cs +++ b/src/samples/WorkflowCore.Sample11/IfWorkflow.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample11 { diff --git a/src/samples/WorkflowCore.Sample11/Program.cs b/src/samples/WorkflowCore.Sample11/Program.cs index a3097ab5f..ea920b195 100644 --- a/src/samples/WorkflowCore.Sample11/Program.cs +++ b/src/samples/WorkflowCore.Sample11/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using WorkflowCore.Interface; @@ -17,7 +16,7 @@ public static void Main(string[] args) host.Start(); Console.WriteLine("Starting workflow..."); - string workflowId = host.StartWorkflow("if-sample", new MyData() { Counter = 4 }).Result; + string workflowId = host.StartWorkflow("if-sample", new MyData { Counter = 4 }).Result; Console.ReadLine(); host.Stop(); @@ -36,9 +35,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample11/Steps/PrintMessage.cs b/src/samples/WorkflowCore.Sample11/Steps/PrintMessage.cs index d2f1e013c..16e709773 100644 --- a/src/samples/WorkflowCore.Sample11/Steps/PrintMessage.cs +++ b/src/samples/WorkflowCore.Sample11/Steps/PrintMessage.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample11/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample11/Steps/SayGoodbye.cs index ba7601881..388f5acbf 100644 --- a/src/samples/WorkflowCore.Sample11/Steps/SayGoodbye.cs +++ b/src/samples/WorkflowCore.Sample11/Steps/SayGoodbye.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample11/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample11/Steps/SayHello.cs index b8b5f0ad3..ed5c2a17d 100644 --- a/src/samples/WorkflowCore.Sample11/Steps/SayHello.cs +++ b/src/samples/WorkflowCore.Sample11/Steps/SayHello.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj b/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj index d62c82859..106048445 100644 --- a/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj +++ b/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj @@ -2,12 +2,11 @@ Exe - netcoreapp1.1 - - + + diff --git a/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs b/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs index 42878e024..aa58e0a74 100644 --- a/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs +++ b/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample12 { @@ -13,23 +10,29 @@ public class OutcomeWorkflow : IWorkflow public void Build(IWorkflowBuilder builder) { + var branch1 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 1") + .Then() + .Input(step => step.Message, data => "bye from 1"); + + var branch2 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 2") + .Then() + .Input(step => step.Message, data => "bye from 2"); + + builder .StartWith() - .Then() - .When(data => 1).Do(then => then - .StartWith() - .Input(step => step.Message, data => "Outcome was 1") - ) - .When(data => 2).Do(then => then - .StartWith() - .Input(step => step.Message, data => "Outcome was 2") - ) - .Then(); + .Decide(data => data.Value) + .Branch(1, branch1) + .Branch(2, branch2); } } public class MyData { - public int Counter { get; set; } + public int Value { get; set; } } } diff --git a/src/samples/WorkflowCore.Sample12/Program.cs b/src/samples/WorkflowCore.Sample12/Program.cs index be1144a6a..c3b6e211a 100644 --- a/src/samples/WorkflowCore.Sample12/Program.cs +++ b/src/samples/WorkflowCore.Sample12/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using WorkflowCore.Interface; @@ -17,7 +16,7 @@ public static void Main(string[] args) host.Start(); Console.WriteLine("Starting workflow..."); - string workflowId = host.StartWorkflow("outcome-sample").Result; + host.StartWorkflow("outcome-sample", new MyData { Value = 2 }); Console.ReadLine(); @@ -37,9 +36,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample12/README.md b/src/samples/WorkflowCore.Sample12/README.md index d0b8e7629..9ecfd1809 100644 --- a/src/samples/WorkflowCore.Sample12/README.md +++ b/src/samples/WorkflowCore.Sample12/README.md @@ -1,32 +1,32 @@ -# Outcome sample +# Decision and branch sample -Illustrates how to switch different workflow paths based on a step outcome +Illustrates how to switch different workflow paths based on an expression value. -First we need a workflow step with a specific outcome. -```c# -public class DetermineSomething : StepBody -{ - public override ExecutionResult Run(IStepExecutionContext context) - { - return ExecutionResult.Outcome(2); - } -} -``` +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 the `Decide` step. + +Use the `Decide` primitive step and hook up your branches via the `Branch` method. The result of the input expression will be matched to the expressions listed via the `Branch` method, and the matching next step(s) will be scheduled to execute next. -Then we use the .When().Do method to determine which sub-path we take. ```c# +var branch1 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 1") + .Then() + .Input(step => step.Message, data => "bye from 1"); + +var branch2 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 2") + .Then() + .Input(step => step.Message, data => "bye from 2"); + + builder .StartWith() - .Then() - .When(data => 1).Do(then => then - .StartWith() - .Input(step => step.Message, data => "Outcome was 1") - ) - .When(data => 2).Do(then => then - .StartWith() - .Input(step => step.Message, data => "Outcome was 2") - ) - .Then(); + .Decide(data => data.Value) + .Branch(1, branch1) + .Branch(2, branch2); ``` diff --git a/src/samples/WorkflowCore.Sample12/Steps/DetermineSomething.cs b/src/samples/WorkflowCore.Sample12/Steps/DetermineSomething.cs index 44f99d41b..807162e4e 100644 --- a/src/samples/WorkflowCore.Sample12/Steps/DetermineSomething.cs +++ b/src/samples/WorkflowCore.Sample12/Steps/DetermineSomething.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample12/Steps/PrintMessage.cs b/src/samples/WorkflowCore.Sample12/Steps/PrintMessage.cs index c655831d2..3809d0226 100644 --- a/src/samples/WorkflowCore.Sample12/Steps/PrintMessage.cs +++ b/src/samples/WorkflowCore.Sample12/Steps/PrintMessage.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample12/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample12/Steps/SayGoodbye.cs index ba4c581a3..7c2be887c 100644 --- a/src/samples/WorkflowCore.Sample12/Steps/SayGoodbye.cs +++ b/src/samples/WorkflowCore.Sample12/Steps/SayGoodbye.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample12/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample12/Steps/SayHello.cs index b232aa55b..5fe4b5c3e 100644 --- a/src/samples/WorkflowCore.Sample12/Steps/SayHello.cs +++ b/src/samples/WorkflowCore.Sample12/Steps/SayHello.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj b/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj index 0c14b0512..e0fb0e8aa 100644 --- a/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj +++ b/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj @@ -2,13 +2,10 @@ Exe - netcoreapp2.0 - - - + diff --git a/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs b/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs index d885f9f55..83209bcc3 100644 --- a/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs +++ b/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample13 { @@ -25,7 +22,9 @@ public void Build(IWorkflowBuilder builder) then.StartWith() .Input(step => step.Message, data => "Item 2.1") .Then() - .Input(step => step.Message, data => "Item 2.2")) + .Input(step => step.Message, data => "Item 2.2") + .Then() + .Input(step => step.Message, data => "Item 2.3")) .Do(then => then.StartWith() .Input(step => step.Message, data => "Item 3.1") diff --git a/src/samples/WorkflowCore.Sample13/Program.cs b/src/samples/WorkflowCore.Sample13/Program.cs index f3364949b..6e0ba9d0a 100644 --- a/src/samples/WorkflowCore.Sample13/Program.cs +++ b/src/samples/WorkflowCore.Sample13/Program.cs @@ -1,11 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using WorkflowCore.Interface; -using System.Threading; -using System.Threading.Tasks; -using System.Security.Cryptography; -using System.Collections.Generic; namespace WorkflowCore.Sample13 { @@ -23,7 +18,7 @@ public static void Main(string[] args) host.Start(); Console.WriteLine("Starting workflow..."); - controller.StartWorkflow("parallel-sample"); + controller.StartWorkflow("parallel-sample"); Console.ReadLine(); host.Stop(); @@ -41,7 +36,7 @@ private static IServiceProvider ConfigureServices() //services.AddWorkflow(x => //{ - // x.UseAzureSyncronization(@"UseDevelopmentStorage=true"); + // x.UseAzureSynchronization(@"UseDevelopmentStorage=true"); // x.UseMongoDB(@"mongodb://localhost:27017", "workflow-test002"); //}); @@ -53,9 +48,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample13/Steps/PrintMessage.cs b/src/samples/WorkflowCore.Sample13/Steps/PrintMessage.cs index dbb59578f..0cd23dc03 100644 --- a/src/samples/WorkflowCore.Sample13/Steps/PrintMessage.cs +++ b/src/samples/WorkflowCore.Sample13/Steps/PrintMessage.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample13/Steps/SayGoodbye.cs b/src/samples/WorkflowCore.Sample13/Steps/SayGoodbye.cs index b7779d3b8..40b851e56 100644 --- a/src/samples/WorkflowCore.Sample13/Steps/SayGoodbye.cs +++ b/src/samples/WorkflowCore.Sample13/Steps/SayGoodbye.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample13/Steps/SayHello.cs b/src/samples/WorkflowCore.Sample13/Steps/SayHello.cs index bf174e0c0..591025768 100644 --- a/src/samples/WorkflowCore.Sample13/Steps/SayHello.cs +++ b/src/samples/WorkflowCore.Sample13/Steps/SayHello.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj b/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj index 58ee4a9a1..522eaac98 100644 --- a/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj +++ b/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj @@ -2,13 +2,10 @@ Exe - netcoreapp2.0 - - - + diff --git a/src/samples/WorkflowCore.Sample14/Program.cs b/src/samples/WorkflowCore.Sample14/Program.cs index 3e9f50db4..f12ae2621 100644 --- a/src/samples/WorkflowCore.Sample14/Program.cs +++ b/src/samples/WorkflowCore.Sample14/Program.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using WorkflowCore.Interface; namespace WorkflowCore.Sample14 @@ -33,9 +32,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample14/RecurSampleWorkflow.cs b/src/samples/WorkflowCore.Sample14/RecurSampleWorkflow.cs index d3d91f05b..862624479 100644 --- a/src/samples/WorkflowCore.Sample14/RecurSampleWorkflow.cs +++ b/src/samples/WorkflowCore.Sample14/RecurSampleWorkflow.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; -using WorkflowCore.Models; namespace WorkflowCore.Sample14 { diff --git a/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj b/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj index c7292293d..539973bee 100644 --- a/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj +++ b/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj @@ -2,17 +2,8 @@ Exe - netcoreapp2.0 - - - - - - - - diff --git a/src/samples/WorkflowCore.Sample15/HelloWorldWorkflow.cs b/src/samples/WorkflowCore.Sample15/HelloWorldWorkflow.cs index 7c2ed4984..4b1744add 100644 --- a/src/samples/WorkflowCore.Sample15/HelloWorldWorkflow.cs +++ b/src/samples/WorkflowCore.Sample15/HelloWorldWorkflow.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Models; using WorkflowCore.Sample15.Steps; namespace WorkflowCore.Sample15 diff --git a/src/samples/WorkflowCore.Sample15/Program.cs b/src/samples/WorkflowCore.Sample15/Program.cs index 64675b0de..76d539baf 100644 --- a/src/samples/WorkflowCore.Sample15/Program.cs +++ b/src/samples/WorkflowCore.Sample15/Program.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Sample15.Steps; using WorkflowCore.Sample15.Services; @@ -18,7 +17,7 @@ public static void Main(string[] args) host.RegisterWorkflow(); host.Start(); - host.StartWorkflow("HelloWorld", 1, null); + host.StartWorkflow("HelloWorld", 1, null, null); Console.ReadLine(); host.Stop(); diff --git a/src/samples/WorkflowCore.Sample15/Services/MyService.cs b/src/samples/WorkflowCore.Sample15/Services/MyService.cs index 290805efd..8b8b322cf 100644 --- a/src/samples/WorkflowCore.Sample15/Services/MyService.cs +++ b/src/samples/WorkflowCore.Sample15/Services/MyService.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Sample15.Services { diff --git a/src/samples/WorkflowCore.Sample15/Steps/DoSomething.cs b/src/samples/WorkflowCore.Sample15/Steps/DoSomething.cs index 5aeafdb3e..e8f7bff3b 100644 --- a/src/samples/WorkflowCore.Sample15/Steps/DoSomething.cs +++ b/src/samples/WorkflowCore.Sample15/Steps/DoSomething.cs @@ -1,8 +1,5 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Sample15.Services; diff --git a/src/samples/WorkflowCore.Sample15/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample15/Steps/HelloWorld.cs index 6d32d532d..21dec311b 100644 --- a/src/samples/WorkflowCore.Sample15/Steps/HelloWorld.cs +++ b/src/samples/WorkflowCore.Sample15/Steps/HelloWorld.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj b/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj index d0f8239c2..f8d1df555 100644 --- a/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj +++ b/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj @@ -1,13 +1,12 @@ - + Exe - netcoreapp2.0 - - + + diff --git a/src/samples/WorkflowCore.Sample16/Program.cs b/src/samples/WorkflowCore.Sample16/Program.cs index 1f95fc65e..db085a6d9 100644 --- a/src/samples/WorkflowCore.Sample16/Program.cs +++ b/src/samples/WorkflowCore.Sample16/Program.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; using WorkflowCore.Interface; @@ -32,9 +31,6 @@ private static IServiceProvider ConfigureServices() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddDebug(LogLevel.Debug); return serviceProvider; } } diff --git a/src/samples/WorkflowCore.Sample16/ScheduleWorkflow.cs b/src/samples/WorkflowCore.Sample16/ScheduleWorkflow.cs index 47d4e72bd..69878f1b8 100644 --- a/src/samples/WorkflowCore.Sample16/ScheduleWorkflow.cs +++ b/src/samples/WorkflowCore.Sample16/ScheduleWorkflow.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; namespace WorkflowCore.Sample16 diff --git a/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj b/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj index 73f2446c6..f8d1df555 100644 --- a/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj +++ b/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj @@ -1,13 +1,12 @@ - + Exe - netcoreapp2.0 - - + + diff --git a/src/samples/WorkflowCore.Sample17/CompensatingWorkflow.cs b/src/samples/WorkflowCore.Sample17/CompensatingWorkflow.cs index 94db0806e..00a0ead8d 100644 --- a/src/samples/WorkflowCore.Sample17/CompensatingWorkflow.cs +++ b/src/samples/WorkflowCore.Sample17/CompensatingWorkflow.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; using WorkflowCore.Interface; using WorkflowCore.Sample17.Steps; diff --git a/src/samples/WorkflowCore.Sample17/README.md b/src/samples/WorkflowCore.Sample17/README.md index 1926e829e..1481688ac 100644 --- a/src/samples/WorkflowCore.Sample17/README.md +++ b/src/samples/WorkflowCore.Sample17/README.md @@ -15,7 +15,7 @@ builder .Then() .CompensateWith() ) - .OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5)) + .OnError(Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(5)) .Then(context => Console.WriteLine("End")); ``` @@ -34,7 +34,7 @@ builder .Then() .CompensateWith() ) - .CompensateWith() + .CompensateWith() .Then(context => Console.WriteLine("End")); ``` @@ -50,7 +50,7 @@ builder .Then() .Then() ) - .CompensateWith() + .CompensateWith() .Then(context => Console.WriteLine("End")); ``` @@ -126,4 +126,4 @@ The compensation steps can be defined by specifying the `CompensateWith` paramet } ] } -``` \ No newline at end of file +``` diff --git a/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj b/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj index e58dcab19..f54a7ad6d 100644 --- a/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj +++ b/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj @@ -1,14 +1,9 @@ - + Exe - netcoreapp2.0 - - - - diff --git a/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs b/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs new file mode 100644 index 000000000..22b32a782 --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs @@ -0,0 +1,29 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Sample18.Steps; + +namespace WorkflowCore.Sample18 +{ + class ActivityWorkflow : IWorkflow + { + public string Id => "activity-sample"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Activity("get-approval", (data) => data.Request) + .Output(data => data.ApprovedBy, step => step.Result) + .Then() + .Input(step => step.Message, data => "Approved by " + data.ApprovedBy) + .Then(); + } + } + + class MyData + { + public string Request { get; set; } + public string ApprovedBy { get; set; } + } +} diff --git a/src/samples/WorkflowCore.Sample18/Program.cs b/src/samples/WorkflowCore.Sample18/Program.cs new file mode 100644 index 000000000..aa7d3f2f7 --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using WorkflowCore.Interface; + +namespace WorkflowCore.Sample18 +{ + class Program + { + static void Main(string[] args) + { + var serviceProvider = ConfigureServices(); + + //start the workflow host + var host = serviceProvider.GetService(); + host.RegisterWorkflow(); + host.Start(); + + Console.WriteLine("Starting workflow..."); + + var workflowId = host.StartWorkflow("activity-sample", new MyData { Request = "Spend $1,000,000" }).Result; + + var approval = host.GetPendingActivity("get-approval", "worker1", TimeSpan.FromMinutes(1)).Result; + + if (approval != null) + { + Console.WriteLine("Approval required for " + approval.Parameters); + host.SubmitActivitySuccess(approval.Token, "John Smith"); + } + + Console.ReadLine(); + host.Stop(); + } + + private static IServiceProvider ConfigureServices() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + //services.AddWorkflow(); + services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); + //services.AddWorkflow(x => x.UseSqlServer(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;", true, true)); + //services.AddWorkflow(x => x.UsePostgreSQL(@"Server=127.0.0.1;Port=5432;Database=workflow;User Id=postgres;", true, true)); + services.AddLogging(cfg => + { + cfg.AddConsole(); + cfg.AddDebug(); + }); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + } +} diff --git a/src/samples/WorkflowCore.Sample18/README.md b/src/samples/WorkflowCore.Sample18/README.md new file mode 100644 index 000000000..85823eaf7 --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/README.md @@ -0,0 +1,28 @@ +# Activity sample + +Illustrates how to have your workflow wait for an external activity that is fulfilled by a worker that you implement. + +This workflow will wait for the `get-approval` activity and pass the request string to it as an input. + +```c# +builder + .StartWith() + .Activity("get-approval", (data) => data.Request) + .Output(data => data.ApprovedBy, step => step.Result) + .Then() + .Input(step => step.Message, data => "Approved by " + data.ApprovedBy) + .Then(); +``` + +Then we implement an activity worker to pull pending activities of type `get-approval`, where we can inspect the input and submit a response back to the waiting workflow. + +```c# +var approval = host.GetPendingActivity("get-approval", "worker1", TimeSpan.FromMinutes(1)).Result; + +if (approval != null) +{ + Console.WriteLine("Approval required for " + approval.Parameters); + host.SubmitActivitySuccess(approval.Token, "John Smith"); +} +``` + diff --git a/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs b/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs new file mode 100644 index 000000000..69f1dd42c --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample18.Steps +{ + public class CustomMessage : StepBody + { + + public string Message { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine(Message); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample18/Steps/GoodbyeWorld.cs b/src/samples/WorkflowCore.Sample18/Steps/GoodbyeWorld.cs new file mode 100644 index 000000000..b42f3e913 --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/Steps/GoodbyeWorld.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample18.Steps +{ + public class GoodbyeWorld : StepBody + { + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Goodbye world"); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample18/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample18/Steps/HelloWorld.cs new file mode 100644 index 000000000..8378bb7cd --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/Steps/HelloWorld.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample18.Steps +{ + public class HelloWorld : StepBody + { + public override ExecutionResult Run(IStepExecutionContext context) + { + Console.WriteLine("Hello world"); + return ExecutionResult.Next(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj b/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj new file mode 100644 index 000000000..c1c6e0846 --- /dev/null +++ b/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj @@ -0,0 +1,19 @@ + + + + Exe + + + + + + + + + + + + + + + diff --git a/src/samples/WorkflowCore.Sample19/FlakyConnectionParams.cs b/src/samples/WorkflowCore.Sample19/FlakyConnectionParams.cs new file mode 100644 index 000000000..0fda1a91f --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/FlakyConnectionParams.cs @@ -0,0 +1,9 @@ +using System; + +namespace WorkflowCore.Sample19 +{ + public class FlakyConnectionParams : IDescriptiveWorkflowParams + { + public string Description { get; set; } + } +} diff --git a/src/samples/WorkflowCore.Sample19/FlakyConnectionWorkflow.cs b/src/samples/WorkflowCore.Sample19/FlakyConnectionWorkflow.cs new file mode 100644 index 000000000..a388930d2 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/FlakyConnectionWorkflow.cs @@ -0,0 +1,25 @@ +using WorkflowCore.Interface; +using WorkflowCore.Sample19.Steps; + +namespace WorkflowCore.Sample19 +{ + public class FlakyConnectionWorkflow : IWorkflow + { + public string Id => "flaky-sample"; + + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Input(x => x.Message, _ => "Starting workflow") + + .Then() + .Input(x => x.SucceedAfterAttempts, _ => 3) + + .Then() + .Input(x => x.Message, _ => "Finishing workflow"); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/IDescriptiveWorkflowParams.cs b/src/samples/WorkflowCore.Sample19/IDescriptiveWorkflowParams.cs new file mode 100644 index 000000000..73bd26892 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/IDescriptiveWorkflowParams.cs @@ -0,0 +1,7 @@ +namespace WorkflowCore.Sample19 +{ + public interface IDescriptiveWorkflowParams + { + string Description { get; } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Middleware/AddDescriptionWorkflowMiddleware.cs b/src/samples/WorkflowCore.Sample19/Middleware/AddDescriptionWorkflowMiddleware.cs new file mode 100644 index 000000000..8c6080c12 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Middleware/AddDescriptionWorkflowMiddleware.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Middleware +{ + 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(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Middleware/LogCorrelationStepMiddleware.cs b/src/samples/WorkflowCore.Sample19/Middleware/LogCorrelationStepMiddleware.cs new file mode 100644 index 000000000..38fefc362 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Middleware/LogCorrelationStepMiddleware.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Middleware +{ + /// + /// Loosely based off this article: + /// https://www.frakkingsweet.com/net-core-log-correlation-easy-access-to-headers/ + /// + public class AddMetadataToLogsMiddleware: IWorkflowStepMiddleware + { + private readonly ILogger _log; + + public AddMetadataToLogsMiddleware(ILogger log) + { + _log = log; + } + + public async Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next) + { + var workflowId = context.Workflow.Id; + var stepId = context.Step.Id; + + using (_log.BeginScope("WorkflowId => {@WorkflowId}", workflowId)) + using (_log.BeginScope("StepId => {@StepId}", stepId)) + { + return await next(); + } + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Middleware/PollyRetryMiddleware.cs b/src/samples/WorkflowCore.Sample19/Middleware/PollyRetryMiddleware.cs new file mode 100644 index 000000000..24e7621e7 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Middleware/PollyRetryMiddleware.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Polly; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Middleware +{ + public class PollyRetryMiddleware : IWorkflowStepMiddleware + { + private const string StepContextKey = "WorkflowStepContext"; + private const int MaxRetries = 3; + private readonly ILogger _log; + + public PollyRetryMiddleware(ILogger log) + { + _log = log; + } + + 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(), new Dictionary + { + { StepContextKey, context } + }); + } + + private Task UpdateRetryCount( + Exception exception, + int retryCount, + IStepExecutionContext stepContext) + { + var stepInstance = stepContext.ExecutionPointer; + stepInstance.RetryCount = retryCount; + + _log.LogWarning( + exception, + "Exception occurred in step {StepId}. Retrying [{RetryCount}/{MaxCount}]", + stepInstance.Id, + retryCount, + MaxRetries + ); + + // TODO: Come up with way to persist workflow + return Task.CompletedTask; + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Middleware/PrintWorkflowSummaryMiddleware.cs b/src/samples/WorkflowCore.Sample19/Middleware/PrintWorkflowSummaryMiddleware.cs new file mode 100644 index 000000000..2cdba963e --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Middleware/PrintWorkflowSummaryMiddleware.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Middleware +{ + 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(); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Program.cs b/src/samples/WorkflowCore.Sample19/Program.cs new file mode 100644 index 000000000..6d3074744 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Program.cs @@ -0,0 +1,65 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using WorkflowCore.Interface; +using WorkflowCore.Sample19.Middleware; +using WorkflowCore.Sample19.Steps; + +namespace WorkflowCore.Sample19 +{ + class Program + { + static void Main(string[] args) + { + var serviceProvider = ConfigureServices(); + + // Start the workflow host + var host = serviceProvider.GetService(); + host.RegisterWorkflow(); + host.Start(); + + var workflowParams = new FlakyConnectionParams + { + Description = "Flaky connection workflow" + }; + var workflowId = host.StartWorkflow("flaky-sample", workflowParams).Result; + Console.WriteLine($"Kicked off workflow {workflowId}"); + + Console.ReadLine(); + host.Stop(); + } + + private static IServiceProvider ConfigureServices() + { + // Setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddWorkflow(); + + // Add step middleware + // Note that middleware will get executed in the order in which they were registered + services.AddWorkflowStepMiddleware(); + services.AddWorkflowStepMiddleware(); + + // Add some pre workflow middleware + // This middleware will run before the workflow starts + services.AddWorkflowMiddleware(); + + // Add some post workflow middleware + // This middleware will run after the workflow completes + services.AddWorkflowMiddleware(); + + // Add workflow steps + services.AddTransient(); + services.AddTransient(); + + services.AddLogging(cfg => + { + cfg.AddConsole(x => x.IncludeScopes = true); + cfg.AddDebug(); + }); + + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Steps/FlakyConnection.cs b/src/samples/WorkflowCore.Sample19/Steps/FlakyConnection.cs new file mode 100644 index 000000000..f04f33f11 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Steps/FlakyConnection.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Steps +{ + public class FlakyConnection : StepBodyAsync + { + private static readonly TimeSpan Delay = TimeSpan.FromSeconds(1); + private int _currentCallCount = 0; + + public int? SucceedAfterAttempts { get; set; } = 3; + + public override async Task RunAsync(IStepExecutionContext context) + { + if (SucceedAfterAttempts.HasValue && _currentCallCount >= SucceedAfterAttempts.Value) + { + return ExecutionResult.Next(); + } + + _currentCallCount++; + await Task.Delay(Delay); + throw new TimeoutException("A call has timed out"); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/Steps/LogMessage.cs b/src/samples/WorkflowCore.Sample19/Steps/LogMessage.cs new file mode 100644 index 000000000..ac8f4b678 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/Steps/LogMessage.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Models; + +namespace WorkflowCore.Sample19.Steps +{ + public class LogMessage : StepBodyAsync + { + private readonly ILogger _log; + + public LogMessage(ILogger log) + { + _log = log; + } + + public string Message { get; set; } + + public override Task RunAsync(IStepExecutionContext context) + { + if (Message != null) + { + _log.LogInformation(Message); + } + + return Task.FromResult(ExecutionResult.Next()); + } + } +} diff --git a/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj b/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj new file mode 100644 index 000000000..181b45e66 --- /dev/null +++ b/src/samples/WorkflowCore.Sample19/WorkflowCore.Sample19.csproj @@ -0,0 +1,19 @@ + + + + Exe + + + + + + + + + + + + + + + diff --git a/src/samples/WorkflowCore.TestSample01/NUnitTest.cs b/src/samples/WorkflowCore.TestSample01/NUnitTest.cs index 8c83661d8..167c97441 100644 --- a/src/samples/WorkflowCore.TestSample01/NUnitTest.cs +++ b/src/samples/WorkflowCore.TestSample01/NUnitTest.cs @@ -1,8 +1,6 @@ using FluentAssertions; using NUnit.Framework; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Models; using WorkflowCore.Testing; using WorkflowCore.TestSample01.Workflow; @@ -13,7 +11,7 @@ namespace WorkflowCore.TestSample01 public class NUnitTest : WorkflowTest { [SetUp] - protected override void Setup() + protected void Setup() { base.Setup(); } @@ -21,7 +19,7 @@ protected override void Setup() [Test] public void NUnit_workflow_test_sample() { - var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 }); + var workflowId = StartWorkflow(new MyDataClass { Value1 = 2, Value2 = 3 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); diff --git a/src/samples/WorkflowCore.TestSample01/Workflow/AddNumbers.cs b/src/samples/WorkflowCore.TestSample01/Workflow/AddNumbers.cs index a45f06f9d..0ec30e24b 100644 --- a/src/samples/WorkflowCore.TestSample01/Workflow/AddNumbers.cs +++ b/src/samples/WorkflowCore.TestSample01/Workflow/AddNumbers.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/samples/WorkflowCore.TestSample01/Workflow/MyDataClass.cs b/src/samples/WorkflowCore.TestSample01/Workflow/MyDataClass.cs index 1db1d9273..84f1abf54 100644 --- a/src/samples/WorkflowCore.TestSample01/Workflow/MyDataClass.cs +++ b/src/samples/WorkflowCore.TestSample01/Workflow/MyDataClass.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.TestSample01.Workflow { diff --git a/src/samples/WorkflowCore.TestSample01/Workflow/MyWorkflow.cs b/src/samples/WorkflowCore.TestSample01/Workflow/MyWorkflow.cs index bf485e559..67b390873 100644 --- a/src/samples/WorkflowCore.TestSample01/Workflow/MyWorkflow.cs +++ b/src/samples/WorkflowCore.TestSample01/Workflow/MyWorkflow.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; namespace WorkflowCore.TestSample01.Workflow diff --git a/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj b/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj index 117b472e3..308565745 100644 --- a/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj +++ b/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj @@ -1,13 +1,8 @@  - - netcoreapp2.0 - - - - + @@ -16,7 +11,7 @@ - + diff --git a/src/samples/WorkflowCore.TestSample01/xUnitTest.cs b/src/samples/WorkflowCore.TestSample01/xUnitTest.cs index 6aeca8cdc..7afb636f0 100644 --- a/src/samples/WorkflowCore.TestSample01/xUnitTest.cs +++ b/src/samples/WorkflowCore.TestSample01/xUnitTest.cs @@ -17,7 +17,7 @@ public xUnitTest() [Fact] public void MyWorkflow() { - var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 }); + var workflowId = StartWorkflow(new MyDataClass { Value1 = 2, Value2 = 3 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 000000000..3b2ad7341 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,24 @@ + + + net6.0;netcoreapp3.1 + latest + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Docker.Testify/Docker.Testify.csproj b/test/Docker.Testify/Docker.Testify.csproj index 36c58c385..a83648df4 100644 --- a/test/Docker.Testify/Docker.Testify.csproj +++ b/test/Docker.Testify/Docker.Testify.csproj @@ -1,11 +1,7 @@  - - netstandard1.4 - - - + diff --git a/test/Docker.Testify/DockerSetup.cs b/test/Docker.Testify/DockerSetup.cs index eb175b22c..71bb76e26 100644 --- a/test/Docker.Testify/DockerSetup.cs +++ b/test/Docker.Testify/DockerSetup.cs @@ -3,12 +3,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace Docker.Testify @@ -20,7 +17,7 @@ public abstract class DockerSetup : IDisposable public abstract int InternalPort { get; } public virtual string ImageTag => "latest"; - public virtual TimeSpan TimeOut => TimeSpan.FromSeconds(30); + public virtual TimeSpan TimeOut => TimeSpan.FromSeconds(60); public virtual IList EnvironmentVariables => new List(); public int ExternalPort { get; } @@ -30,6 +27,8 @@ public abstract class DockerSetup : IDisposable protected readonly DockerClient docker; protected string containerId; + private static HashSet UsedPorts = new HashSet(); + protected DockerSetup() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -58,7 +57,7 @@ public async Task StartContainer() await PullImage(ImageName, ImageTag); - var container = await docker.Containers.CreateContainerAsync(new CreateContainerParameters() + var container = await docker.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{ImageName}:{ImageTag}", Name = $"{ContainerPrefix}-{Guid.NewGuid()}", @@ -110,38 +109,38 @@ public async Task PullImage(string name, string tag) if (exists) return; - var stream = await docker.Images.PullImageAsync(new ImagesPullParameters() { Parent = name, Tag = tag }, null); - - using (StreamReader reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - Debug.WriteLine(reader.ReadLine()); - } + Debug.WriteLine($"Pulling docker image {name}:{tag}"); + await docker.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = name, Tag = tag }, null, new Progress()); } - public void Dispose() + public void Dispose() { docker.Containers.KillContainerAsync(containerId, new ContainerKillParameters()).Wait(); - docker.Containers.RemoveContainerAsync(containerId, new ContainerRemoveParameters() { Force = true }).Wait(); + docker.Containers.RemoveContainerAsync(containerId, new ContainerRemoveParameters { Force = true }).Wait(); } private int GetFreePort() { - const int startRange = 1000; - const int endRange = 10000; - var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); - var tcpPorts = ipGlobalProperties.GetActiveTcpListeners(); - var udpPorts = ipGlobalProperties.GetActiveUdpListeners(); + lock (UsedPorts) + { + const int startRange = 10002; + const int endRange = 15000; + var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); + var tcpPorts = ipGlobalProperties.GetActiveTcpListeners(); + var udpPorts = ipGlobalProperties.GetActiveUdpListeners(); - var result = startRange; + var result = startRange; - while (((tcpPorts.Any(x => x.Port == result)) || (udpPorts.Any(x => x.Port == result))) && result <= endRange) - result++; + while (((tcpPorts.Any(x => x.Port == result)) || (udpPorts.Any(x => x.Port == result))) && result <= endRange && !UsedPorts.Contains(result)) + result++; - if (result > endRange) - throw new PortsInUseException(); + if (result > endRange) + throw new PortsInUseException(); + + UsedPorts.Add(result); - return result; + return result; + } } } } diff --git a/test/Docker.Testify/PortsInUseException.cs b/test/Docker.Testify/PortsInUseException.cs index 48a897c22..f4ad7ae07 100644 --- a/test/Docker.Testify/PortsInUseException.cs +++ b/test/Docker.Testify/PortsInUseException.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Docker.Testify { diff --git a/test/ScratchPad/ElasticTest.cs b/test/ScratchPad/ElasticTest.cs new file mode 100644 index 000000000..6401bda70 --- /dev/null +++ b/test/ScratchPad/ElasticTest.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; +using Nest; +using WorkflowCore.Models.Search; + +namespace ScratchPad +{ + public class ElasticTest + { + public static void test(string[] args) + { + IServiceProvider serviceProvider = ConfigureServices(); + + //start the workflow host + var host = serviceProvider.GetService(); + var searchIndex = serviceProvider.GetService(); + + host.RegisterWorkflow(); + host.RegisterWorkflow(); + + host.Start(); + var data1 = new WorkflowCore.Sample03.MyDataClass { Value1 = 2, Value2 = 3 }; + host.StartWorkflow("PassingDataWorkflow", data1, "quick dog").Wait(); + + var data2 = new WorkflowCore.Sample04.MyDataClass { Value1 = "test" }; + host.StartWorkflow("EventSampleWorkflow", data2, "alt1 boom").Wait(); + + + var searchResult1 = searchIndex.Search("dog", 0, 10).Result; + var searchResult2 = searchIndex.Search("quick dog", 0, 10).Result; + var searchResult3 = searchIndex.Search("fast", 0, 10).Result; + var searchResult4 = searchIndex.Search("alt1", 0, 10).Result; + var searchResult5 = searchIndex.Search("dogs", 0, 10).Result; + var searchResult6 = searchIndex.Search("test", 0, 10).Result; + var searchResult7 = searchIndex.Search("", 0, 10).Result; + var searchResult8 = searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Reference, "quick dog")).Result; + var searchResult9 = searchIndex.Search("", 0, 10, ScalarFilter.Equals(x => x.Value1, 2)).Result; + + Console.ReadLine(); + host.Stop(); + } + + private static IServiceProvider ConfigureServices() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + //services.AddWorkflow(); + //services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); + services.AddWorkflow(cfg => + { + //var ddbConfig = new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }; + //cfg.UseAwsDynamoPersistence(new EnvironmentVariablesAWSCredentials(), ddbConfig, "elastic"); + cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://localhost:9200")), "workflows"); + //cfg.UseAwsSimpleQueueService(new EnvironmentVariablesAWSCredentials(), new AmazonSQSConfig() { RegionEndpoint = RegionEndpoint.USWest2 }); + //cfg.UseAwsDynamoLocking(new EnvironmentVariablesAWSCredentials(), new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }, "workflow-core-locks"); + }); + + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + + return serviceProvider; + } + + } + +} + diff --git a/test/ScratchPad/HelloWorld.json b/test/ScratchPad/HelloWorld.json index 257cddcdc..2e7fff6bf 100644 --- a/test/ScratchPad/HelloWorld.json +++ b/test/ScratchPad/HelloWorld.json @@ -1,77 +1,32 @@ { - "Id": "HelloWorld", + "Id": "Test02", "Version": 1, "Description": "", - "DataType": "ScratchPad.MyDataClass, ScratchPad", + "DataType": "ScratchPad.WfData, ScratchPad", "Steps": [ { "Id": "Hello", "StepType": "ScratchPad.HelloWorld, ScratchPad", - "NextStepId": "Generate" + "NextStepId": "decide" }, { - "Id": "Generate", - "StepType": "ScratchPad.GenerateMessage, ScratchPad", - "NextStepId": "Print", - "Outputs": { "Value3": "step.Message" } + "Id": "decide", + "StepType": "WorkflowCore.Primitives.Decide, WorkflowCore", + "SelectNextStep": + { + "Print1": "data.Value1 == \"one\"", + "Print2": "data.Value1 == \"two\"" + } }, { - "Id": "Print", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "NextStepId": "saga", - "Inputs": { "Message": "data.Value3 + \" - \" + DateTime.Now.ToString()" } + "Id": "Print1", + "StepType": "ScratchPad.CustomMessage, ScratchPad", + "Inputs": { "Message": "\"Hello from 1\"" } }, - { - "Id": "saga", - "StepType": "WorkflowCore.Primitives.Sequence, WorkflowCore", - "NextStepId": "Bye", - "Saga": true, - "Do": [ - [ - { - "Id": "do1", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "NextStepId": "do2", - "Inputs": { "Message": "\"inner 1\"" }, - "CompensateWith": [ - { - "Id": "comp0", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "Inputs": { "Message": "\"undoing do1\"" } - } - ] - }, - { - "Id": "do2", - "StepType": "ScratchPad.Throw, ScratchPad", - "NextStepId": "do3", - "CompensateWith": [ - { - "Id": "comp1", - "NextStepId": "comp2", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "Inputs": { "Message": "\"undoing do2\"" } - }, - { - "Id": "comp2", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "Inputs": { "Message": "\"still undoing do2\"" } - } - ] - }, - { - "Id": "do3", - "StepType": "ScratchPad.PrintMessage, ScratchPad", - "Inputs": { "Message": "\"inner 3\"" } - } - ] - ] - }, - - { - "Id": "Bye", - "StepType": "ScratchPad.GoodbyeWorld, ScratchPad" + "Id": "Print2", + "StepType": "ScratchPad.CustomMessage, ScratchPad", + "Inputs": { "Message": "\"Hello from 2\"" } } ] } \ No newline at end of file diff --git a/test/ScratchPad/Program.cs b/test/ScratchPad/Program.cs index 31a869b92..0d44810b9 100644 --- a/test/ScratchPad/Program.cs +++ b/test/ScratchPad/Program.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -7,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Models; -using System.Text; namespace ScratchPad { @@ -15,23 +13,39 @@ public class Program { public static void Main(string[] args) { - //var s = typeof(HelloWorld).AssemblyQualifiedName; - - IServiceProvider serviceProvider = ConfigureServices(); //start the workflow host var host = serviceProvider.GetService(); var loader = serviceProvider.GetService(); - var str = ScratchPad.Properties.Resources.HelloWorld; //Encoding.UTF8.GetString(ScratchPad.Properties.Resources.HelloWorld); + var activityController = serviceProvider.GetService(); + host.RegisterWorkflow(); + //loader.LoadDefinition(Properties.Resources.HelloWorld, Deserializers.Json); + + host.Start(); - loader.LoadDefinition(str); + var ids = new List(); + //for (var i = 0; i < 12000; i++) + //{ + // var wid = host.StartWorkflow("Test01", 1, new WfData() { Value1 = "two", Value2 = "data2" }).Result; + // ids.Add(wid); + //} + //Console.WriteLine("started..."); + //Thread.Sleep(5000); - //host.RegisterWorkflow(); - host.Start(); + host.PublishEvent("MyEvent", "Key", "one", DateTime.Now); + + for (var i = 0; i < 12000; i++) + { + var wid = host.StartWorkflow("Test01", 1, new WfData { Value1 = "two", Value2 = "data2" }).Result; + ids.Add(wid); + } + + Console.WriteLine("started2..."); + Thread.Sleep(5000); + + host.PublishEvent("MyEvent", "Key", "one", DateTime.Now); - host.StartWorkflow("HelloWorld", 1, new MyDataClass() { Value3 = "hi there" }); - Console.ReadLine(); host.Stop(); @@ -42,20 +56,29 @@ private static IServiceProvider ConfigureServices() //setup dependency injection IServiceCollection services = new ServiceCollection(); services.AddLogging(); - services.AddWorkflow(); + //services.AddWorkflow(); //services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); - services.AddTransient(); - + + services.AddWorkflow(cfg => + { + cfg.UseSqlServer(@"Server=.;Database=WorkflowCore;Trusted_Connection=True;", true, true); + cfg.UseMaxConcurrentWorkflows(100); + //var ddbConfig = new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }; + //cfg.UseAwsDynamoPersistence(new EnvironmentVariablesAWSCredentials(), ddbConfig, "elastic"); + //cfg.UseElasticsearch(new ConnectionSettings(new Uri("http://localhost:9200")), "workflows"); + //cfg.UseAwsSimpleQueueService(new EnvironmentVariablesAWSCredentials(), new AmazonSQSConfig() { RegionEndpoint = RegionEndpoint.USWest2 }); + //cfg.UseAwsDynamoLocking(new EnvironmentVariablesAWSCredentials(), new AmazonDynamoDBConfig() { RegionEndpoint = RegionEndpoint.USWest2 }, "workflow-core-locks"); + }); + services.AddWorkflowDSL(); + + var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddDebug(); return serviceProvider; } } - + public class HelloWorld : StepBody { public override ExecutionResult Run(IStepExecutionContext context) @@ -64,26 +87,10 @@ public override ExecutionResult Run(IStepExecutionContext context) return ExecutionResult.Next(); } } - public class GoodbyeWorld : StepBody - { - public override ExecutionResult Run(IStepExecutionContext context) - { - Console.WriteLine("Goodbye world"); - return ExecutionResult.Next(); - } - } - - public class Throw : StepBody - { - public override ExecutionResult Run(IStepExecutionContext context) - { - Console.WriteLine("throwing..."); - throw new Exception("up"); - } - } - - public class PrintMessage : StepBody + + public class CustomMessage : StepBody { + public string Message { get; set; } public override ExecutionResult Run(IStepExecutionContext context) @@ -92,25 +99,52 @@ public override ExecutionResult Run(IStepExecutionContext context) return ExecutionResult.Next(); } } - - public class GenerateMessage : StepBody + + public class WfData { - public string Message { get; set; } - - public override ExecutionResult Run(IStepExecutionContext context) + public string Value1 { get; set; } + public string Value2 { get; set; } + public string Value3 { get; set; } + } + + public class Test01Workflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) { - Message = "Generated message"; - return ExecutionResult.Next(); + var branch1 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 1") + .Then() + .Input(step => step.Message, data => "bye from 1"); + + var branch2 = builder.CreateBranch() + .StartWith() + .Input(step => step.Message, data => "hi from 2") + .Then() + .Input(step => step.Message, data => "bye from 2"); + + + builder + .StartWith() + .WaitFor("MyEvent", (data, context) => "Key", data => DateTime.Now) + .Output(data => data.Value1, step => step.EventData) + .Then((context) => + { + Console.WriteLine("------1"); + Task.Delay(TimeSpan.FromSeconds(5)).Wait(); + Console.WriteLine("------2"); + return ExecutionResult.Next(); + }) + .Decide(data => data.Value1) + .Branch((data, outcome) => data.Value1 == "one", branch1) + .Branch((data, outcome) => data.Value1 == "two", branch2); } - } - public class MyDataClass - { - public int Value1 { get; set; } - - public int Value2 { get; set; } - - public string Value3 { get; set; } + public string Id => "Test01"; + + public int Version => 1; + } + } diff --git a/test/ScratchPad/Properties/Resources.Designer.cs b/test/ScratchPad/Properties/Resources.Designer.cs index bb7355e59..6e79d8a01 100644 --- a/test/ScratchPad/Properties/Resources.Designer.cs +++ b/test/ScratchPad/Properties/Resources.Designer.cs @@ -1,93 +1,93 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace ScratchPad.Properties { - using System; - using System.Reflection; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ScratchPad.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "HelloWorld", - /// "Version": 1, - /// "Description": "", - /// "DataType": "System.Object", - /// "Steps": [ - /// { - /// "Id": "Hello", - /// "StepType": "ScratchPad.HelloWorld; ScratchPad", - /// "Name": "Hello", - /// "NextStepId": "Bye", - /// "Inputs": [], - /// "Outputs": [] - /// }, - /// { - /// "Id": "Bye", - /// "StepType": "ScratchPad.GoodbyeWorld; ScratchPad", - /// "Name": "Bye" - /// } - /// ] - ///}. - /// - internal static string HelloWorld { - get { - return ResourceManager.GetString("HelloWorld", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ScratchPad.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ScratchPad.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to { + /// "Id": "Test02", + /// "Version": 1, + /// "Description": "", + /// "DataType": "ScratchPad.WfData, ScratchPad", + /// "Steps": [ + /// { + /// "Id": "Hello", + /// "StepType": "ScratchPad.HelloWorld, ScratchPad", + /// "NextStepId": "decide" + /// }, + /// { + /// "Id": "decide", + /// "StepType": "WorkflowCore.Primitives.Decide, WorkflowCore", + /// "SelectNextStep": + /// { + /// "Print1": "data.Value1 == \"one\"", + /// "Print2": "data.Value1 == \"two\"" + /// } + /// }, + /// { + /// "Id": "Print1", /// [rest of string was truncated]";. + /// + internal static string HelloWorld { + get { + return ResourceManager.GetString("HelloWorld", resourceCulture); + } + } + } +} diff --git a/test/ScratchPad/Properties/Resources.resx b/test/ScratchPad/Properties/Resources.resx index 7a5a7544d..421b272c5 100644 --- a/test/ScratchPad/Properties/Resources.resx +++ b/test/ScratchPad/Properties/Resources.resx @@ -119,6 +119,6 @@ - ..\helloworld.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + ..\HelloWorld.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 \ No newline at end of file diff --git a/test/ScratchPad/ScratchPad.csproj b/test/ScratchPad/ScratchPad.csproj index c2e79a161..a194035c2 100644 --- a/test/ScratchPad/ScratchPad.csproj +++ b/test/ScratchPad/ScratchPad.csproj @@ -1,28 +1,28 @@  - netcoreapp2.0 ScratchPad Exe ScratchPad false false false + false + + + + + + + - - - - - - - True diff --git a/test/WorkflowCore.IntegrationTests/Properties/AssemblyInfo.cs b/test/WorkflowCore.IntegrationTests/Properties/AssemblyInfo.cs index 2a6d71492..f0355ab5a 100644 --- a/test/WorkflowCore.IntegrationTests/Properties/AssemblyInfo.cs +++ b/test/WorkflowCore.IntegrationTests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario.cs new file mode 100644 index 000000000..1e082f4db --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario.cs @@ -0,0 +1,74 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class ActivityScenario : WorkflowTest + { + public class MyDataClass + { + public object ActivityInput { get; set; } + public object ActivityOutput { get; set; } + } + + public class ActivityInput + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + public class ActivityOutput + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + public class ActivityWorkflow : IWorkflow + { + public string Id => "ActivityWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Activity("act-1", data => data.ActivityInput) + .Output(data => data.ActivityOutput, step => step.Result); + } + } + + public ActivityScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new MyDataClass { ActivityInput = new ActivityInput { Value1 = "a", Value2 = 1 } }); + var activity = Host.GetPendingActivity("act-1", "worker1", TimeSpan.FromSeconds(30)).Result; + + if (activity != null) + { + var actInput = (ActivityInput)activity.Parameters; + Host.SubmitActivitySuccess(activity.Token, new ActivityOutput + { + Value1 = actInput.Value1 + "1", + Value2 = actInput.Value2 + 1 + }); + } + + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).ActivityOutput.Should().BeOfType(); + var outData = (GetData(workflowId).ActivityOutput as ActivityOutput); + outData.Value1.Should().Be("a1"); + outData.Value2.Should().Be(2); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario2.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario2.cs new file mode 100644 index 000000000..1cf5b6330 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario2.cs @@ -0,0 +1,78 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + /* + * DISABLED for bug on build pipeline + public class ActivityScenario2 : WorkflowTest + { + public class MyDataClass + { + public object ActivityInput { get; set; } + public object ActivityOutput { get; set; } + } + + public class ActivityInput + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + public class ActivityOutput + { + public string Value1 { get; set; } + public int Value2 { get; set; } + } + + public class ActivityWorkflow : IWorkflow + { + public string Id => "ActivityWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Activity((_, context) => "act-1-" + context.Workflow.Id, data => data.ActivityInput) + .Output(data => data.ActivityOutput, step => step.Result); + } + } + + public ActivityScenario2() + { + Setup(); + } + + [Fact] + public void Scenario() + { + // compound key + var workflowId = StartWorkflow(new MyDataClass { ActivityInput = new ActivityInput { Value1 = "a", Value2 = 1 } }); + var activity = Host.GetPendingActivity("act-1-" + workflowId, "worker1", TimeSpan.FromSeconds(30)).Result; + + if (activity != null) + { + var actInput = (ActivityInput)activity.Parameters; + Host.SubmitActivitySuccess(activity.Token, new ActivityOutput + { + Value1 = actInput.Value1 + "1", + Value2 = actInput.Value2 + 1 + }); + } + + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).ActivityOutput.Should().BeOfType(); + var outData = (GetData(workflowId).ActivityOutput as ActivityOutput); + outData.Value1.Should().Be("a1"); + outData.Value2.Should().Be(2); + } + }*/ + +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/AttachScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/AttachScenario.cs new file mode 100644 index 000000000..1c1fe7bdd --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/AttachScenario.cs @@ -0,0 +1,61 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class AttachScenario : WorkflowTest + { + internal static int Step1Ticker = 0; + internal static int Step2Ticker = 0; + + public class MyDataClass + { + } + + public class GotoWorkflow : IWorkflow + { + public string Id => "GotoWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => + { + Step1Ticker++; + return ExecutionResult.Next(); + }) + .Id("step1") + .If(data => Step1Ticker < 4).Do(then => then + .StartWith(context => + { + Step2Ticker++; + return ExecutionResult.Next(); + }) + .Attach("step1") + ); + } + } + + public AttachScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new MyDataClass()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + Step1Ticker.Should().Be(4); + Step2Ticker.Should().Be(3); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs index b7bb6d78e..db8211b44 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; namespace WorkflowCore.IntegrationTests.Scenarios @@ -24,15 +22,11 @@ protected void Setup() { //setup dependency injection IServiceCollection services = new ServiceCollection(); - services.AddLogging(); + services.AddLogging(x => x.AddConsole()); Configure(services); var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddConsole(LogLevel.Debug); - PersistenceProvider = serviceProvider.GetService(); Host = serviceProvider.GetService(); Host.RegisterWorkflow(); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/BasicScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/BasicScenario.cs index 77b660088..5d5e0d37b 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/BasicScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/BasicScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/CancelledEventScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/CancelledEventScenario.cs index b0675f3f2..61b7a6540 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/CancelledEventScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/CancelledEventScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario.cs index 55c336020..2a6cc8da4 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -54,7 +52,7 @@ public CompensationScenario() [Fact] public void NoExceptionScenario() { - var workflowId = StartWorkflow(new MyDataClass() { ThrowException = false }); + var workflowId = StartWorkflow(new MyDataClass { ThrowException = false }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); @@ -68,7 +66,7 @@ public void NoExceptionScenario() [Fact] public void ExceptionScenario() { - var workflowId = StartWorkflow(new MyDataClass() { ThrowException = true }); + var workflowId = StartWorkflow(new MyDataClass { ThrowException = true }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario2.cs b/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario2.cs new file mode 100644 index 000000000..bbb997d14 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/CompensationScenario2.cs @@ -0,0 +1,87 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class CompensationScenario2 : WorkflowTest + { + public class MyDataClass + { + public bool ThrowException { get; set; } + } + + public class Workflow : IWorkflow + { + public static bool Event1Fired = false; + public static bool Event2Fired = false; + public static bool TailEventFired = false; + public static bool Compensation1Fired = false; + public static bool Compensation2Fired = false; + + public string Id => "CompensationWorkflow2"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Then(context => + { + Event1Fired = true; + if ((context.Workflow.Data as MyDataClass).ThrowException) + throw new Exception(); + Event2Fired = true; + }) + .CompensateWithSequence(seq => seq + .StartWith(context => Compensation1Fired = true) + .Then(context => Compensation2Fired = true) + ) + .Then(context => TailEventFired = true); + } + } + + public CompensationScenario2() + { + Setup(); + Workflow.Event1Fired = false; + Workflow.Event2Fired = false; + Workflow.Compensation1Fired = false; + Workflow.Compensation2Fired = false; + Workflow.TailEventFired = false; + } + + [Fact] + public void NoExceptionScenario() + { + var workflowId = StartWorkflow(new MyDataClass { ThrowException = false }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + Workflow.Event1Fired.Should().BeTrue(); + Workflow.Event2Fired.Should().BeTrue(); + Workflow.Compensation1Fired.Should().BeFalse(); + Workflow.Compensation2Fired.Should().BeFalse(); + Workflow.TailEventFired.Should().BeTrue(); + } + + [Fact] + public void ExceptionScenario() + { + var workflowId = StartWorkflow(new MyDataClass { ThrowException = true }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(1); + Workflow.Event1Fired.Should().BeTrue(); + Workflow.Event2Fired.Should().BeFalse(); + Workflow.Compensation1Fired.Should().BeTrue(); + Workflow.Compensation2Fired.Should().BeTrue(); + Workflow.TailEventFired.Should().BeTrue(); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DataIOScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DataIOScenario.cs index 630bcbfe7..1d4157c96 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/DataIOScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DataIOScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -29,6 +27,14 @@ public class MyDataClass public int Value1 { get; set; } public int Value2 { get; set; } public int Value3 { get; set; } + public decimal Value4 { get; set; } + + public DataSubclass SubValue { get; set; } + } + + public class DataSubclass + { + public decimal Value5 { get; set; } } public class DataIOWorkflow : IWorkflow @@ -53,12 +59,14 @@ public DataIOScenario() [Fact] public void Scenario() { - var workflowId = StartWorkflow(new MyDataClass() { Value1 = 2, Value2 = 3 }); + decimal v4 = 1.235465673450897890m; + var workflowId = StartWorkflow(new MyDataClass {Value1 = 2, Value2 = 3, Value4 = v4, SubValue = new DataSubclass {Value5 = v4}}); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); UnhandledStepErrors.Count.Should().Be(0); GetData(workflowId).Value3.Should().Be(5); + GetData(workflowId).Value4.Should().Be(v4); } } } diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DecisionScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DecisionScenario.cs new file mode 100644 index 000000000..4d9f733fd --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DecisionScenario.cs @@ -0,0 +1,100 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Primitives; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class DecisionScenario : WorkflowTest + { + public class AddNumbers : StepBody + { + public int Input1 { get; set; } + public int Input2 { get; set; } + public int Output { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Output = (Input1 + Input2); + return ExecutionResult.Next(); + } + } + + public class SubtractNumbers : StepBody + { + public int Input1 { get; set; } + public int Input2 { get; set; } + public int Output { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Output = (Input1 - Input2); + return ExecutionResult.Next(); + } + } + + public class MyDataClass + { + public string Op { get; set; } + public int Value1 { get; set; } + public int Value2 { get; set; } + public int Value3 { get; set; } + } + + public class DecisionWorkflow : IWorkflow + { + public string Id => "DecisionWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + var addBranch = builder.CreateBranch() + .StartWith() + .Input(step => step.Input1, data => data.Value1) + .Input(step => step.Input2, data => data.Value2) + .Output(data => data.Value3, step => step.Output); + + var subtractBranch = builder.CreateBranch() + .StartWith() + .Input(step => step.Input1, data => data.Value1) + .Input(step => step.Input2, data => data.Value2) + .Output(data => data.Value3, step => step.Output); + + builder + .StartWith() + .Input(step => step.Expression, data => data.Op) + .Branch("+", addBranch) + .Branch("-", subtractBranch); + } + } + + public DecisionScenario() + { + Setup(); + } + + [Fact] + public void Scenario1() + { + var workflowId = StartWorkflow(new MyDataClass { Op = "+", Value1 = 2, Value2 = 3 }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).Value3.Should().Be(5); + } + + [Fact] + public void Scenario2() + { + var workflowId = StartWorkflow(new MyDataClass { Op = "-", Value1 = 2, Value2 = 3 }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).Value3.Should().Be(-1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DelayScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DelayScenario.cs new file mode 100644 index 000000000..704b27d79 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DelayScenario.cs @@ -0,0 +1,57 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class DelayWorkflow : IWorkflow + { + internal static int Step1Ticker = 0; + internal static int Step2Ticker = 0; + + public string Id => "DelayWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => Step1Ticker++) + .Delay(data => data.WaitTime) + .Then(context => Step2Ticker++); + + } + + public class MyDataClass + { + public TimeSpan WaitTime { get; set; } + } + } + + public class DelayScenario : WorkflowTest + { + public DelayScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + DelayWorkflow.Step1Ticker = 0; + DelayWorkflow.Step2Ticker = 0; + + var workflowId = StartWorkflow(new DelayWorkflow.MyDataClass() + { + WaitTime = Host.Options.PollInterval.Add(TimeSpan.FromSeconds(1)) + }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + DelayWorkflow.Step1Ticker.Should().Be(1); + DelayWorkflow.Step2Ticker.Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs new file mode 100644 index 000000000..fd100fe2d --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs @@ -0,0 +1,254 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Autofac.Extensions.DependencyInjection; +using Autofac; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class DiWorkflow : IWorkflow + { + public string Id => "DiWorkflow"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Output(_ => _.instance1, _ => _.dependency1.Instance) + .Output(_ => _.instance2, _ => _.dependency2.dependency1.Instance) + .Then(context => + { + return ExecutionResult.Next(); + }); + } + } + + public class DiData + { + public int instance1 { get; set; } = -1; + public int instance2 { get; set; } = -1; + } + + public class Dependency1 + { + private static int InstanceCounter = 0; + + public int Instance { get; set; } = ++InstanceCounter; + } + + public class Dependency2 + { + public Dependency1 dependency1 { get; private set; } + + public Dependency2(Dependency1 dependency1) + { + this.dependency1 = dependency1; + } + } + + public class DiStep1 : StepBody + { + public Dependency1 dependency1 { get; private set; } + public Dependency2 dependency2 { get; private set; } + + public DiStep1(Dependency1 dependency1, Dependency2 dependency2) + { + this.dependency1 = dependency1; + this.dependency2 = dependency2; + } + + public override ExecutionResult Run(IStepExecutionContext context) + { + return ExecutionResult.Next(); + } + } + + /// + /// The DI scenarios are design to test whether the scoped / transient dependecies are honoured with + /// various IoC container implementations. The basic premise is that a step has a dependency on + /// two services, one of which has a dependency on the other. + /// + /// We then use the instance numbers of the services to determine whether the container has created a + /// transient instance or a scoped instance + /// + /// if step.dependency2.dependency1.instance == step.dependency1.instance then + /// we can be assured that dependency1 was created in the same scope as dependency 2 + /// + /// otherwise if the instances are different, they were created as transient + /// + /// + public abstract class DiScenario : WorkflowTest + { + protected void ConfigureHost(IServiceProvider serviceProvider) + { + PersistenceProvider = serviceProvider.GetService(); + Host = serviceProvider.GetService(); + Host.RegisterWorkflow(); + Host.OnStepError += Host_OnStepError; + Host.Start(); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiMsTransientScenario", DisableParallelization = true)] + [Collection("DiMsTransientScenario")] + public class DiMsTransientScenario : DiScenario + { + public DiMsTransientScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddLogging(); + ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // DI provider should have created two transient instances, with different instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().NotBe(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiMsScopedScenario", DisableParallelization = true)] + [Collection("DiMsScopedScenario")] + public class DiMsScopedScenario : DiScenario + { + public DiMsScopedScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddLogging(); + ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().Be(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiAutoFacTransientScenario", DisableParallelization = true)] + [Collection("DiAutoFacTransientScenario")] + public class DiAutoFacTransientScenario : DiScenario + { + public DiAutoFacTransientScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + ConfigureServices(services); + + //setup dependency injection + var builder = new ContainerBuilder(); + builder.Populate(services); + builder.RegisterType().InstancePerDependency(); + builder.RegisterType().InstancePerDependency(); + builder.RegisterType().InstancePerDependency(); + var container = builder.Build(); + + var serviceProvider = new AutofacServiceProvider(container); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().NotBe(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiAutoFacScopedScenario", DisableParallelization = true)] + [Collection("DiAutoFacScopedScenario")] + public class DiAutoFacScopedScenario : DiScenario + { + public DiAutoFacScopedScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + ConfigureServices(services); + + //setup dependency injection + var builder = new ContainerBuilder(); + builder.Populate(services); + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().InstancePerLifetimeScope(); + var container = builder.Build(); + + var serviceProvider = new AutofacServiceProvider(container); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().Be(data.instance2); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs new file mode 100644 index 000000000..aa6591f6a --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Testing; +using Xunit; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class DynamicDataIOScenario : WorkflowTest + { + public class AddNumbers : StepBody + { + public int Input1 { get; set; } + public int Input2 { get; set; } + public int Output { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + Output = (Input1 + Input2); + return ExecutionResult.Next(); + } + } + + public class MyDataClass + { + public int Value1 { get; set; } + public int Value2 { get; set; } + public Dictionary Storage { get; set; } = new Dictionary(); + + public int this[string propertyName] + { + get => Storage[propertyName]; + set => Storage[propertyName] = value; + } + } + + public class DataIOWorkflow : IWorkflow + { + public string Id => "DynamicDataIOWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Input(step => step.Input1, data => data.Value1) + .Input(step => step.Input2, data => data.Value2) + .Output((step, data) => data["Value3"] = step.Output); + } + } + + public DynamicDataIOScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new MyDataClass { Value1 = 2, Value2 = 3 }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId)["Value3"].Should().Be(5); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/EndStepScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/EndStepScenario.cs index f950d58b3..f177423a8 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/EndStepScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/EndStepScenario.cs @@ -1,7 +1,5 @@ using FluentAssertions; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Testing; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/EventOrderScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/EventOrderScenario.cs new file mode 100644 index 000000000..79fd2633d --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/EventOrderScenario.cs @@ -0,0 +1,70 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class EventOrderScenario : WorkflowTest + { + public class MyDataClass + { + public int Value1 { get; set; } + public int Value2 { get; set; } + public int Value3 { get; set; } + public int Value4 { get; set; } + public int Value5 { get; set; } + } + + public class EventWorkflow : IWorkflow + { + public string Id => "EventOrder"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .WaitFor("OrderedEvent", data => string.Empty, data => new DateTime(2000, 1, 1, 0, 1, 0)) + .Output(data => data.Value1, step => step.EventData) + .WaitFor("OrderedEvent", data => string.Empty, data => new DateTime(2000, 1, 1, 0, 2, 0)) + .Output(data => data.Value2, step => step.EventData) + .WaitFor("OrderedEvent", data => string.Empty, data => new DateTime(2000, 1, 1, 0, 3, 0)) + .Output(data => data.Value3, step => step.EventData) + .WaitFor("OrderedEvent", data => string.Empty, data => new DateTime(2000, 1, 1, 0, 4, 0)) + .Output(data => data.Value4, step => step.EventData) + .WaitFor("OrderedEvent", data => string.Empty, data => new DateTime(2000, 1, 1, 0, 5, 0)) + .Output(data => data.Value5, step => step.EventData); + } + } + + public EventOrderScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + Host.PublishEvent("OrderedEvent", string.Empty, 1, new DateTime(2000, 1, 1, 0, 1, 1)); + Host.PublishEvent("OrderedEvent", string.Empty, 2, new DateTime(2000, 1, 1, 0, 2, 1)); + Host.PublishEvent("OrderedEvent", string.Empty, 3, new DateTime(2000, 1, 1, 0, 3, 1)); + Host.PublishEvent("OrderedEvent", string.Empty, 4, new DateTime(2000, 1, 1, 0, 4, 1)); + Host.PublishEvent("OrderedEvent", string.Empty, 5, new DateTime(2000, 1, 1, 0, 5, 1)); + + var workflowId = StartWorkflow(new MyDataClass()); + + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + GetData(workflowId).Value1.Should().Be(1); + GetData(workflowId).Value2.Should().Be(2); + GetData(workflowId).Value3.Should().Be(3); + GetData(workflowId).Value4.Should().Be(4); + GetData(workflowId).Value5.Should().Be(5); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/EventScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/EventScenario.cs index 86a6703df..6c5439b18 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/EventScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/EventScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -42,7 +40,7 @@ public EventScenario() public void Scenario() { var eventKey = Guid.NewGuid().ToString(); - var workflowId = StartWorkflow(new MyDataClass() { StrValue1 = eventKey, StrValue2 = eventKey }); + var workflowId = StartWorkflow(new MyDataClass { StrValue1 = eventKey, StrValue2 = eventKey }); WaitForEventSubscription("MyEvent", eventKey, TimeSpan.FromSeconds(30)); Host.PublishEvent("MyEvent", eventKey, "Pass1"); WaitForEventSubscription("MyEvent2", eventKey, TimeSpan.FromSeconds(30)); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/FailingSagaScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/FailingSagaScenario.cs new file mode 100644 index 000000000..b2f83ef65 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/FailingSagaScenario.cs @@ -0,0 +1,63 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class FailingSagaScenario : WorkflowTest + { + public class Workflow : IWorkflow + { + public static int Event1Fired; + public static int Event2Fired; + public static int Event3Fired; + + public string Id => "NestedRetrySaga2Workflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Saga(x => x + .StartWith(context => ExecutionResult.Next()) + .If(data => true) + .Do(i => i + .StartWith(context => + { + Event1Fired++; + throw new Exception(); + }) + ) + .Then(context => Event2Fired++) + ) + .OnError(WorkflowErrorHandling.Terminate) + .Then(context => Event3Fired++); + } + } + + public FailingSagaScenario() + { + Setup(); + Workflow.Event1Fired = 0; + Workflow.Event2Fired = 0; + Workflow.Event3Fired = 0; + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Terminated); + UnhandledStepErrors.Count.Should().Be(1); + Workflow.Event1Fired.Should().Be(1); + Workflow.Event2Fired.Should().Be(0); + Workflow.Event3Fired.Should().Be(0); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ForeachScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachScenario.cs index f10c8ee01..774064b8b 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/ForeachScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachScenario.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -19,7 +18,6 @@ public class ForeachScenario : WorkflowTest Numbers { get; set; } = new List(); + + public bool IsParallel { get; set; } = true; } public class ForeachWorkflow : IWorkflow @@ -44,7 +45,7 @@ public void Build(IWorkflowBuilder builder) Step1Ticker++; return ExecutionResult.Next(); }) - .ForEach(x => new List() { 2, 2, 3 }) + .ForEach(x => x.Numbers, x => x.IsParallel) .Do(x => x.StartWith()) .Then(context => { @@ -58,12 +59,18 @@ public void Build(IWorkflowBuilder builder) public ForeachScenario() { Setup(); + + Step1Ticker = 0; + Step2Ticker = 0; + Step3Ticker = 0; + AfterLoopValue = 0; + CheckSum = 0; } [Fact] public void Scenario() { - var workflowId = StartWorkflow(null); + var workflowId = StartWorkflow(new MyDataClass { Numbers = new List { 2, 2, 3 } }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); Step1Ticker.Should().Be(1); @@ -74,5 +81,20 @@ public void Scenario() GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); UnhandledStepErrors.Count.Should().Be(0); } + + [Fact] + public void EmptyCollectionSequentialScenario() + { + var workflowId = StartWorkflow(new MyDataClass { IsParallel = false }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + Step1Ticker.Should().Be(1); + Step2Ticker.Should().Be(0); + Step3Ticker.Should().Be(1); + AfterLoopValue.Should().Be(0); + CheckSum.Should().Be(0); + } } } diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ForeachSyncScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachSyncScenario.cs new file mode 100644 index 000000000..74e29fd3a --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachSyncScenario.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Primitives; +using WorkflowCore.Testing; +using Xunit; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class ForeachSyncScenario : WorkflowTest + { + public class DoSomething : StepBody + { + public int Counter { get; set; } + + public override ExecutionResult Run(IStepExecutionContext context) + { + return ExecutionResult.Next(); + } + } + + public class MyDataClass + { + public int Counter { get; set; } + } + + public class ForeachSyncWorkflow : IWorkflow + { + public string Id => "ForeachSyncWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(_ => ExecutionResult.Next()) + .ForEach(x => new List { 10, 2, 3 }, _ => false) + .Do(x => x + .StartWith() + .Input(step => step.Period, (_, context) => TimeSpan.FromSeconds((int)context.Item)) + .Then() + .Input(step => step.Counter, (data, context) => (int)context.Item) + .Output(data => data.Counter, step => step.Counter) + ); + } + } + + public ForeachSyncScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + GetData(workflowId).Counter.Should().Be(3); + UnhandledStepErrors.Count.Should().Be(0); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ForeachWithCompensationScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachWithCompensationScenario.cs new file mode 100644 index 000000000..144774fcb --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ForeachWithCompensationScenario.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class ForeachWithCompensationScenario : WorkflowTest + { + internal static int Step1Ticker = 0; + internal static int Step2Ticker = 0; + internal static int Step3Ticker = 0; + internal static int CompensateTicker = 0; + + public class MyDataClass + { + } + + public class ForeachWorkflow : IWorkflow + { + public string Id => "ForeachWithCompensationWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(data => { + Step1Ticker++; + }) + .Then(data => { + Step2Ticker++; + }) + .ForEach(step => new List { 1 }) + .Do(then => then + .Decide(data => 1) + .Branch(1, builder.CreateBranch() + .StartWith(data => { + Step3Ticker++; + throw new Exception(); + }) + .CompensateWithSequence(builder => builder.StartWith(_ => { + CompensateTicker++; + }))) + ); + } + } + + public ForeachWithCompensationScenario() + { + Setup(); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new MyDataClass()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + Step1Ticker.Should().Be(1); + Step2Ticker.Should().Be(1); + Step3Ticker.Should().Be(1); + CompensateTicker.Should().Be(1); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ForkScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ForkScenario.cs index 54fb31432..89a9b46e0 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/ForkScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ForkScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/IfScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/IfScenario.cs index f7e0e7817..8b716715f 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/IfScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/IfScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -78,7 +76,7 @@ public IfScenario() [Fact] public void Scenario() { - var workflowId = StartWorkflow(new MyDataClass() { Counter = 2 }); + var workflowId = StartWorkflow(new MyDataClass { Counter = 2 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); Step1Ticker.Should().Be(1); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/MiddlewareScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/MiddlewareScenario.cs new file mode 100644 index 000000000..530b94a58 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/MiddlewareScenario.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Testing; +using Xunit; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class MiddlewareScenario : WorkflowTest + { + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(5); + private readonly List _workflowMiddleware = new List(); + private readonly List _stepMiddleware = new List(); + private readonly TestStep _step = new TestStep(); + + public MiddlewareScenario() + { + Setup(); + } + + public TestWorkflowMiddleware[] PreMiddleware => _workflowMiddleware + .Where(x => x.Phase == WorkflowMiddlewarePhase.PreWorkflow) + .ToArray(); + + public TestWorkflowMiddleware[] PostMiddleware => _workflowMiddleware + .Where(x => x.Phase == WorkflowMiddlewarePhase.PostWorkflow) + .ToArray(); + + public class MyWorkflow: IWorkflow + { + public string Id => nameof(MyWorkflow); + + public int Version => 1; + + public void Build(IWorkflowBuilder builder) => + builder.StartWith(); + } + + public class TestStep : StepBodyAsync + { + + public DateTime? StartTime { get; private set; } + public DateTime? EndTime { get; private set; } + public bool HasCompleted => StartTime.HasValue && EndTime.HasValue; + + public override async Task RunAsync(IStepExecutionContext context) + { + StartTime = DateTime.UtcNow; + await Task.Delay(Delay); + EndTime = DateTime.UtcNow; + return ExecutionResult.Next(); + } + } + + public class TestWorkflowMiddleware : IWorkflowMiddleware + { + public TestWorkflowMiddleware(WorkflowMiddlewarePhase phase) + { + Phase = phase; + } + + public WorkflowMiddlewarePhase Phase { get; } + + public DateTime? StartTime { get; private set; } + public DateTime? EndTime { get; private set; } + public bool HasCompleted => StartTime.HasValue && EndTime.HasValue; + + public async Task HandleAsync(WorkflowInstance workflow, WorkflowDelegate next) + { + StartTime = DateTime.UtcNow; + await Task.Delay(Delay); + await next(); + await Task.Delay(Delay); + EndTime = DateTime.UtcNow; + } + } + + public class TestStepMiddleware : IWorkflowStepMiddleware + { + public DateTime? StartTime { get; private set; } + public DateTime? EndTime { get; private set; } + + public bool HasCompleted => StartTime.HasValue && EndTime.HasValue; + + public async Task HandleAsync(IStepExecutionContext context, IStepBody body, WorkflowStepDelegate next) + { + StartTime = DateTime.UtcNow; + await Task.Delay(Delay); + var result = await next(); + await Task.Delay(Delay); + EndTime = DateTime.UtcNow; + return result; + } + } + + protected override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + + services.AddTransient(_ => _step); + + // Configure 3 middleware of each type + const int middlewareCount = 3; + foreach (var _ in Enumerable.Range(0, middlewareCount)) + { + var preMiddleware = new TestWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow); + var postMiddleware = new TestWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow); + _workflowMiddleware.Add(preMiddleware); + _workflowMiddleware.Add(postMiddleware); + services.AddWorkflowMiddleware(p => preMiddleware); + services.AddWorkflowMiddleware(p => postMiddleware); + } + + // Configure 3 step middleware + foreach (var _ in Enumerable.Range(0, middlewareCount)) + { + var middleware = new TestStepMiddleware(); + services.AddWorkflowStepMiddleware(p => middleware); + _stepMiddleware.Add(middleware); + } + + } + + [Fact(DisplayName = "Should run all workflow and step middleware")] + public async Task Should_run_all_workflow_and_step_middleware() + { + var workflowId = await StartWorkflowAsync(new object()); + var status = await WaitForWorkflowToCompleteAsync(workflowId, Timeout); + + // Workflow should complete without errors + status.Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + + // Wait for post middleware to complete + while (_workflowMiddleware.Any(x => !x.HasCompleted)) + { + await Task.Delay(500); + } + + // Each middleware should have run + _workflowMiddleware.Should() + .HaveCount(6).And + .OnlyContain(x => x.HasCompleted); + _stepMiddleware.Should() + .HaveCount(3) + .And + .OnlyContain(x => x.HasCompleted); + + // Step middleware should have been run in order + _stepMiddleware.Should().BeInAscendingOrder(x => x.StartTime); + _stepMiddleware.Should().BeInDescendingOrder(x => x.EndTime); + + // Step should have been called after all step middleware + _step.HasCompleted.Should().BeTrue(); + _step.StartTime.Should().BeAfter(_stepMiddleware.Last().StartTime.Value); + _step.EndTime.Should().BeBefore(_stepMiddleware.Last().EndTime.Value); + + // Pre workflow middleware should have been run in order + PreMiddleware.Should().BeInAscendingOrder(x => x.StartTime); + PreMiddleware.Should().BeInDescendingOrder(x => x.EndTime); + + // Post workflow middleware should have been run in order + PostMiddleware.Should().BeInAscendingOrder(x => x.StartTime); + PostMiddleware.Should().BeInDescendingOrder(x => x.EndTime); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario.cs new file mode 100644 index 000000000..0f080504a --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario.cs @@ -0,0 +1,81 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class MultistepCompensationScenario : WorkflowTest + { + public class Workflow : IWorkflow + { + public static int Compensation1Fired = -1; + public static int Compensation2Fired = -1; + public static int Compensation3Fired = -1; + public static int Compensation4Fired = -1; + public static int CompensationCounter = 0; + + public string Id => "CompensatingWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Saga(x => x + .StartWith(context => ExecutionResult.Next()) + .CompensateWith(context => + { + CompensationCounter++; + Compensation1Fired = CompensationCounter; + }) + .Then(context => ExecutionResult.Next()) + .CompensateWithSequence(context => context.StartWith(c => + { + CompensationCounter++; + Compensation2Fired = CompensationCounter; + })) + .Then(context => ExecutionResult.Next()) + .CompensateWith(context => + { + CompensationCounter++; + Compensation3Fired = CompensationCounter; + }) + .Then(context => throw new Exception()) + .CompensateWith(context => + { + CompensationCounter++; + Compensation4Fired = CompensationCounter; + }) + ); + } + } + + public MultistepCompensationScenario() + { + Setup(); + Workflow.Compensation1Fired = -1; + Workflow.Compensation2Fired = -1; + Workflow.Compensation3Fired = -1; + Workflow.Compensation4Fired = -1; + Workflow.CompensationCounter = 0; + } + + [Fact] + public void MultiCompensationStepOrder() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(1); + + Workflow.Compensation1Fired.Should().Be(4); + Workflow.Compensation2Fired.Should().Be(3); + Workflow.Compensation3Fired.Should().Be(2); + Workflow.Compensation4Fired.Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario2.cs b/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario2.cs new file mode 100644 index 000000000..ec0888501 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario2.cs @@ -0,0 +1,97 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class MultistepCompensationScenario2 : WorkflowTest + { + public class Workflow : IWorkflow + { + public static int CompensationCounter = 0; + public static int Compensation1_1Fired = 0; + public static int Compensation1_2Fired = 0; + public static int Compensation2_1Fired = 0; + public static int Compensation2_2Fired = 0; + public static int Compensation3Fired = 0; + public static int Compensation4Fired = 0; + + + public string Id => "MultistepCompensationScenario2"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Saga(x => x + .StartWith(context => ExecutionResult.Next()) + .CompensateWithSequence(context => context.StartWith(c => + { + CompensationCounter++; + Compensation1_1Fired = CompensationCounter; + }) + .Then(c => + { + CompensationCounter++; + Compensation1_2Fired = CompensationCounter; + })) + .If(c => true).Do(then => then + .Then(context => ExecutionResult.Next()) + .CompensateWithSequence(context => context.StartWith(c => + { + CompensationCounter++; + Compensation2_1Fired = CompensationCounter; + }).Then(c => + { + CompensationCounter++; + Compensation2_2Fired = CompensationCounter; + })) + .Then(context => ExecutionResult.Next()) + .CompensateWith(context => + { + CompensationCounter++; + Compensation3Fired = CompensationCounter; + }) + .Then(context => throw new Exception()) + .CompensateWith(context => + { + CompensationCounter++; + Compensation4Fired = CompensationCounter; + })) + ); + } + } + + public MultistepCompensationScenario2() + { + Setup(); + Workflow.Compensation1_1Fired = -1; + Workflow.Compensation1_2Fired = -1; + Workflow.Compensation2_1Fired = -1; + Workflow.Compensation2_2Fired = -1; + Workflow.Compensation3Fired = -1; + Workflow.Compensation4Fired = -1; + Workflow.CompensationCounter = 0; + } + + [Fact] + public void MultiCompensationStepOrder() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(1); + + Workflow.Compensation1_2Fired.Should().Be(6); + Workflow.Compensation1_1Fired.Should().Be(5); + Workflow.Compensation2_2Fired.Should().Be(4); + Workflow.Compensation2_1Fired.Should().Be(3); + Workflow.Compensation3Fired.Should().Be(2); + Workflow.Compensation4Fired.Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/NestedRetrySagaScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/NestedRetrySagaScenario.cs new file mode 100644 index 000000000..f8b3c1a08 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/NestedRetrySagaScenario.cs @@ -0,0 +1,88 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using WorkflowCore.Testing; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class NestedRetrySagaScenario : WorkflowTest + { + public class MyDataClass + { + } + + public class Workflow : IWorkflow + { + public static int Event1Fired; + public static int Event2Fired; + public static int Event3Fired; + public static int TailEventFired; + public static int Compensation1Fired; + public static int Compensation2Fired; + public static int Compensation3Fired; + public static int Compensation4Fired; + + public string Id => "NestedRetrySagaWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .CompensateWith(context => Compensation1Fired++) + .Saga(x => x + .StartWith(context => ExecutionResult.Next()) + .CompensateWith(context => Compensation2Fired++) + .If(data => true) + .Do(i => i + .StartWith(context => + { + Event1Fired++; + if (Event1Fired < 3) + throw new Exception(); + Event2Fired++; + }) + .CompensateWith(context => Compensation3Fired++) + ) + .Then(context => Event3Fired++) + .CompensateWith(context => Compensation4Fired++) + ) + .OnError(WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(1)) + .Then(context => TailEventFired++); + } + } + + public NestedRetrySagaScenario() + { + Setup(); + Workflow.Event1Fired = 0; + Workflow.Event2Fired = 0; + Workflow.Event3Fired = 0; + Workflow.Compensation1Fired = 0; + Workflow.Compensation2Fired = 0; + Workflow.Compensation3Fired = 0; + Workflow.Compensation4Fired = 0; + Workflow.TailEventFired = 0; + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new MyDataClass()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(2); + Workflow.Event1Fired.Should().Be(3); + Workflow.Event2Fired.Should().Be(1); + Workflow.Event3Fired.Should().Be(1); + Workflow.Compensation1Fired.Should().Be(0); + Workflow.Compensation2Fired.Should().Be(2); + Workflow.Compensation3Fired.Should().Be(2); + Workflow.Compensation4Fired.Should().Be(0); + Workflow.TailEventFired.Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ParallelEventsScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ParallelEventsScenario.cs new file mode 100644 index 000000000..2c3b6825f --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ParallelEventsScenario.cs @@ -0,0 +1,94 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public sealed class ParallelEventsScenario + : WorkflowTest + { + private const string EVENT_KEY = nameof(EVENT_KEY); + + public class MyDataClass + { + public string StrValue1 { get; set; } + public string StrValue2 { get; set; } + } + + public class SomeTask : StepBodyAsync + { + public TimeSpan Delay { get; set; } + + public override async Task RunAsync(IStepExecutionContext context) + { + await Task.Delay(Delay); + + return ExecutionResult.Next(); + } + } + + public class ParallelEventsWorkflow : IWorkflow + { + public string Id => nameof(ParallelEventsScenario); + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .Parallel() + .Do(then => + then.WaitFor("Event1", data => EVENT_KEY).Then() + .Input(step => step.Delay, data => TimeSpan.FromMilliseconds(2000))) + .Do(then => + then.WaitFor("Event2", data => EVENT_KEY).Then() + .Input(step => step.Delay, data => TimeSpan.FromMilliseconds(2000))) + .Do(then => + then.WaitFor("Event3", data => EVENT_KEY).Then() + .Input(step => step.Delay, data => TimeSpan.FromMilliseconds(5000))) + .Do(then => + then.WaitFor("Event4", data => EVENT_KEY).Then() + .Input(step => step.Delay, data => TimeSpan.FromMilliseconds(100))) + .Do(then => + then.WaitFor("Event5", data => EVENT_KEY).Then() + .Input(step => step.Delay, data => TimeSpan.FromMilliseconds(100))) + .Join() + .Then(x => + { + return ExecutionResult.Next(); + }); + } + } + + public ParallelEventsScenario() + { + Setup(); + } + + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(s => s.UsePollInterval(TimeSpan.FromSeconds(1))); + } + + [Fact] + public async Task Scenario() + { + var eventKey = Guid.NewGuid().ToString(); + var workflowId = await StartWorkflowAsync(new MyDataClass { StrValue1 = eventKey, StrValue2 = eventKey }); + await Host.PublishEvent("Event1", EVENT_KEY, "Pass1"); + await Host.PublishEvent("Event2", EVENT_KEY, "Pass2"); + await Host.PublishEvent("Event3", EVENT_KEY, "Pass3"); + await Host.PublishEvent("Event4", EVENT_KEY, "Pass4"); + await Host.PublishEvent("Event5", EVENT_KEY, "Pass5"); + + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/ParallelScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/ParallelScenario.cs index 12decbf73..f63041858 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/ParallelScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ParallelScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaScenario.cs index 70ce71510..c4ac48238 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaWithUserTaskScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaWithUserTaskScenario.cs new file mode 100644 index 000000000..ccd67a7b8 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/RetrySagaWithUserTaskScenario.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using System.Linq; +using System.Threading.Tasks; +using WorkflowCore.Testing; +using WorkflowCore.Users.Models; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class RetrySagaWithUserTaskScenario : WorkflowTest + { + public class MyDataClass + { + } + + public class Workflow : IWorkflow + { + public static int Event1Fired; + public static int Event2Fired; + public static int Event3Fired; + public static int TailEventFired; + public static int Compensation1Fired; + public static int Compensation2Fired; + public static int Compensation3Fired; + public static int Compensation4Fired; + + public string Id => "RetrySagaWithUserTaskWorkflow"; + public int Version => 1; + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .CompensateWith(context => Compensation1Fired++) + .Saga(x => x + .StartWith(context => ExecutionResult.Next()) + .CompensateWith(context => Compensation2Fired++) + .UserTask("prompt", data => "assigner") + .WithOption("a", "Option A") + .Do(wb => wb + .StartWith(context => ExecutionResult.Next()) + .Then(context => + { + Event1Fired++; + if (Event1Fired < 3) + throw new Exception(); + Event2Fired++; + }) + .CompensateWith(context => Compensation3Fired++) + .Then(context => Event3Fired++) + .CompensateWith(context => Compensation4Fired++) + ) + ) + .OnError(WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(1)) + .Then(context => TailEventFired++); + } + } + + public RetrySagaWithUserTaskScenario() + { + Setup(); + Workflow.Event1Fired = 0; + Workflow.Event2Fired = 0; + Workflow.Event3Fired = 0; + Workflow.Compensation1Fired = 0; + Workflow.Compensation2Fired = 0; + Workflow.Compensation3Fired = 0; + Workflow.Compensation4Fired = 0; + Workflow.TailEventFired = 0; + } + + [Fact] + public async Task Scenario() + { + var workflowId = StartWorkflow(new MyDataClass()); + var instance = await Host.PersistenceStore.GetWorkflowInstance(workflowId); + + string oldUserOptionKey = null; + for (var i = 0; i != 3; ++i) + { + var userOptions = await WaitForDifferentUserStepAsync(instance, TimeSpan.FromSeconds(1), oldUserOptionKey); + userOptions.Count.Should().Be(1); + + var userOption = userOptions.Single(); + userOption.Prompt.Should().Be("prompt"); + userOption.AssignedPrincipal.Should().Be("assigner"); + userOption.Options.Count.Should().Be(1); + + var selectionOption = userOption.Options.Single(); + selectionOption.Key.Should().Be("Option A"); + selectionOption.Value.Should().Be("a"); + await Host.PublishUserAction(userOption.Key, string.Empty, selectionOption.Value); + + oldUserOptionKey = userOption.Key; + } + + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(2); + Workflow.Event1Fired.Should().Be(3); + Workflow.Event2Fired.Should().Be(1); + Workflow.Event3Fired.Should().Be(1); + Workflow.Compensation1Fired.Should().Be(0); + Workflow.Compensation2Fired.Should().Be(2); + Workflow.Compensation3Fired.Should().Be(2); + Workflow.Compensation4Fired.Should().Be(0); + Workflow.TailEventFired.Should().Be(1); + } + + private static async Task> WaitForDifferentUserStepAsync( + WorkflowInstance instance, + TimeSpan timeout, + string oldUserActionKey = null) + { + var startTime = DateTime.UtcNow; + + while (DateTime.UtcNow - startTime <= timeout) + { + var userActions = await WaitForUserStepAsync(instance); + + if (oldUserActionKey != null && userActions.Any(x => x.Key == oldUserActionKey)) + { + continue; + } + + return userActions; + } + + return Array.Empty(); + } + + private static async Task> WaitForUserStepAsync(WorkflowInstance instance) + { + var delayCount = 200; + var openActions = instance.GetOpenUserActions()?.ToList(); + while ((openActions?.Count ?? 0) == 0) + { + await Task.Delay(TimeSpan.FromMilliseconds(100)); + openActions = instance.GetOpenUserActions()?.ToList(); + if (delayCount-- == 0) + { + break; + } + } + + return openActions; + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/SagaScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/SagaScenario.cs index fea1e4402..de3dc1105 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/SagaScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/SagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -75,7 +73,7 @@ public SagaScenario() [Fact] public void NoExceptionScenario() { - var workflowId = StartWorkflow(new MyDataClass() { ThrowException = false }); + var workflowId = StartWorkflow(new MyDataClass { ThrowException = false }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); @@ -95,7 +93,7 @@ public void NoExceptionScenario() [Fact] public void ExceptionScenario() { - var workflowId = StartWorkflow(new MyDataClass() { ThrowException = true }); + var workflowId = StartWorkflow(new MyDataClass { ThrowException = true }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/StoredJsonScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/StoredJsonScenario.cs new file mode 100644 index 000000000..383eb39a4 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/StoredJsonScenario.cs @@ -0,0 +1,88 @@ +using System; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; +using WorkflowCore.TestAssets.DataTypes; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class StoredJsonScenario : JsonWorkflowTest + { + public StoredJsonScenario() + { + Setup(); + } + + [Fact(DisplayName = "Execute branch 1")] + public void should_execute_branch1() + { + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard { Flag1 = true, Flag2 = true, Flag3 = true }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + var data = GetData(workflowId); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + data.Counter1.Should().Be(1); + data.Counter2.Should().Be(1); + data.Counter3.Should().Be(1); + data.Counter4.Should().Be(1); + data.Counter5.Should().Be(0); + data.Counter6.Should().Be(1); + data.Counter7.Should().Be(1); + data.Counter8.Should().Be(0); + data.Counter10.Should().Be(1); + } + + [Fact(DisplayName = "Execute branch 2")] + public void should_execute_branch2() + { + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard { Flag1 = true, Flag2 = true, Flag3 = false }); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + var data = GetData(workflowId); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + data.Counter1.Should().Be(1); + data.Counter2.Should().Be(1); + data.Counter3.Should().Be(1); + data.Counter4.Should().Be(1); + data.Counter5.Should().Be(0); + data.Counter6.Should().Be(1); + data.Counter7.Should().Be(0); + data.Counter8.Should().Be(1); + data.Counter10.Should().Be(1); + } + + [Fact] + public void should_execute_json_workflow_with_dynamic_data() + { + var initialData = new DynamicData + { + ["Flag1"] = true, + ["Flag2"] = true, + ["Counter1"] = 0, + ["Counter2"] = 0, + ["Counter3"] = 0, + ["Counter4"] = 0, + ["Counter5"] = 0, + ["Counter6"] = 0, + ["Counter10"] = 0 + }; + + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionDynamicJson(), initialData); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); + + var data = GetData(workflowId); + GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); + UnhandledStepErrors.Count.Should().Be(0); + data["Counter1"].Should().Be(1); + data["Counter2"].Should().Be(1); + data["Counter3"].Should().Be(1); + data["Counter4"].Should().Be(1); + data["Counter5"].Should().Be(0); + data["Counter6"].Should().Be(1); + data["Counter10"].Should().Be(1); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/StoredScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/StoredYamlScenario.cs similarity index 71% rename from test/WorkflowCore.IntegrationTests/Scenarios/StoredScenario.cs rename to test/WorkflowCore.IntegrationTests/Scenarios/StoredYamlScenario.cs index f5636751a..95243ff03 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/StoredScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/StoredYamlScenario.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; using FluentAssertions; @@ -10,17 +7,17 @@ namespace WorkflowCore.IntegrationTests.Scenarios { - public class StoredScenario : JsonWorkflowTest - { - public StoredScenario() + public class StoredYamlScenario : YamlWorkflowTest + { + public StoredYamlScenario() { Setup(); } - [Fact(DisplayName = "Execute workflow from stored JSON definition")] - public void Scenario() + [Fact(DisplayName = "Execute workflow from stored YAML definition")] + public void should_execute_yaml_workflow() { - var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard() { Flag1 = true, Flag2 = true }); + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionYaml(), new CounterBoard { Flag1 = true, Flag2 = true }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); var data = GetData(workflowId); @@ -32,6 +29,7 @@ public void Scenario() data.Counter4.Should().Be(1); data.Counter5.Should().Be(0); data.Counter6.Should().Be(1); + data.Counter10.Should().Be(1); } } } diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/UserScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/UserScenario.cs index b99ca99a8..66d442018 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/UserScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/UserScenario.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; using FluentAssertions; -using FluentAssertions.Collections; -using WorkflowCore.Users.Models; using System.Linq; using WorkflowCore.Testing; diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/WhenScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/WhenScenario.cs index 95103dc63..b56d4c81d 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/WhenScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/WhenScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -71,7 +69,7 @@ public WhenScenario() [Fact] public void Scenario() { - var workflowId = StartWorkflow(new MyDataClass() { Counter = 2 }); + var workflowId = StartWorkflow(new MyDataClass { Counter = 2 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); Case1Ticker.Should().Be(0); diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/WhileScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/WhileScenario.cs index 67d364ced..f91209d44 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/WhileScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/WhileScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -69,7 +67,7 @@ public WhileScenario() [Fact] public void Scenario() { - var workflowId = StartWorkflow(new MyDataClass() { Counter = 0 }); + var workflowId = StartWorkflow(new MyDataClass { Counter = 0 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); Step1Ticker.Should().Be(1); diff --git a/test/WorkflowCore.IntegrationTests/SearchIndexTests.cs b/test/WorkflowCore.IntegrationTests/SearchIndexTests.cs new file mode 100644 index 000000000..cc9847574 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/SearchIndexTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.Search; + +namespace WorkflowCore.IntegrationTests +{ + public abstract class SearchIndexTests + { + protected abstract ISearchIndex CreateService(); + protected ISearchIndex Subject { get; set; } + + protected SearchIndexTests() + { + Subject = CreateService(); + Subject.Start().Wait(); + + foreach (var item in BuildTestData()) + Subject.IndexWorkflow(item).Wait(); + System.Threading.Thread.Sleep(1000); + } + + protected IEnumerable BuildTestData() + { + var result = new List(); + + result.Add(new WorkflowInstance + { + Id = "1", + CreateTime = new DateTime(2010, 1, 1), + Status = WorkflowStatus.Runnable, + Reference = "ref1" + }); + + result.Add(new WorkflowInstance + { + Id = "2", + CreateTime = new DateTime(2020, 1, 1), + Status = WorkflowStatus.Runnable, + Reference = "ref2", + Data = new DataObject + { + Value3 = 7 + } + }); + + result.Add(new WorkflowInstance + { + Id = "3", + CreateTime = new DateTime(2010, 1, 1), + Status = WorkflowStatus.Complete, + Reference = "ref3", + Data = new DataObject + { + Value3 = 5, + Value1 = "quick fox", + Value2 = "lazy dog" + } + }); + + result.Add(new WorkflowInstance + { + Id = "4", + CreateTime = new DateTime(2010, 1, 1), + Status = WorkflowStatus.Complete, + Reference = "ref4", + Data = new AltDataObject + { + Value1 = 9, + Value2 = new DateTime(2000, 1, 1) + } + }); + + return result; + } + + + [Fact] + public async void should_search_on_reference() + { + var result1 = await Subject.Search("ref1", 0, 10); + var result2 = await Subject.Search("ref2", 0, 10); + + result1.Data.Should().Contain(x => x.Id == "1"); + result1.Data.Should().NotContain(x => x.Id == "2"); + result1.Data.Should().NotContain(x => x.Id == "3"); + + result2.Data.Should().NotContain(x => x.Id == "1"); + result2.Data.Should().Contain(x => x.Id == "2"); + result2.Data.Should().NotContain(x => x.Id == "3"); + } + + [Fact] + public async void should_search_on_custom_data() + { + var result = await Subject.Search("dog fox", 0, 10); + + result.Data.Should().NotContain(x => x.Id == "1"); + result.Data.Should().NotContain(x => x.Id == "2"); + result.Data.Should().Contain(x => x.Id == "3"); + } + + [Fact] + public async void should_filter_on_custom_data() + { + var result = await Subject.Search(null, 0, 10, ScalarFilter.Equals(x => x.Value3, 7)); + + result.Data.Should().NotContain(x => x.Id == "1"); + result.Data.Should().Contain(x => x.Id == "2"); + result.Data.Should().NotContain(x => x.Id == "3"); + } + + [Fact] + public async void should_filter_on_alt_custom_data_with_conflicting_names() + { + var result1 = await Subject.Search(null, 0, 10, ScalarFilter.Equals(x => x.Value1, 9)); + var result2 = await Subject.Search(null, 0, 10, DateRangeFilter.Between(x => x.Value2, new DateTime(1999, 12, 31), new DateTime(2000, 1, 2))); + + result1.Data.Should().Contain(x => x.Id == "4"); + result2.Data.Should().Contain(x => x.Id == "4"); + } + + [Fact] + public async void should_filter_on_reference() + { + var result = await Subject.Search(null, 0, 10, ScalarFilter.Equals(x => x.Reference, "ref2")); + + result.Data.Should().NotContain(x => x.Id == "1"); + result.Data.Should().Contain(x => x.Id == "2"); + result.Data.Should().NotContain(x => x.Id == "3"); + } + + [Fact] + public async void should_filter_on_status() + { + var result = await Subject.Search(null, 0, 10, StatusFilter.Equals(WorkflowStatus.Runnable)); + + result.Data.Should().NotContain(x => x.Status != WorkflowStatus.Runnable); + result.Data.Should().Contain(x => x.Status == WorkflowStatus.Runnable); + } + + [Fact] + public async void should_filter_on_date_range() + { + var start = new DateTime(2000, 1, 1); + var end = new DateTime(2015, 1, 1); + var result = await Subject.Search(null, 0, 10, DateRangeFilter.Between(x => x.CreateTime, start, end)); + + result.Data.Should().NotContain(x => x.CreateTime < start || x.CreateTime > end); + result.Data.Should().Contain(x => x.CreateTime > start && x.CreateTime < end); + } + + class DataObject : ISearchable + { + public string Value1 { get; set; } + public string Value2 { get; set; } + + public int Value3 { get; set; } + + public IEnumerable GetSearchTokens() + { + return new List + { + Value1, + Value2 + }; + } + } + + class AltDataObject + { + public int Value1 { get; set; } + public DateTime Value2 { get; set; } + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index 957f8a481..bdc973728 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.IntegrationTests WorkflowCore.IntegrationTests true @@ -14,18 +13,11 @@ - + - - - - - - - - + diff --git a/test/WorkflowCore.TestAssets/DataTypes/CounterBoard.cs b/test/WorkflowCore.TestAssets/DataTypes/CounterBoard.cs index 3ef48c525..a93b43585 100644 --- a/test/WorkflowCore.TestAssets/DataTypes/CounterBoard.cs +++ b/test/WorkflowCore.TestAssets/DataTypes/CounterBoard.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.TestAssets.DataTypes { @@ -15,6 +13,7 @@ public class CounterBoard public int Counter7 { get; set; } public int Counter8 { get; set; } public int Counter9 { get; set; } + public int Counter10 { get; set; } public bool Flag1 { get; set; } public bool Flag2 { get; set; } public bool Flag3 { get; set; } diff --git a/test/WorkflowCore.TestAssets/DataTypes/CounterBoardWithDynamicData.cs b/test/WorkflowCore.TestAssets/DataTypes/CounterBoardWithDynamicData.cs new file mode 100644 index 000000000..3a708724d --- /dev/null +++ b/test/WorkflowCore.TestAssets/DataTypes/CounterBoardWithDynamicData.cs @@ -0,0 +1,10 @@ +using System; + +namespace WorkflowCore.TestAssets.DataTypes +{ + public class CounterBoardWithDynamicData: CounterBoard + { + public DynamicData DynamicDataInstance { get; set; } + public CounterBoard CounterBoardInstance { get; set; } + } +} diff --git a/test/WorkflowCore.TestAssets/DataTypes/DynamicData.cs b/test/WorkflowCore.TestAssets/DataTypes/DynamicData.cs new file mode 100644 index 000000000..508ecda1e --- /dev/null +++ b/test/WorkflowCore.TestAssets/DataTypes/DynamicData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace WorkflowCore.TestAssets.DataTypes +{ + public class DynamicData + { + public Dictionary Storage { get; set; } = new Dictionary(); + + public object this[string propertyName] + { + get => Storage.TryGetValue(propertyName, out var value) ? value : null; + set => Storage[propertyName] = value; + } + } +} diff --git a/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs b/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs index 29f0a8f7e..7ea5218bb 100644 --- a/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs +++ b/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs @@ -1,14 +1,12 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; +using System.Threading.Tasks; using WorkflowCore.Interface; -using NUnit; using FluentAssertions; using NUnit.Framework; namespace WorkflowCore.TestAssets.LockProvider -{ +{ public abstract class DistributedLockProviderTests { protected IDistributedLockProvider Subject; @@ -20,10 +18,10 @@ public void Setup() Subject.Start(); } - protected abstract IDistributedLockProvider CreateProvider(); + protected abstract IDistributedLockProvider CreateProvider(); [Test] - public async void AcquiresLock() + public async Task AcquiresLock() { const string lock1 = "lock1"; const string lock2 = "lock2"; @@ -35,7 +33,7 @@ public async void AcquiresLock() } [Test] - public async void DoesNotAcquireWhenLocked() + public async Task DoesNotAcquireWhenLocked() { const string lock1 = "lock1"; await Subject.AcquireLock(lock1, new CancellationToken()); @@ -46,7 +44,7 @@ public async void DoesNotAcquireWhenLocked() } [Test] - public async void ReleasesLock() + public async Task ReleasesLock() { const string lock1 = "lock1"; await Subject.AcquireLock(lock1, new CancellationToken()); diff --git a/test/WorkflowCore.TestAssets/Properties/AssemblyInfo.cs b/test/WorkflowCore.TestAssets/Properties/AssemblyInfo.cs index 31e55a72d..1fccce37d 100644 --- a/test/WorkflowCore.TestAssets/Properties/AssemblyInfo.cs +++ b/test/WorkflowCore.TestAssets/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs b/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs index 83ec55dd1..d4b47397a 100644 --- a/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs +++ b/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs @@ -10,7 +10,6 @@ namespace WorkflowCore.TestAssets.Properties { using System; - using System.Reflection; /// @@ -20,7 +19,7 @@ namespace WorkflowCore.TestAssets.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -40,7 +39,7 @@ internal Resources() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WorkflowCore.TestAssets.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WorkflowCore.TestAssets.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -71,21 +70,28 @@ internal Resources() { /// { /// "Id": "Step1", /// "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", - /// - /// "NextStepId": "Generate" + /// "ErrorBehavior": "Retry", + /// "Inputs": { "Value": "data.Counter1" }, + /// "Outputs": { "Counter1": "step.Value" }, + /// "NextStepId": "Step2" /// }, /// { - /// "Id": "Generate", - /// "StepType": "ScratchPad.GenerateMessage, ScratchPad", - /// "NextStepId": "Print", - /// "Outputs": { "Value3": "step.Message" } - /// }, - /// { - /// "I [rest of string was truncated]";. + /// "Id": "Step2", + /// "StepType": "WorkflowCore.TestAsset [rest of string was truncated]";. + /// + public static string stored_definition_json { + get { + return ResourceManager.GetString("stored_definition_json", resourceCulture); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. /// - public static string stored_definition { + public static byte[] stored_definition_yaml { get { - return ResourceManager.GetString("stored_definition", resourceCulture); + object obj = ResourceManager.GetObject("stored_definition_yaml", resourceCulture); + return ((byte[])(obj)); } } } diff --git a/test/WorkflowCore.TestAssets/Properties/Resources.resx b/test/WorkflowCore.TestAssets/Properties/Resources.resx index 78ec93e80..5ed7638ee 100644 --- a/test/WorkflowCore.TestAssets/Properties/Resources.resx +++ b/test/WorkflowCore.TestAssets/Properties/Resources.resx @@ -118,7 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + ..\stored-definition.json;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\stored-definition.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/test/WorkflowCore.TestAssets/Steps/Counter.cs b/test/WorkflowCore.TestAssets/Steps/Counter.cs index af5b433f4..64d276199 100644 --- a/test/WorkflowCore.TestAssets/Steps/Counter.cs +++ b/test/WorkflowCore.TestAssets/Steps/Counter.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/test/WorkflowCore.TestAssets/Utils.cs b/test/WorkflowCore.TestAssets/Utils.cs index 87e523494..b7a7919cd 100644 --- a/test/WorkflowCore.TestAssets/Utils.cs +++ b/test/WorkflowCore.TestAssets/Utils.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Newtonsoft.Json; using System.IO; @@ -9,8 +7,8 @@ namespace WorkflowCore.TestAssets { public static class Utils { - private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All, DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Utc }; - + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All, DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Utc }; + public static T DeepCopy(T obj) { string str = JsonConvert.SerializeObject(obj, SerializerSettings); @@ -20,9 +18,27 @@ public static T DeepCopy(T obj) public static string GetTestDefinitionJson() { - //return Properties.Resources.ResourceManager.GetString("stored_definition"); return File.ReadAllText("stored-definition.json"); } + + public static string GetTestDefinitionYaml() + { + return File.ReadAllText("stored-definition.yaml"); + } + + public static string GetTestDefinitionDynamicJson() + { + return File.ReadAllText("stored-dynamic-definition.json"); + } + public static string GetTestDefinitionDynamicYaml() + { + return File.ReadAllText("stored-dynamic-definition.yaml"); + } + + public static string GetTestDefinitionJsonMissingInputProperty() + { + return File.ReadAllText("stored-def-missing-input-property.json"); + } } } diff --git a/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj b/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj index bb6e46dae..cbba0dccb 100644 --- a/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj +++ b/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj @@ -1,11 +1,8 @@  - netstandard1.6 WorkflowCore.TestAssets WorkflowCore.TestAssets - 1.6.1 - $(PackageTargetFallback);dnxcore50 false false false @@ -13,12 +10,27 @@ + + + + + Always + + + Always + + + Always + Always + + Always + @@ -26,9 +38,7 @@ - - - + diff --git a/test/WorkflowCore.TestAssets/stored-def-missing-input-property.json b/test/WorkflowCore.TestAssets/stored-def-missing-input-property.json new file mode 100644 index 000000000..dbf43105a --- /dev/null +++ b/test/WorkflowCore.TestAssets/stored-def-missing-input-property.json @@ -0,0 +1,19 @@ +{ + "Id": "Test", + "Version": 1, + "Description": "", + "DataType": "WorkflowCore.TestAssets.DataTypes.CounterBoard, WorkflowCore.TestAssets", + "Steps": [ + { + "Id": "Step1", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "ErrorBehavior": "Retry", + "Inputs": { + "Value1": "data.Counter1" + }, + "Outputs": { + "Counter1": "step.Value" + } + } + ] +} \ No newline at end of file diff --git a/test/WorkflowCore.TestAssets/stored-definition.json b/test/WorkflowCore.TestAssets/stored-definition.json index 1b131d42f..7c77806b3 100644 --- a/test/WorkflowCore.TestAssets/stored-definition.json +++ b/test/WorkflowCore.TestAssets/stored-definition.json @@ -7,6 +7,7 @@ { "Id": "Step1", "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "ErrorBehavior": "Retry", "Inputs": { "Value": "data.Counter1" }, "Outputs": { "Counter1": "step.Value" }, "NextStepId": "Step2" @@ -57,14 +58,56 @@ "Inputs": { "Value": "data.Counter5" }, "Outputs": { "Counter5": "step.Value" } } + ], + [ + { + "Id": "Step3.3.1", + "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", + "NextStepId": "Step3.3.2", + "CancelCondition": "data.Flag2", + "ProceedOnCancel": true, + "Inputs": { + "EventName": "\"Event1\"", + "EventKey": "\"Key1\"", + "EffectiveDate": "DateTime.Now" + } + }, + { + "Id": "Step3.3.2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data.Counter10" }, + "Outputs": { "Counter10": "step.Value" } + } ] ] }, { "Id": "Step4", + "NextStepId": "decide", "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", "Inputs": { "Value": "data.Counter6" }, "Outputs": { "Counter6": "step.Value" } + }, + { + "Id": "decide", + "StepType": "WorkflowCore.Primitives.Decide, WorkflowCore", + "SelectNextStep": { + "Outcome1": "data.Flag3", + "Outcome2": "!data.Flag3" + } + }, + { + "Id": "Outcome1", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data.Counter7" }, + "Outputs": { "Counter7": "step.Value" } + }, + { + "Id": "Outcome2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data.Counter8" }, + "Outputs": { "Counter8": "step.Value" } } + ] -} \ No newline at end of file +} diff --git a/test/WorkflowCore.TestAssets/stored-definition.yaml b/test/WorkflowCore.TestAssets/stored-definition.yaml new file mode 100644 index 000000000..e469b1e88 --- /dev/null +++ b/test/WorkflowCore.TestAssets/stored-definition.yaml @@ -0,0 +1,73 @@ +Id: Test +Version: 1 +DataType: WorkflowCore.TestAssets.DataTypes.CounterBoard, WorkflowCore.TestAssets +Steps: +- Id: Step1 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + ErrorBehavior: Retry + Inputs: + Value: data.Counter1 + Outputs: + Counter1: step.Value + NextStepId: Step2 +- Id: Step2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter2 + Outputs: + Counter2: step.Value + NextStepId: Step3 +- Id: Step3 + StepType: WorkflowCore.Primitives.If, WorkflowCore + NextStepId: Step4 + Inputs: + Condition: data.Flag1 + Do: + - - Id: Step3.1.1 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter3 + Outputs: + Counter3: step.Value + NextStepId: Step3.1.2 + - Id: Step3.1.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter4 + Outputs: + Counter4: step.Value + - - Id: Step3.2.1 + StepType: WorkflowCore.Primitives.WaitFor, WorkflowCore + NextStepId: Step3.2.2 + CancelCondition: data.Flag2 + Inputs: + EventName: '"Event1"' + EventKey: '"Key1"' + EffectiveDate: DateTime.Now + - Id: Step3.2.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter5 + Outputs: + Counter5: step.Value + - - Id: Step3.3.1 + StepType: WorkflowCore.Primitives.WaitFor, WorkflowCore + NextStepId: Step3.3.2 + CancelCondition: data.Flag2 + ProceedOnCancel: true + Inputs: + EventName: '"Event1"' + EventKey: '"Key1"' + EffectiveDate: DateTime.Now + - Id: Step3.3.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter10 + Outputs: + Counter10: step.Value +- Id: Step4 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter6 + Outputs: + Counter6: step.Value diff --git a/test/WorkflowCore.TestAssets/stored-dynamic-definition.json b/test/WorkflowCore.TestAssets/stored-dynamic-definition.json new file mode 100644 index 000000000..8c6a10dbe --- /dev/null +++ b/test/WorkflowCore.TestAssets/stored-dynamic-definition.json @@ -0,0 +1,90 @@ +{ + "Id": "Test", + "Version": 1, + "Description": "", + "DataType": "WorkflowCore.TestAssets.DataTypes.DynamicData, WorkflowCore.TestAssets", + "Steps": [ + { + "Id": "Step1", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter1\"]" }, + "Outputs": { "Counter1": "step.Value" }, + "NextStepId": "Step2" + }, + { + "Id": "Step2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter2\"]" }, + "Outputs": { "Counter2": "step.Value" }, + "NextStepId": "Step3" + }, + { + "Id": "Step3", + "StepType": "WorkflowCore.Primitives.If, WorkflowCore", + "NextStepId": "Step4", + "Inputs": { "Condition": "object.Equals(data[\"Flag1\"], true)" }, + "Do": [ + [ + { + "Id": "Step3.1.1", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter3\"]" }, + "Outputs": { "Counter3": "step.Value" }, + "NextStepId": "Step3.1.2" + }, + { + "Id": "Step3.1.2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter4\"]" }, + "Outputs": { "Counter4": "step.Value" } + } + ], + [ + { + "Id": "Step3.2.1", + "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", + "NextStepId": "Step3.2.2", + "CancelCondition": "object.Equals(data[\"Flag2\"], true)", + "Inputs": { + "EventName": "\"Event1\"", + "EventKey": "\"Key1\"", + "EffectiveDate": "DateTime.Now" + } + }, + { + "Id": "Step3.2.2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter5\"]" }, + "Outputs": { "Counter5": "step.Value" } + } + ], + [ + { + "Id": "Step3.3.1", + "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", + "NextStepId": "Step3.3.2", + "CancelCondition": "object.Equals(data[\"Flag2\"], true)", + "ProceedOnCancel": true, + "Inputs": { + "EventName": "\"Event1\"", + "EventKey": "\"Key1\"", + "EffectiveDate": "DateTime.Now" + } + }, + { + "Id": "Step3.3.2", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter10\"]" }, + "Outputs": { "Counter10": "step.Value" } + } + ] + ] + }, + { + "Id": "Step4", + "StepType": "WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets", + "Inputs": { "Value": "data[\"Counter6\"]" }, + "Outputs": { "Counter6": "step.Value" } + } + ] +} \ No newline at end of file diff --git a/test/WorkflowCore.TestAssets/stored-dynamic-definition.yaml b/test/WorkflowCore.TestAssets/stored-dynamic-definition.yaml new file mode 100644 index 000000000..9888be21e --- /dev/null +++ b/test/WorkflowCore.TestAssets/stored-dynamic-definition.yaml @@ -0,0 +1,73 @@ +Id: Test +Version: 1 +DataType: WorkflowCore.TestAssets.DataTypes.CounterBoardWithDynamicData, WorkflowCore.TestAssets +Steps: +- Id: Step1 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + ErrorBehavior: Retry + Inputs: + Value: data.DynamicDataInstance["test"] + Outputs: + DynamicDataInstance["test"]: step.Value + NextStepId: Step2 +- Id: Step2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter2 + Outputs: + CounterBoardInstance.Counter1: step.Value + NextStepId: Step3 +- Id: Step3 + StepType: WorkflowCore.Primitives.If, WorkflowCore + NextStepId: Step4 + Inputs: + Condition: object.Equals(data.Flag1, true) + Do: + - - Id: Step3.1.1 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter3 + Outputs: + Counter3: step.Value + NextStepId: Step3.1.2 + - Id: Step3.1.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter4 + Outputs: + Counter4: step.Value + - - Id: Step3.2.1 + StepType: WorkflowCore.Primitives.WaitFor, WorkflowCore + NextStepId: Step3.2.2 + CancelCondition: data.Flag2 + Inputs: + EventName: '"Event1"' + EventKey: '"Key1"' + EffectiveDate: DateTime.Now + - Id: Step3.2.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter5 + Outputs: + Counter5: step.Value + - - Id: Step3.3.1 + StepType: WorkflowCore.Primitives.WaitFor, WorkflowCore + NextStepId: Step3.3.2 + CancelCondition: data.Flag2 + ProceedOnCancel: true + Inputs: + EventName: '"Event1"' + EventKey: '"Key1"' + EffectiveDate: DateTime.Now + - Id: Step3.3.2 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter10 + Outputs: + Counter10: step.Value +- Id: Step4 + StepType: WorkflowCore.TestAssets.Steps.Counter, WorkflowCore.TestAssets + Inputs: + Value: data.Counter6 + Outputs: + Counter6: step.Value diff --git a/test/WorkflowCore.Tests.DynamoDB/DynamoDbDockerSetup.cs b/test/WorkflowCore.Tests.DynamoDB/DynamoDbDockerSetup.cs new file mode 100644 index 000000000..83501a6d5 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/DynamoDbDockerSetup.cs @@ -0,0 +1,51 @@ +using System; +using System.Net; +using Docker.Testify; +using Xunit; +using Amazon.DynamoDBv2; +using Amazon.Runtime; + +namespace WorkflowCore.Tests.DynamoDB +{ + public class DynamoDbDockerSetup : DockerSetup + { + public static string ConnectionString { get; set; } + + public static AWSCredentials Credentials => new BasicAWSCredentials("DUMMYIDEXAMPLE", "DUMMYEXAMPLEKEY"); + + public override string ImageName => @"amazon/dynamodb-local"; + public override int InternalPort => 8000; + public override TimeSpan TimeOut => TimeSpan.FromSeconds(120); + + public override void PublishConnectionInfo() + { + ConnectionString = $"http://localhost:{ExternalPort}"; + } + + public override bool TestReady() + { + try + { + AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig + { + ServiceURL = $"http://localhost:{ExternalPort}" + }; + AmazonDynamoDBClient client = new AmazonDynamoDBClient(Credentials, clientConfig); + var resp = client.ListTablesAsync().Result; + + return resp.HttpStatusCode == HttpStatusCode.OK; + } + catch + { + return false; + } + + } + } + + [CollectionDefinition("DynamoDb collection")] + public class DynamoDbCollection : ICollectionFixture + { + } + +} diff --git a/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs new file mode 100644 index 000000000..7d215f4ee --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs @@ -0,0 +1,39 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Providers.AWS.Services; +using WorkflowCore.UnitTests; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB +{ + [Collection("DynamoDb collection")] + public class DynamoPersistenceProviderFixture : BasePersistenceFixture + { + DynamoDbDockerSetup _dockerSetup; + private IPersistenceProvider _subject; + + public DynamoPersistenceProviderFixture(DynamoDbDockerSetup dockerSetup) + { + _dockerSetup = dockerSetup; + } + + protected override IPersistenceProvider Subject + { + get + { + if (_subject == null) + { + var cfg = new AmazonDynamoDBConfig { ServiceURL = DynamoDbDockerSetup.ConnectionString }; + var dbClient = new AmazonDynamoDBClient(DynamoDbDockerSetup.Credentials, cfg); + var provisioner = new DynamoDbProvisioner(dbClient, "unittests", new LoggerFactory()); + var client = new DynamoPersistenceProvider(dbClient, provisioner, "unittests", new LoggerFactory()); + client.EnsureStoreExists(); + _subject = client; + } + return _subject; + } + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs new file mode 100644 index 000000000..8b6754c02 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoBasicScenario : BasicScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs new file mode 100644 index 000000000..f475c58cb --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoCompensationScenario : CompensationScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs new file mode 100644 index 000000000..648b18868 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoDataScenario : DataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs new file mode 100644 index 000000000..419d7ac37 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoDynamicDataScenario : DynamicDataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs new file mode 100644 index 000000000..e421d31a2 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoEventScenario : EventScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs new file mode 100644 index 000000000..1c9e30679 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoForeachScenario : ForeachScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs new file mode 100644 index 000000000..efc3738d8 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoIfScenario : IfScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs new file mode 100644 index 000000000..5aea6ee1a --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoRetrySagaScenario : RetrySagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs new file mode 100644 index 000000000..8c1f6a317 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoSagaScenario : SagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs new file mode 100644 index 000000000..7912b3f53 --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs @@ -0,0 +1,18 @@ +using System; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.DynamoDB.Scenarios +{ + [Collection("DynamoDb collection")] + public class DynamoWhileScenario : WhileScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + var cfg = new AmazonDynamoDBConfig {ServiceURL = DynamoDbDockerSetup.ConnectionString}; + services.AddWorkflow(x => x.UseAwsDynamoPersistence(DynamoDbDockerSetup.Credentials, cfg, "tests-")); + } + } +} diff --git a/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj b/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj new file mode 100644 index 000000000..65320489d --- /dev/null +++ b/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs new file mode 100644 index 000000000..4ed16a152 --- /dev/null +++ b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Squadron; +using Xunit; + +namespace WorkflowCore.Tests.Elasticsearch +{ + public class ElasticsearchDockerSetup : IAsyncLifetime + { + private readonly ElasticsearchResource _elasticsearchResource; + public static string ConnectionString { get; set; } + + public ElasticsearchDockerSetup() + { + _elasticsearchResource = new ElasticsearchResource(); + } + + public async Task InitializeAsync() + { + await _elasticsearchResource.InitializeAsync(); + ConnectionString = $"http://localhost:{_elasticsearchResource.Instance.HostPort}"; + } + + public Task DisposeAsync() + { + return _elasticsearchResource.DisposeAsync(); + } + } + + [CollectionDefinition("Elasticsearch collection")] + public class ElasticsearchCollection : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchIndexerTests.cs b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchIndexerTests.cs new file mode 100644 index 000000000..322191e0c --- /dev/null +++ b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchIndexerTests.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.Logging; +using Nest; +using WorkflowCore.IntegrationTests; +using WorkflowCore.Interface; +using WorkflowCore.Providers.Elasticsearch.Services; +using Xunit; + +namespace WorkflowCore.Tests.Elasticsearch +{ + [Collection("Elasticsearch collection")] + public class ElasticsearchIndexerTests : SearchIndexTests + { + ElasticsearchDockerSetup _dockerSetup; + + public ElasticsearchIndexerTests(ElasticsearchDockerSetup dockerSetup) + { + _dockerSetup = dockerSetup; + } + + protected override ISearchIndex CreateService() + { + var settings = new ConnectionSettings(new Uri(ElasticsearchDockerSetup.ConnectionString)); + return new ElasticsearchIndexer(settings, "workflowcore.tests", new LoggerFactory()); + } + } +} diff --git a/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj b/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj new file mode 100644 index 000000000..9aa005f40 --- /dev/null +++ b/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs b/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs index 515f3ed8a..df281ce29 100644 --- a/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs +++ b/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs @@ -1,46 +1,35 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using Docker.Testify; -using MongoDB.Driver; +using System; +using System.Threading.Tasks; +using MongoDB.Bson; +using Squadron; using Xunit; namespace WorkflowCore.Tests.MongoDB -{ - public class MongoDockerSetup : DockerSetup +{ + public class MongoDockerSetup : IAsyncLifetime { + private readonly MongoResource _mongoResource; public static string ConnectionString { get; set; } - public override string ImageName => "mongo"; - public override int InternalPort => 27017; - - public override void PublishConnectionInfo() + public MongoDockerSetup() { - ConnectionString = $"mongodb://localhost:{ExternalPort}"; + _mongoResource = new MongoResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var client = new MongoClient($"mongodb://localhost:{ExternalPort}"); - client.ListDatabases(); - return true; - } - catch - { - return false; - } + await _mongoResource.InitializeAsync(); + ConnectionString = _mongoResource.ConnectionString; + } + public Task DisposeAsync() + { + return _mongoResource.DisposeAsync(); } } [CollectionDefinition("Mongo collection")] public class MongoCollection : ICollectionFixture - { + { } - -} +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs index b3146105d..c61b4b5e2 100644 --- a/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs @@ -1,7 +1,5 @@ using MongoDB.Driver; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Persistence.MongoDB.Services; using WorkflowCore.UnitTests; @@ -24,7 +22,7 @@ protected override IPersistenceProvider Subject get { var client = new MongoClient(MongoDockerSetup.ConnectionString); - var db = client.GetDatabase("workflow-tests"); + var db = client.GetDatabase(nameof(MongoPersistenceProviderFixture)); return new MongoPersistenceProvider(db); } } diff --git a/test/WorkflowCore.Tests.MongoDB/Properties/AssemblyInfo.cs b/test/WorkflowCore.Tests.MongoDB/Properties/AssemblyInfo.cs index 608b546e7..d533a178e 100644 --- a/test/WorkflowCore.Tests.MongoDB/Properties/AssemblyInfo.cs +++ b/test/WorkflowCore.Tests.MongoDB/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoActivityScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoActivityScenario.cs new file mode 100644 index 000000000..ca815f41a --- /dev/null +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoActivityScenario.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MongoDB.Scenarios +{ + [Collection("Mongo collection")] + public class MongoActivityScenario : ActivityScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + BsonClassMap.RegisterClassMap(x => x.AutoMap()); + BsonClassMap.RegisterClassMap(x => x.AutoMap()); + + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoActivityScenario))); + } + } +} diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoBasicScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoBasicScenario.cs index 7ffad9531..c24ffe46c 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoBasicScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoBasicScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoBasicScenario : BasicScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoBasicScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoCompensationScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoCompensationScenario.cs index 00fbec0c9..ed3114e55 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoCompensationScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoCompensationScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoCompensationScenario : CompensationScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoCompensationScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDataScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDataScenario.cs index 520c62f5d..6c647bebd 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDataScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDataScenario.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -9,10 +8,15 @@ namespace WorkflowCore.Tests.MongoDB.Scenarios { [Collection("Mongo collection")] public class MongoDataScenario : DataIOScenario - { + { + public MongoDataScenario() : base() + { + BsonClassMap.RegisterClassMap(map => map.AutoMap()); + } + protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoDataScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDecisionScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDecisionScenario.cs new file mode 100644 index 000000000..0db093806 --- /dev/null +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDecisionScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MongoDB.Scenarios +{ + [Collection("Mongo collection")] + public class MongoDecisionScenario : DecisionScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoDecisionScenario))); + } + } +} diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDelayScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDelayScenario.cs new file mode 100644 index 000000000..45aca549f --- /dev/null +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDelayScenario.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MongoDB.Scenarios +{ + [Collection("Mongo collection")] + public class MongoDelayScenario : DelayScenario + { + public MongoDelayScenario() : base() + { + BsonClassMap.RegisterClassMap(map => map.AutoMap()); + } + + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(cfg => + { + cfg.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoDelayScenario)); + cfg.UsePollInterval(TimeSpan.FromSeconds(2)); + }); + } + } +} diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoEventScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoEventScenario.cs index af2e51006..2aa6e8461 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoEventScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoEventScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoEventScenario : EventScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoEventScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForEachScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForEachScenario.cs index 756e76595..5fa8b94b8 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForEachScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForEachScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoForEachScenario : ForeachScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoForEachScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForkScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForkScenario.cs index 40c2ef556..f582f0e7b 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForkScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoForkScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoForkScenario : ForkScenario { protected override void Configure(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoForkScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoIfScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoIfScenario.cs index 51e7beee3..5c78d3ddc 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoIfScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoIfScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoIfScenario : IfScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoIfScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoRetrySagaScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoRetrySagaScenario.cs index 311043260..5e30ed877 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoRetrySagaScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoRetrySagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoRetrySagaScenario : RetrySagaScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoRetrySagaScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoSagaScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoSagaScenario.cs index 1e5e384f9..c9ceb9d83 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoSagaScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoSagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoSagaScenario : SagaScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoSagaScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoUserScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoUserScenario.cs index c082624b8..fbaa0f223 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoUserScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoUserScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoUserScenario : UserScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoUserScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhenScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhenScenario.cs index 8c8925e52..b966f35b8 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhenScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhenScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoWhenScenario : WhenScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoWhenScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhileScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhileScenario.cs index 7f2bdc43b..78e5277f6 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhileScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoWhileScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -12,7 +10,7 @@ public class MongoWhileScenario : WhileScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, nameof(MongoWhileScenario))); } } } diff --git a/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj b/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj index 68d7dd9ee..c708f7bf3 100644 --- a/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj +++ b/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Tests.MongoDB WorkflowCore.Tests.MongoDB true @@ -15,19 +14,14 @@ - + - - - - - - - + + diff --git a/test/WorkflowCore.Tests.MySQL/DockerSetup.cs b/test/WorkflowCore.Tests.MySQL/DockerSetup.cs new file mode 100644 index 000000000..06c041818 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/DockerSetup.cs @@ -0,0 +1,38 @@ +using Xunit; +using System; +using System.Threading.Tasks; +using Squadron; + +namespace WorkflowCore.Tests.MySQL +{ + public class MysqlDockerSetup : IAsyncLifetime + { + private readonly MySqlResource _mySqlResource; + public static string ConnectionString { get; set; } + public static string ScenarioConnectionString { get; set; } + + public MysqlDockerSetup() + { + _mySqlResource = new MySqlResource(); + } + + public async Task InitializeAsync() + { + await _mySqlResource.InitializeAsync(); + var workflowConnection = await _mySqlResource.CreateDatabaseAsync("workflow"); + ConnectionString = workflowConnection.ConnectionString; + var scenariosConnection = await _mySqlResource.CreateDatabaseAsync("scenarios"); + ScenarioConnectionString = scenariosConnection.ConnectionString; + } + + public Task DisposeAsync() + { + return _mySqlResource.DisposeAsync(); + } + } + + [CollectionDefinition("Mysql collection")] + public class MysqlCollection : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.MySQL/MysqlPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.MySQL/MysqlPersistenceProviderFixture.cs new file mode 100644 index 000000000..03f5501c1 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/MysqlPersistenceProviderFixture.cs @@ -0,0 +1,23 @@ +using WorkflowCore.Interface; +using WorkflowCore.Persistence.EntityFramework.Services; +using WorkflowCore.Persistence.MySQL; +using WorkflowCore.UnitTests; +using Xunit; +using Xunit.Abstractions; + +namespace WorkflowCore.Tests.MySQL +{ + [Collection("Mysql collection")] + public class MysqlPersistenceProviderFixture : BasePersistenceFixture + { + private readonly IPersistenceProvider _subject; + protected override IPersistenceProvider Subject => _subject; + + public MysqlPersistenceProviderFixture(MysqlDockerSetup dockerSetup, ITestOutputHelper output) + { + output.WriteLine($"Connecting on {MysqlDockerSetup.ConnectionString}"); + _subject = new EntityFrameworkPersistenceProvider(new MysqlContextFactory(MysqlDockerSetup.ConnectionString), true, true); + _subject.EnsureStoreExists(); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlBasicScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlBasicScenario.cs new file mode 100644 index 000000000..f2de417db --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlBasicScenario.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlBasicScenario : BasicScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDataScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDataScenario.cs new file mode 100644 index 000000000..7dbeb27c8 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDataScenario.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlDataScenario : DataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDelayScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDelayScenario.cs new file mode 100644 index 000000000..deed80f2b --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDelayScenario.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlDelayScenario : DelayScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(cfg => + { + cfg.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true); + cfg.UsePollInterval(TimeSpan.FromSeconds(2)); + }); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDynamicDataScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDynamicDataScenario.cs new file mode 100644 index 000000000..5ec59395d --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDynamicDataScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlDynamicDataScenario : DynamicDataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs new file mode 100644 index 000000000..65e99ad84 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlEventScenario : EventScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs new file mode 100644 index 000000000..01a15de17 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlForeachScenario : ForeachScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs new file mode 100644 index 000000000..8a57e465b --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlForkScenario : ForkScenario + { + protected override void Configure(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs new file mode 100644 index 000000000..7cf6721fc --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlIfScenario : IfScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs new file mode 100644 index 000000000..f16190f73 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlRetrySagaScenario : RetrySagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs new file mode 100644 index 000000000..e23719b22 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlSagaScenario : SagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs new file mode 100644 index 000000000..909b98b4f --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlUserScenario : UserScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs new file mode 100644 index 000000000..e7721e382 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlWhenScenario : WhenScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs new file mode 100644 index 000000000..8e6c5f6ee --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.MySQL.Scenarios +{ + [Collection("Mysql collection")] + public class MysqlWhileScenario : WhileScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseMySQL(MysqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj b/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj new file mode 100644 index 000000000..a56acc9b7 --- /dev/null +++ b/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.PostgreSQL/DockerSetup.cs b/test/WorkflowCore.Tests.PostgreSQL/DockerSetup.cs index 79161fdb5..8548bf85d 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/DockerSetup.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/DockerSetup.cs @@ -1,50 +1,37 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Docker.Testify; -using Npgsql; +using System; +using System.Threading.Tasks; +using Squadron; using Xunit; namespace WorkflowCore.Tests.PostgreSQL { - public class PostgresDockerSetup : DockerSetup + public class PostgresDockerSetup : IAsyncLifetime { + private readonly PostgreSqlResource _postgreSqlResource; public static string ConnectionString { get; set; } public static string ScenarioConnectionString { get; set; } - public override string ImageName => "postgres"; - public override int InternalPort => 5432; - - public override void PublishConnectionInfo() + public PostgresDockerSetup() { - ConnectionString = $"Server=127.0.0.1;Port={ExternalPort};Database=workflow;User Id=postgres;"; - ScenarioConnectionString = $"Server=127.0.0.1;Port={ExternalPort};Database=workflow-scenarios;User Id=postgres;"; + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + _postgreSqlResource = new PostgreSqlResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var connection = new NpgsqlConnection($"Server=127.0.0.1;Port={ExternalPort};Database=postgres;User Id=postgres;"); - connection.Open(); - connection.Close(); - return true; - } - catch - { - return false; - } + await _postgreSqlResource.InitializeAsync(); + ConnectionString = _postgreSqlResource.ConnectionString; + ScenarioConnectionString = _postgreSqlResource.ConnectionString; + } + public Task DisposeAsync() + { + return _postgreSqlResource.DisposeAsync(); } } - + [CollectionDefinition("Postgres collection")] public class PostgresCollection : ICollectionFixture - { + { } - -} +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs index 94f0f30b7..9322b033a 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.PostgreSQL; using WorkflowCore.UnitTests; using Xunit; @@ -18,7 +17,7 @@ public class PostgresPersistenceProviderFixture : BasePersistenceFixture public PostgresPersistenceProviderFixture(PostgresDockerSetup dockerSetup, ITestOutputHelper output) { output.WriteLine($"Connecting on {PostgresDockerSetup.ConnectionString}"); - _subject = new PostgresPersistenceProvider(PostgresDockerSetup.ConnectionString, true, true); + _subject = new EntityFrameworkPersistenceProvider(new PostgresContextFactory(PostgresDockerSetup.ConnectionString,"wfc"), true, true); _subject.EnsureStoreExists(); } } diff --git a/test/WorkflowCore.Tests.PostgreSQL/Properties/AssemblyInfo.cs b/test/WorkflowCore.Tests.PostgreSQL/Properties/AssemblyInfo.cs index 1d2a140ee..9d931e022 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Properties/AssemblyInfo.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresActivityScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresActivityScenario.cs new file mode 100644 index 000000000..2f31eca31 --- /dev/null +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresActivityScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.PostgreSQL.Scenarios +{ + [Collection("Postgres collection")] + public class PostgresActivityScenario : ActivityScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UsePostgreSQL(PostgresDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresBasicScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresBasicScenario.cs index 7b7817e20..2b1a8e62d 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresBasicScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresBasicScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDataScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDataScenario.cs index e72fdcf07..3c0742b0b 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDataScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDataScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDelayScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDelayScenario.cs new file mode 100644 index 000000000..649632ac5 --- /dev/null +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDelayScenario.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.PostgreSQL.Scenarios +{ + [Collection("Postgres collection")] + public class PostgresDelayScenario : DelayScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(cfg => + { + cfg.UsePostgreSQL(PostgresDockerSetup.ScenarioConnectionString, true, true); + cfg.UsePollInterval(TimeSpan.FromSeconds(2)); + }); + } + } +} diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDynamicDataScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDynamicDataScenario.cs new file mode 100644 index 000000000..f654ca16b --- /dev/null +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresDynamicDataScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.PostgreSQL.Scenarios +{ + [Collection("Postgres collection")] + public class PostgresDynamicDataScenario : DynamicDataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UsePostgreSQL(PostgresDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresEventScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresEventScenario.cs index 3af74377b..ec9d520cb 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresEventScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresEventScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForeachScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForeachScenario.cs index 49b2bd92b..a0c5e03f4 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForeachScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForeachScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForkScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForkScenario.cs index 9719ab4b9..4ad8fe86a 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForkScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresForkScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresIfScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresIfScenario.cs index 68be367db..f962dd70d 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresIfScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresIfScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresRetrySagaScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresRetrySagaScenario.cs index f8c792aa6..0c63d54ff 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresRetrySagaScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresRetrySagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresSagaScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresSagaScenario.cs index 38f304029..2d42dcd10 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresSagaScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresSagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresUserScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresUserScenario.cs index 57c8fc5b9..5c371bb93 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresUserScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresUserScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhenScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhenScenario.cs index 9ffc43761..52445096a 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhenScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhenScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhileScenario.cs b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhileScenario.cs index 91532221a..9aaa2583c 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhileScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresWhileScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj b/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj index 197d9374d..66cc649b4 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj +++ b/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Tests.PostgreSQL WorkflowCore.Tests.PostgreSQL true @@ -16,18 +15,12 @@ - + - - - - - - - + diff --git a/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/Tests/DefaultRabbitMqQueueNameProviderTests.cs b/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/Tests/DefaultRabbitMqQueueNameProviderTests.cs new file mode 100644 index 000000000..66bd39233 --- /dev/null +++ b/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/Tests/DefaultRabbitMqQueueNameProviderTests.cs @@ -0,0 +1,28 @@ +using FluentAssertions; +using WorkflowCore.Interface; +using WorkflowCore.QueueProviders.RabbitMQ.Services; +using Xunit; + +namespace WorkflowCore.Tests.QueueProviders.RabbitMQ.Tests +{ + public class DefaultRabbitMqQueueNameProviderTests + { + private readonly DefaultRabbitMqQueueNameProvider _sut; + + public DefaultRabbitMqQueueNameProviderTests() + { + _sut = new DefaultRabbitMqQueueNameProvider(); + } + + [Theory] + [InlineData(QueueType.Event, "wfc.event_queue")] + [InlineData(QueueType.Index, "wfc.index_queue")] + [InlineData(QueueType.Workflow, "wfc.workflow_queue")] + public void GetQueueName_ValidInput_ReturnsValidQueueName(QueueType queueType, string queueName) + { + var result = _sut.GetQueueName(queueType); + + result.Should().Be(queueName); + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj b/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj new file mode 100644 index 000000000..98e658fed --- /dev/null +++ b/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs b/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs new file mode 100644 index 000000000..4faba0af2 --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Squadron; +using Xunit; + +namespace WorkflowCore.Tests.Redis +{ + public class RedisDockerSetup : IAsyncLifetime + { + private readonly RedisResource _redisResource; + public static string ConnectionString { get; set; } + + public RedisDockerSetup() + { + _redisResource = new RedisResource(); + } + + public async Task InitializeAsync() + { + await _redisResource.InitializeAsync(); + ConnectionString = _redisResource.ConnectionString; + } + + public Task DisposeAsync() + { + return _redisResource.DisposeAsync(); + } + } + + [CollectionDefinition("Redis collection")] + public class RedisCollection : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.Redis/RedisPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.Redis/RedisPersistenceProviderFixture.cs new file mode 100644 index 000000000..22a898e2b --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/RedisPersistenceProviderFixture.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; +using WorkflowCore.Providers.Redis.Services; +using WorkflowCore.UnitTests; +using Xunit; + +namespace WorkflowCore.Tests.Redis +{ + [Collection("Redis collection")] + public class RedisPersistenceProviderFixture : BasePersistenceFixture + { + RedisDockerSetup _dockerSetup; + private IPersistenceProvider _subject; + + public RedisPersistenceProviderFixture(RedisDockerSetup dockerSetup) + { + _dockerSetup = dockerSetup; + } + + protected override IPersistenceProvider Subject + { + get + { + if (_subject == null) + { + var client = new RedisPersistenceProvider(RedisDockerSetup.ConnectionString, "test", false, new LoggerFactory()); + client.EnsureStoreExists(); + _subject = client; + } + return _subject; + } + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisBasicScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisBasicScenario.cs new file mode 100644 index 000000000..8a28ff189 --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisBasicScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisBasicScenario : BasicScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisDataScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisDataScenario.cs new file mode 100644 index 000000000..1d79e240a --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisDataScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisDataScenario : DataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisEventScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisEventScenario.cs new file mode 100644 index 000000000..77a06ee3d --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisEventScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisEventScenario : DataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisForeachScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisForeachScenario.cs new file mode 100644 index 000000000..5fb2c3580 --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisForeachScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisForeachScenario : ForeachScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisIfScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisIfScenario.cs new file mode 100644 index 000000000..744e2307f --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisIfScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisIfScenario : IfScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/Scenarios/RedisWhileScenario.cs b/test/WorkflowCore.Tests.Redis/Scenarios/RedisWhileScenario.cs new file mode 100644 index 000000000..06d422ac8 --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/Scenarios/RedisWhileScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.Redis.Scenarios +{ + [Collection("Redis collection")] + public class RedisWhileScenario : WhileScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseRedisPersistence(RedisDockerSetup.ConnectionString, "scenario-")); + } + } +} diff --git a/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj b/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj new file mode 100644 index 000000000..147dad6da --- /dev/null +++ b/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs b/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs index 5ded402f5..cf50f854d 100644 --- a/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs +++ b/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs @@ -1,55 +1,36 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Docker.Testify; +using System; +using System.Threading.Tasks; +using Squadron; using Xunit; namespace WorkflowCore.Tests.SqlServer { - public class SqlDockerSetup : DockerSetup + public class SqlDockerSetup : IAsyncLifetime { + private readonly SqlServerResource _sqlServerResource; public static string ConnectionString { get; set; } public static string ScenarioConnectionString { get; set; } - public override string ImageName => "microsoft/mssql-server-linux"; - public override int InternalPort => 1433; - public override TimeSpan TimeOut => TimeSpan.FromSeconds(60); - - public const string SqlPassword = "I@mJustT3st1ing"; - - public override IList EnvironmentVariables => new List {"ACCEPT_EULA=Y", $"SA_PASSWORD={SqlPassword}"}; - - public override void PublishConnectionInfo() + public SqlDockerSetup() { - ConnectionString = $"Server=127.0.0.1,{ExternalPort};Database=workflowcore-tests;User Id=sa;Password={SqlPassword};"; - ScenarioConnectionString = $"Server=127.0.0.1,{ExternalPort};Database=workflowcore-scenario-tests;User Id=sa;Password={SqlPassword};"; + _sqlServerResource = new SqlServerResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var client = new SqlConnection($"Server=127.0.0.1,{ExternalPort};Database=master;User Id=sa;Password={SqlPassword};"); - client.Open(); - client.Close(); - return true; - } - catch - { - return false; - } + await _sqlServerResource.InitializeAsync(); + ConnectionString = _sqlServerResource.CreateConnectionString("workflowcore-tests"); + ScenarioConnectionString = _sqlServerResource.CreateConnectionString("workflowcore-scenario-tests"); + } + public Task DisposeAsync() + { + return _sqlServerResource.DisposeAsync(); } } [CollectionDefinition("SqlServer collection")] public class SqlServerCollection : ICollectionFixture - { + { } - } \ No newline at end of file diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerActivityScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerActivityScenario.cs new file mode 100644 index 000000000..2094cdf66 --- /dev/null +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerActivityScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.SqlServer.Scenarios +{ + [Collection("SqlServer collection")] + public class SqlServerActivityScenario : ActivityScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseSqlServer(SqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerBasicScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerBasicScenario.cs index 1333131eb..56c951884 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerBasicScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerBasicScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerCompenstationScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerCompenstationScenario.cs index 2114b5d19..63b8ff470 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerCompenstationScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerCompenstationScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDataScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDataScenario.cs index e892534c4..0ecabc61f 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDataScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDataScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDelayScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDelayScenario.cs new file mode 100644 index 000000000..5e623c1db --- /dev/null +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDelayScenario.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.SqlServer.Scenarios +{ + [Collection("SqlServer collection")] + public class SqlServerDelayScenario : DelayScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(cfg => + { + cfg.UseSqlServer(SqlDockerSetup.ScenarioConnectionString, true, true); + cfg.UsePollInterval(TimeSpan.FromSeconds(2)); + }); + } + } +} diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDynamicDataScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDynamicDataScenario.cs new file mode 100644 index 000000000..34cb86215 --- /dev/null +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerDynamicDataScenario.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using Xunit; + +namespace WorkflowCore.Tests.SqlServer.Scenarios +{ + [Collection("SqlServer collection")] + public class SqlServerDynamicDataScenario : DynamicDataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseSqlServer(SqlDockerSetup.ScenarioConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerEventScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerEventScenario.cs index 32cb07dc2..fce8a9bae 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerEventScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerEventScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerForEachScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerForEachScenario.cs index 2163fa998..b4df418d2 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerForEachScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerForEachScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerIfScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerIfScenario.cs index e51c4a514..798863285 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerIfScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerIfScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerRetrySagaScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerRetrySagaScenario.cs index 258ca298f..fd00b2d20 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerRetrySagaScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerRetrySagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerSagaScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerSagaScenario.cs index 2f1bf58ae..b8e13f0c4 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerSagaScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerSagaScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhenScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhenScenario.cs index 5180db8e9..77da4ee10 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhenScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhenScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhileScenario.cs b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhileScenario.cs index cd870f805..832579484 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhileScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerWhileScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; diff --git a/test/WorkflowCore.Tests.SqlServer/SqlServerPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.SqlServer/SqlServerPersistenceProviderFixture.cs index ca82db4ab..cbd019bb9 100644 --- a/test/WorkflowCore.Tests.SqlServer/SqlServerPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.SqlServer/SqlServerPersistenceProviderFixture.cs @@ -1,4 +1,5 @@ using WorkflowCore.Interface; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.SqlServer; using WorkflowCore.UnitTests; using Xunit; @@ -19,7 +20,7 @@ protected override IPersistenceProvider Subject { get { - var db = new SqlServerPersistenceProvider(_connectionString, true, true); + var db = new EntityFrameworkPersistenceProvider(new SqlContextFactory(_connectionString), true, true); db.EnsureStoreExists(); return db; } diff --git a/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj b/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj index d81b3ffa0..24e83536e 100644 --- a/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj +++ b/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj @@ -1,17 +1,12 @@  - - netcoreapp2.0 - - - - - + + diff --git a/test/WorkflowCore.Tests.Sqlite/SqliteCollection.cs b/test/WorkflowCore.Tests.Sqlite/SqliteCollection.cs index a57bc2c1b..69ed8c41a 100644 --- a/test/WorkflowCore.Tests.Sqlite/SqliteCollection.cs +++ b/test/WorkflowCore.Tests.Sqlite/SqliteCollection.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Xunit; namespace WorkflowCore.Tests.Sqlite diff --git a/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs b/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs index dd4c6ca9e..f8318ad9b 100644 --- a/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; +using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.Sqlite; using WorkflowCore.UnitTests; using Xunit; @@ -22,7 +21,7 @@ protected override IPersistenceProvider Subject { get { - var db = new SqlitePersistenceProvider(_connectionString, true); + var db = new EntityFrameworkPersistenceProvider(new SqliteContextFactory(_connectionString), true, false); db.EnsureStoreExists(); return db; } diff --git a/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj b/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj index a8cfbe250..63acee283 100644 --- a/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj +++ b/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj @@ -1,15 +1,5 @@  - - netcoreapp2.0 - - - - - - - - diff --git a/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs b/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs index a315eba59..9114da7b9 100644 --- a/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs +++ b/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; using WorkflowCore.Interface; using WorkflowCore.Models; -using Xunit; -using FluentAssertions; using WorkflowCore.TestAssets; -using System.Threading.Tasks; +using Xunit; namespace WorkflowCore.UnitTests { @@ -15,9 +15,9 @@ public abstract class BasePersistenceFixture protected abstract IPersistenceProvider Subject { get; } [Fact] - public void CreateNewWorkflow() + public void CreateNewWorkflow_should_generate_id() { - var workflow = new WorkflowInstance() + var workflow = new WorkflowInstance { Data = new { Value1 = 7 }, Description = "My Description", @@ -26,7 +26,7 @@ public void CreateNewWorkflow() Version = 1, WorkflowDefinitionId = "My Workflow" }; - workflow.ExecutionPointers.Add(new ExecutionPointer() + workflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, @@ -40,61 +40,217 @@ public void CreateNewWorkflow() } [Fact] - public void GetWorkflowInstance() + public void GetWorkflowInstance_should_retrieve_workflow() { - var workflow = new WorkflowInstance() + var workflow = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, Version = 1, - WorkflowDefinitionId = "My Workflow" + WorkflowDefinitionId = "My Workflow", + Reference = "My Reference" }; - workflow.ExecutionPointers.Add(new ExecutionPointer() + workflow.ExecutionPointers.Add(new ExecutionPointer { - Id = Guid.NewGuid().ToString(), + Id = "1", Active = true, StepId = 0, - SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime() + SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime(), + Scope = new List { "4", "3", "2", "1" } }); var workflowId = Subject.CreateNewWorkflow(workflow).Result; var retrievedWorkflow = Subject.GetWorkflowInstance(workflowId).Result; retrievedWorkflow.ShouldBeEquivalentTo(workflow); + retrievedWorkflow.ExecutionPointers.FindById("1") + .Scope.Should().ContainInOrder(workflow.ExecutionPointers.FindById("1").Scope); + } + + [Fact] + public void GetWorkflowInstances_should_retrieve_workflows() + { + var workflow01 = new WorkflowInstance + { + Data = new TestData { Value1 = 7 }, + Description = "My Description", + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Version = 1, + WorkflowDefinitionId = "My Workflow", + Reference = "My Reference" + }; + workflow01.ExecutionPointers.Add(new ExecutionPointer + { + Id = "1", + Active = true, + StepId = 0, + SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime(), + Scope = new List { "4", "3", "2", "1" } + }); + var workflowId01 = Subject.CreateNewWorkflow(workflow01).Result; + + var workflow02 = new WorkflowInstance + { + Data = new TestData { Value1 = 7 }, + Description = "My Description", + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Version = 1, + WorkflowDefinitionId = "My Workflow", + Reference = "My Reference" + }; + workflow02.ExecutionPointers.Add(new ExecutionPointer + { + Id = "1", + Active = true, + StepId = 0, + SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime(), + Scope = new List { "4", "3", "2", "1" } + }); + var workflowId02 = Subject.CreateNewWorkflow(workflow02).Result; + + var workflow03 = new WorkflowInstance + { + Data = new TestData { Value1 = 7 }, + Description = "My Description", + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Version = 1, + WorkflowDefinitionId = "My Workflow", + Reference = "My Reference" + }; + workflow03.ExecutionPointers.Add(new ExecutionPointer + { + Id = "1", + Active = true, + StepId = 0, + SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime(), + Scope = new List { "4", "3", "2", "1" } + }); + var workflowId03 = Subject.CreateNewWorkflow(workflow03).Result; + + var retrievedWorkflows = Subject.GetWorkflowInstances(new[] { workflowId01, workflowId02, workflowId03 }).Result; + + retrievedWorkflows.Count().ShouldBeEquivalentTo(3); + + var retrievedWorkflow01 = retrievedWorkflows.Single(o => o.Id == workflowId01); + retrievedWorkflow01.ShouldBeEquivalentTo(workflow01); + retrievedWorkflow01.ExecutionPointers.FindById("1") + .Scope.Should().ContainInOrder(workflow01.ExecutionPointers.FindById("1").Scope); + + var retrievedWorkflow02 = retrievedWorkflows.Single(o => o.Id == workflowId02); + retrievedWorkflow02.ShouldBeEquivalentTo(workflow02); + retrievedWorkflow02.ExecutionPointers.FindById("1") + .Scope.Should().ContainInOrder(workflow02.ExecutionPointers.FindById("1").Scope); + + var retrievedWorkflow03 = retrievedWorkflows.Single(o => o.Id == workflowId03); + retrievedWorkflow03.ShouldBeEquivalentTo(workflow03); + retrievedWorkflow03.ExecutionPointers.FindById("1") + .Scope.Should().ContainInOrder(workflow03.ExecutionPointers.FindById("1").Scope); } [Fact] public void PersistWorkflow() { - var oldWorkflow = new WorkflowInstance() + var oldWorkflow = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, Version = 1, WorkflowDefinitionId = "My Workflow", - CreateTime = new DateTime(2000, 1, 1).ToUniversalTime() + CreateTime = new DateTime(2000, 1, 1).ToUniversalTime(), + Reference = "My Reference" }; - oldWorkflow.ExecutionPointers.Add(new ExecutionPointer() + oldWorkflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, - StepId = 0 + StepId = 0, + Scope = new List { "1", "2", "3", "4" } }); var workflowId = Subject.CreateNewWorkflow(oldWorkflow).Result; var newWorkflow = Utils.DeepCopy(oldWorkflow); newWorkflow.Data = oldWorkflow.Data; + newWorkflow.Reference = oldWorkflow.Reference; newWorkflow.NextExecution = 7; - newWorkflow.ExecutionPointers.Add(new ExecutionPointer() { Id = Guid.NewGuid().ToString(), Active = true, StepId = 1 }); + newWorkflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, StepId = 1 }); Subject.PersistWorkflow(newWorkflow).Wait(); var current = Subject.GetWorkflowInstance(workflowId).Result; current.ShouldBeEquivalentTo(newWorkflow); } + + [Fact] + public void PersistWorkflow_with_subscriptions() + { + var workflow = new WorkflowInstance + { + Data = new TestData { Value1 = 7 }, + Description = "My Description", + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Version = 1, + WorkflowDefinitionId = "My Workflow", + CreateTime = new DateTime(2000, 1, 1).ToUniversalTime(), + ExecutionPointers = new ExecutionPointerCollection(), + Reference = Guid.NewGuid().ToString() + }; + + workflow.ExecutionPointers.Add(new ExecutionPointer + { + Id = Guid.NewGuid().ToString(), + Active = true, + StepId = 0, + Scope = new List { "1", "2", "3", "4" }, + EventName = "Event1" + }); + + workflow.ExecutionPointers.Add(new ExecutionPointer + { + Id = Guid.NewGuid().ToString(), + Active = true, + StepId = 1, + Scope = new List { "1", "2", "3", "4" }, + EventName = "Event2", + }); + + var workflowId = Subject.CreateNewWorkflow(workflow).Result; + workflow.NextExecution = 0; + + List subscriptions = new List(); + foreach (var pointer in workflow.ExecutionPointers) + { + var subscription = new EventSubscription() + { + WorkflowId = workflowId, + StepId = pointer.StepId, + ExecutionPointerId = pointer.Id, + EventName = pointer.EventName, + EventKey = workflowId, + SubscribeAsOf = DateTime.UtcNow, + SubscriptionData = "data" + }; + + subscriptions.Add(subscription); + } + + Subject.PersistWorkflow(workflow, subscriptions).Wait(); + + var current = Subject.GetWorkflowInstance(workflowId).Result; + current.ShouldBeEquivalentTo(workflow); + + foreach (var pointer in workflow.ExecutionPointers) + { + subscriptions = Subject.GetSubscriptions(pointer.EventName, workflowId, DateTime.UtcNow).Result.ToList(); + subscriptions.Should().HaveCount(1); + } + } [Fact] public void ConcurrentPersistWorkflow() @@ -107,9 +263,9 @@ public void ConcurrentPersistWorkflow() { actions.Add(() => { - var oldWorkflow = new WorkflowInstance() + var oldWorkflow = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, @@ -117,7 +273,7 @@ public void ConcurrentPersistWorkflow() WorkflowDefinitionId = "My Workflow", CreateTime = new DateTime(2000, 1, 1).ToUniversalTime() }; - oldWorkflow.ExecutionPointers.Add(new ExecutionPointer() + oldWorkflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, @@ -126,7 +282,7 @@ public void ConcurrentPersistWorkflow() var workflowId = subject.CreateNewWorkflow(oldWorkflow).Result; var newWorkflow = Utils.DeepCopy(oldWorkflow); newWorkflow.NextExecution = 7; - newWorkflow.ExecutionPointers.Add(new ExecutionPointer() { Id = Guid.NewGuid().ToString(), Active = true, StepId = 1 }); + newWorkflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, StepId = 1 }); subject.PersistWorkflow(newWorkflow).Wait(); // It will throw an exception if the persistence provider occurred resource competition. }); @@ -143,4 +299,4 @@ public class TestData { public int Value1 { get; set; } } -} +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs b/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs new file mode 100644 index 000000000..81e667139 --- /dev/null +++ b/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; + +namespace WorkflowCore.UnitTests +{ + public class MemberMapParameterTests + { + + [Fact] + public void should_assign_input() + { + Expression> memberExpr = (x => x.Value1); + Expression> valueExpr = (x => x.Value1); + var subject = new MemberMapParameter(valueExpr, memberExpr); + var data = new MyData + { + Value1 = 5 + }; + var step = new MyStep(); + + subject.AssignInput(data, step, new StepExecutionContext()); + + step.Value1.Should().Be(data.Value1); + } + + [Fact] + public void should_assign_output() + { + Expression> memberExpr = (x => x.Value1); + Expression> valueExpr = (x => x.Value1); + var subject = new MemberMapParameter(valueExpr, memberExpr); + var data = new MyData(); + var step = new MyStep + { + Value1 = 5 + }; + + subject.AssignOutput(data, step, new StepExecutionContext()); + + data.Value1.Should().Be(step.Value1); + } + + [Fact] + public void should_convert_input() + { + Expression> memberExpr = (x => x.Value2); + Expression> valueExpr = (x => x.Value1); + var subject = new MemberMapParameter(valueExpr, memberExpr); + + var data = new MyData + { + Value1 = 5 + }; + + var step = new MyStep(); + + subject.AssignInput(data, step, new StepExecutionContext()); + + step.Value2.Should().Be(data.Value1); + } + + [Fact] + public void should_convert_output() + { + Expression> memberExpr = (x => x.Value2); + Expression> valueExpr = (x => x.Value1); + var subject = new MemberMapParameter(valueExpr, memberExpr); + + var data = new MyData + { + Value1 = 5 + }; + + var step = new MyStep(); + + subject.AssignOutput(data, step, new StepExecutionContext()); + + data.Value2.Should().Be(step.Value1); + } + + + class MyData + { + public int Value1 { get; set; } + public object Value2 { get; set; } + } + + class MyStep : IStepBody + { + public int Value1 { get; set; } + public object Value2 { get; set; } + + public Task RunAsync(IStepExecutionContext context) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/WorkflowCore.UnitTests/Properties/AssemblyInfo.cs b/test/WorkflowCore.UnitTests/Properties/AssemblyInfo.cs index a44538b0e..4e6557e23 100644 --- a/test/WorkflowCore.UnitTests/Properties/AssemblyInfo.cs +++ b/test/WorkflowCore.UnitTests/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/test/WorkflowCore.UnitTests/Services/DefinitionStorage/DefinitionLoaderTests.cs b/test/WorkflowCore.UnitTests/Services/DefinitionStorage/DefinitionLoaderTests.cs index 5aacbf88a..01a743e8a 100644 --- a/test/WorkflowCore.UnitTests/Services/DefinitionStorage/DefinitionLoaderTests.cs +++ b/test/WorkflowCore.UnitTests/Services/DefinitionStorage/DefinitionLoaderTests.cs @@ -1,15 +1,11 @@ using FakeItEasy; using FluentAssertions; using System; -using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Services.DefinitionStorage; using WorkflowCore.TestAssets.DataTypes; -using WorkflowCore.TestAssets.Steps; using Xunit; namespace WorkflowCore.UnitTests.Services.DefinitionStorage @@ -27,9 +23,9 @@ public DefinitionLoaderTests() } [Fact(DisplayName = "Should register workflow")] - public void RegisterDefintion() + public void RegisterDefinition() { - _subject.LoadDefinition("{\"Id\": \"HelloWorld\", \"Version\": 1, \"Steps\": []}"); + _subject.LoadDefinition("{\"Id\": \"HelloWorld\", \"Version\": 1, \"Steps\": []}", Deserializers.Json); A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Id == "HelloWorld"))).MustHaveHappened(); A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Version == 1))).MustHaveHappened(); @@ -37,23 +33,50 @@ public void RegisterDefintion() } [Fact(DisplayName = "Should parse definition")] - public void ParseDefintion() + public void ParseDefinition() { - _subject.LoadDefinition(TestAssets.Utils.GetTestDefinitionJson()); + _subject.LoadDefinition(TestAssets.Utils.GetTestDefinitionJson(), Deserializers.Json); A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Id == "Test"))).MustHaveHappened(); A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Version == 1))).MustHaveHappened(); - A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.DataType == typeof(TestAssets.DataTypes.CounterBoard)))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.DataType == typeof(CounterBoard)))).MustHaveHappened(); A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(MatchTestDefinition, ""))).MustHaveHappened(); } + [Fact(DisplayName = "Should parse definition")] + public void ParseDefinitionPropertyDynamic() + { + _subject.LoadDefinition(TestAssets.Utils.GetTestDefinitionDynamicYaml(), Deserializers.Yaml); + + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Id == "Test"))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Version == 1))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.DataType == typeof(CounterBoardWithDynamicData)))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(MatchTestDefinition, ""))).MustHaveHappened(); + } + + [Fact(DisplayName = "Should parse definition")] + public void ParseDefinitionDynamic() + { + _subject.LoadDefinition(TestAssets.Utils.GetTestDefinitionDynamicJson(), Deserializers.Json); + + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Id == "Test"))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.Version == 1))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(x => x.DataType == typeof(DynamicData)))).MustHaveHappened(); + A.CallTo(() => _registry.RegisterWorkflow(A.That.Matches(MatchTestDefinition, ""))).MustHaveHappened(); + } + + [Fact(DisplayName = "Should throw error for bad input property name on step")] + public void ParseDefinitionInputException() + { + Assert.Throws(() => _subject.LoadDefinition(TestAssets.Utils.GetTestDefinitionJsonMissingInputProperty(), Deserializers.Json)); + } private bool MatchTestDefinition(WorkflowDefinition def) { //TODO: make this better - var step1 = def.Steps.Single(s => s.Tag == "Step1"); - var step2 = def.Steps.Single(s => s.Tag == "Step2"); - + var step1 = def.Steps.Single(s => s.ExternalId == "Step1"); + var step2 = def.Steps.Single(s => s.ExternalId == "Step2"); + step1.Outcomes.Count.Should().Be(1); step1.Inputs.Count.Should().Be(1); step1.Outputs.Count.Should().Be(1); diff --git a/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs b/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs new file mode 100644 index 000000000..a5697882b --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs @@ -0,0 +1,204 @@ +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; +using FluentAssertions; +using Xunit; +using System.Linq.Expressions; + +namespace WorkflowCore.UnitTests.Services +{ + public class ExecutionResultProcessorFixture + { + protected IExecutionResultProcessor Subject; + protected IExecutionPointerFactory PointerFactory; + protected IDateTimeProvider DateTimeProvider; + protected ILifeCycleEventPublisher EventHub; + protected ICollection ErrorHandlers; + protected WorkflowOptions Options; + + public ExecutionResultProcessorFixture() + { + PointerFactory = A.Fake(); + DateTimeProvider = A.Fake(); + EventHub = A.Fake(); + ErrorHandlers = new HashSet(); + + Options = new WorkflowOptions(A.Fake()); + + A.CallTo(() => DateTimeProvider.Now).Returns(DateTime.Now); + A.CallTo(() => DateTimeProvider.UtcNow).Returns(DateTime.UtcNow); + + //config logging + var loggerFactory = new LoggerFactory(); + //loggerFactory.AddConsole(LogLevel.Debug); + + Subject = new ExecutionResultProcessor(PointerFactory, DateTimeProvider, EventHub, ErrorHandlers, Options, loggerFactory); + } + + [Fact(DisplayName = "Should advance workflow")] + public void should_advance_workflow() + { + //arrange + var definition = new WorkflowDefinition(); + var pointer1 = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var pointer2 = new ExecutionPointer { Id = "2" }; + var outcome = new ValueOutcome { NextStep = 1 }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer1); + var result = ExecutionResult.Next(); + + A.CallTo(() => step.Outcomes).Returns(new List { outcome }); + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome)).Returns(pointer2); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer1, step, result, workflowResult); + + //assert + pointer1.Active.Should().BeFalse(); + pointer1.Status.Should().Be(PointerStatus.Complete); + pointer1.EndTime.Should().NotBeNull(); + + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome)).MustHaveHappened(); + instance.ExecutionPointers.Should().Contain(pointer2); + } + + [Fact(DisplayName = "Should set persistence data")] + public void should_set_persistence_data() + { + //arrange + var persistenceData = new object(); + var definition = new WorkflowDefinition(); + var pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer); + var result = ExecutionResult.Persist(persistenceData); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer, step, result, workflowResult); + + //assert + pointer.PersistenceData.Should().Be(persistenceData); + } + + [Fact(DisplayName = "Should subscribe to event")] + public void should_subscribe_to_event() + { + //arrange + var definition = new WorkflowDefinition(); + var pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer); + var result = ExecutionResult.WaitForEvent("Event", "Key", DateTime.Now); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer, step, result, workflowResult); + + //assert + pointer.Status.Should().Be(PointerStatus.WaitingForEvent); + pointer.Active.Should().BeFalse(); + pointer.EventName.Should().Be("Event"); + pointer.EventKey.Should().Be("Key"); + workflowResult.Subscriptions.Should().Contain(x => x.StepId == pointer.StepId && x.EventName == "Event" && x.EventKey == "Key"); + } + + [Fact(DisplayName = "Should select correct outcomes")] + public void should_select_correct_outcomes() + { + //arrange + var definition = new WorkflowDefinition(); + var pointer1 = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var pointer2 = new ExecutionPointer { Id = "2" }; + var pointer3 = new ExecutionPointer { Id = "3" }; + Expression> expr1 = data => 10; + Expression> expr2 = data => 20; + var outcome1 = new ValueOutcome { NextStep = 1, Value = expr1 }; + var outcome2 = new ValueOutcome { NextStep = 2, Value = expr2 }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer1); + var result = ExecutionResult.Outcome(20); + + A.CallTo(() => step.Outcomes).Returns(new List { outcome1, outcome2 }); + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome1)).Returns(pointer2); + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome2)).Returns(pointer3); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer1, step, result, workflowResult); + + //assert + pointer1.Active.Should().BeFalse(); + pointer1.Status.Should().Be(PointerStatus.Complete); + pointer1.EndTime.Should().NotBeNull(); + + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome1)).MustNotHaveHappened(); + A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome2)).MustHaveHappened(); + instance.ExecutionPointers.Should().NotContain(pointer2); + instance.ExecutionPointers.Should().Contain(pointer3); + } + + [Fact(DisplayName = "Should sleep pointer")] + public void should_sleep_pointer() + { + //arrange + var persistenceData = new object(); + var definition = new WorkflowDefinition(); + var pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer); + var result = ExecutionResult.Sleep(TimeSpan.FromMinutes(5), persistenceData); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer, step, result, workflowResult); + + //assert + pointer.Status.Should().Be(PointerStatus.Sleeping); + pointer.SleepUntil.Should().NotBeNull(); + } + + [Fact(DisplayName = "Should branch children")] + public void should_branch_children() + { + //arrange + var branch = 10; + var child = 2; + var definition = new WorkflowDefinition(); + var pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; + var childPointer = new ExecutionPointer { Id = "2" }; + var step = A.Fake(); + var workflowResult = new WorkflowExecutorResult(); + var instance = GivenWorkflow(pointer); + var result = ExecutionResult.Branch(new List { branch }, null); + + A.CallTo(() => step.Children).Returns(new List { child }); + A.CallTo(() => PointerFactory.BuildChildPointer(definition, pointer, child, branch)).Returns(childPointer); + + //act + Subject.ProcessExecutionResult(instance, definition, pointer, step, result, workflowResult); + + //assert + A.CallTo(() => PointerFactory.BuildChildPointer(definition, pointer, child, branch)).MustHaveHappened(); + instance.ExecutionPointers.Should().Contain(childPointer); + } + + private static WorkflowInstance GivenWorkflow(ExecutionPointer pointer) + { + return new WorkflowInstance + { + Status = WorkflowStatus.Runnable, + ExecutionPointers = new ExecutionPointerCollection(new List + { + pointer + }) + }; + } + } +} diff --git a/test/WorkflowCore.UnitTests/Services/LifeCycleEventPublisherTests.cs b/test/WorkflowCore.UnitTests/Services/LifeCycleEventPublisherTests.cs new file mode 100644 index 000000000..d8e38437e --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/LifeCycleEventPublisherTests.cs @@ -0,0 +1,97 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Models.LifeCycleEvents; +using WorkflowCore.Services; +using Xunit; + +namespace WorkflowCore.UnitTests.Services +{ + public class LifeCycleEventPublisherTests + { + [Fact(DisplayName = "Notifications should be published when the publisher is running")] + public async Task PublishNotification_WhenStarted_PublishesNotification() + { + // Arrange + var wasCalled = new TaskCompletionSource(); + var eventHubMock = new Mock(); + var serviceCollectionMock = new Mock(); + + var workflowOptions = new WorkflowOptions(serviceCollectionMock.Object) + { + EnableLifeCycleEventsPublisher = true + }; + + eventHubMock + .Setup(hub => hub.PublishNotification(It.IsAny())) + .Callback(() => wasCalled.SetResult(true)); + LifeCycleEventPublisher publisher = new LifeCycleEventPublisher(eventHubMock.Object, workflowOptions, new LoggerFactory()); + + // Act + publisher.Start(); + publisher.PublishNotification(new StepCompleted()); + + // Assert + await wasCalled.Task; + eventHubMock.Verify(hub => hub.PublishNotification(It.IsAny()), Times.Once()); + } + + [Fact(DisplayName = "Notifications should be published when the publisher is running")] + public async Task PublishNotification_WhenRestarted_PublishesNotification() + { + // Arrange + var wasCalled = new TaskCompletionSource(); + var eventHubMock = new Mock(); + var serviceCollectionMock = new Mock(); + + var workflowOptions = new WorkflowOptions(serviceCollectionMock.Object) + { + EnableLifeCycleEventsPublisher = true + }; + + eventHubMock + .Setup(hub => hub.PublishNotification(It.IsAny())) + .Callback(() => wasCalled.SetResult(true)); + LifeCycleEventPublisher publisher = new LifeCycleEventPublisher(eventHubMock.Object, workflowOptions, new LoggerFactory()); + + // Act + publisher.Start(); + publisher.Stop(); + publisher.Start(); + publisher.PublishNotification(new StepCompleted()); + + // Assert + await wasCalled.Task; + eventHubMock.Verify(hub => hub.PublishNotification(It.IsAny()), Times.Once()); + } + + [Fact(DisplayName = "Notifications should be disabled if option EnableLifeCycleEventsPublisher is disabled")] + public void PublishNotification_Disabled() + { + // Arrange + var eventHubMock = new Mock(); + var serviceCollectionMock = new Mock(); + + var workflowOptions = new WorkflowOptions(serviceCollectionMock.Object) + { + EnableLifeCycleEventsPublisher = false + }; + + eventHubMock + .Setup(hub => hub.PublishNotification(It.IsAny())) + .Returns(Task.CompletedTask); + LifeCycleEventPublisher publisher = new LifeCycleEventPublisher(eventHubMock.Object, workflowOptions, new LoggerFactory()); + + // Act + publisher.Start(); + publisher.PublishNotification(new StepCompleted()); + publisher.Stop(); + + // Assert + eventHubMock.Verify(hub => hub.PublishNotification(It.IsAny()), Times.Never()); + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs index 01e7b7461..1bfa2b3f7 100644 --- a/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/MemoryPersistenceProviderFixture.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Services; @@ -8,6 +6,8 @@ namespace WorkflowCore.UnitTests.Services { public class MemoryPersistenceProviderFixture : BasePersistenceFixture { - protected override IPersistenceProvider Subject => new MemoryPersistenceProvider(); + private readonly IPersistenceProvider _subject = new MemoryPersistenceProvider(); + + protected override IPersistenceProvider Subject => _subject; } } diff --git a/test/WorkflowCore.UnitTests/Services/ScopeProviderTests.cs b/test/WorkflowCore.UnitTests/Services/ScopeProviderTests.cs new file mode 100644 index 000000000..ab7869d1d --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/ScopeProviderTests.cs @@ -0,0 +1,36 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using WorkflowCore.Interface; +using WorkflowCore.Services; +using Xunit; + +namespace WorkflowCore.UnitTests.Services +{ + public class ScopeProviderTests + { + private readonly ScopeProvider _sut; + private readonly Mock _scopeFactoryMock; + + public ScopeProviderTests() + { + _scopeFactoryMock = new Mock(); + + _sut = new ScopeProvider(_scopeFactoryMock.Object); + } + + [Fact(DisplayName = "Should return IServiceScope")] + public void ReturnsServiceScope_CreateScopeCalled() + { + var scope = new Mock().Object; + _scopeFactoryMock.Setup(x => x.CreateScope()) + .Returns(scope); + + var result = _sut.CreateScope(new Mock().Object); + + result.Should().NotBeNull().And.BeSameAs(scope); + _scopeFactoryMock.Verify(x => x.CreateScope(), Times.Once); + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/Services/StepExecutorTests.cs b/test/WorkflowCore.UnitTests/Services/StepExecutorTests.cs new file mode 100644 index 000000000..86d2bfcee --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/StepExecutorTests.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; +using Xunit; +using Xunit.Abstractions; + +namespace WorkflowCore.UnitTests.Services +{ + public class StepExecutorTests + { + protected List Middleware { get; } + protected IStepBody Body { get; } + protected IStepExecutionContext Context { get; } + protected IStepExecutor Runner { get; } + protected ExecutionResult DummyResult { get; } = ExecutionResult.Persist(null); + protected ITestOutputHelper Out { get; } + + public StepExecutorTests(ITestOutputHelper output) + { + Middleware = new List(); + Body = A.Fake(); + Context = A.Fake(); + Out = output; + Runner = new StepExecutor(Middleware); + + A + .CallTo(() => Body.RunAsync(A._)) + .Invokes(() => Out.WriteLine("Called step body")) + .Returns(DummyResult); + } + + [Fact(DisplayName = "ExecuteStep should run step when no middleware")] + public async Task ExecuteStep_should_run_step_when_no_middleware() + { + // Act + var result = await Runner.ExecuteStep(Context, Body); + + // Assert + result.Should().Be(DummyResult); + } + + [Fact(DisplayName = "ExecuteStep should run middleware and step when one middleware")] + public async Task ExecuteStep_should_run_middleware_and_step_when_one_middleware() + { + // Arrange + var middleware = BuildStepMiddleware(); + Middleware.Add(middleware); + + // Act + var result = await Runner.ExecuteStep(Context, Body); + + // Assert + result.Should().Be(DummyResult); + A + .CallTo(RunMethodFor(Body)) + .MustHaveHappenedOnceExactly() + .Then( + A.CallTo(HandleMethodFor(middleware)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = + "ExecuteStep should run middleware chain completing in reverse order and step when multiple middleware")] + public async Task + ExecuteStep_should_run_middleware_chain_completing_in_reverse_order_and_step_when_multiple_middleware() + { + // Arrange + var middleware1 = BuildStepMiddleware(1); + var middleware2 = BuildStepMiddleware(2); + var middleware3 = BuildStepMiddleware(3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + + // Act + var result = await Runner.ExecuteStep(Context, Body); + + // Assert + result.Should().Be(DummyResult); + A + .CallTo(RunMethodFor(Body)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(middleware3)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware2)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware1)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = "ExecuteStep should bubble up exceptions in middleware")] + public void ExecuteStep_should_bubble_up_exceptions_in_middleware() + { + // Arrange + var middleware1 = BuildStepMiddleware(1); + var middleware2 = BuildStepMiddleware(2); + var middleware3 = BuildStepMiddleware(3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + A + .CallTo(HandleMethodFor(middleware2)) + .Throws(new ApplicationException("Failed")); + + // Act + Func> action = async () => await Runner.ExecuteStep(Context, Body); + + // Assert + action + .ShouldThrow() + .WithMessage("Failed"); + } + + #region Helpers + + private IWorkflowStepMiddleware BuildStepMiddleware(int id = 0) + { + var middleware = A.Fake(); + A + .CallTo(HandleMethodFor(middleware)) + .ReturnsLazily(async call => + { + Out.WriteLine($@"Before step middleware {id}"); + var result = await call.Arguments[2].As().Invoke(); + Out.WriteLine($@"After step middleware {id}"); + return result; + }); + + return middleware; + } + + private static Expression>> HandleMethodFor(IWorkflowStepMiddleware middleware) => + () => middleware.HandleAsync( + A._, + A._, + A._); + + private static Expression>> RunMethodFor(IStepBody body) => + () => body.RunAsync(A._); + + #endregion + } +} diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs index 3705152d3..e548b8e88 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs @@ -3,152 +3,468 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Text; +using System.Threading.Tasks; +using FluentAssertions; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Services; -using FluentAssertions; using Xunit; namespace WorkflowCore.UnitTests.Services { public class WorkflowExecutorFixture { - class EventSubscribeTestWorkflow : IWorkflow + protected IWorkflowExecutor Subject; + protected IWorkflowHost Host; + protected IPersistenceProvider PersistenceProvider; + protected IWorkflowRegistry Registry; + protected IExecutionResultProcessor ResultProcesser; + protected ILifeCycleEventPublisher EventHub; + protected ICancellationProcessor CancellationProcessor; + protected IServiceProvider ServiceProvider; + protected IScopeProvider ScopeProvider; + protected IDateTimeProvider DateTimeProvider; + protected IStepExecutor StepExecutor; + protected IWorkflowMiddlewareRunner MiddlewareRunner; + protected WorkflowOptions Options; + + public WorkflowExecutorFixture() { - static int StartStepTicker = 0; - public string Id { get { return "EventSubscribeTestWorkflow"; } } - public int Version { get { return 1; } } - public void Build(IWorkflowBuilder builder) + Host = A.Fake(); + PersistenceProvider = A.Fake(); + ServiceProvider = A.Fake(); + ScopeProvider = A.Fake(); + Registry = A.Fake(); + ResultProcesser = A.Fake(); + EventHub = A.Fake(); + CancellationProcessor = A.Fake(); + DateTimeProvider = A.Fake(); + MiddlewareRunner = A.Fake(); + StepExecutor = A.Fake(); + + Options = new WorkflowOptions(A.Fake()); + + var stepExecutionScope = A.Fake(); + A.CallTo(() => ScopeProvider.CreateScope(A._)).Returns(stepExecutionScope); + A.CallTo(() => stepExecutionScope.ServiceProvider).Returns(ServiceProvider); + + var scope = A.Fake(); + var scopeFactory = A.Fake(); + A.CallTo(() => ServiceProvider.GetService(typeof(IServiceScopeFactory))).Returns(scopeFactory); + A.CallTo(() => scopeFactory.CreateScope()).Returns(scope); + A.CallTo(() => scope.ServiceProvider).Returns(ServiceProvider); + + A.CallTo(() => DateTimeProvider.Now).Returns(DateTime.Now); + A.CallTo(() => DateTimeProvider.UtcNow).Returns(DateTime.UtcNow); + + A + .CallTo(() => ServiceProvider.GetService(typeof(IWorkflowMiddlewareRunner))) + .Returns(MiddlewareRunner); + + A + .CallTo(() => ServiceProvider.GetService(typeof(IStepExecutor))) + .Returns(StepExecutor); + + A.CallTo(() => MiddlewareRunner + .RunPostMiddleware(A._, A._)) + .Returns(Task.CompletedTask); + + A.CallTo(() => MiddlewareRunner + .RunExecuteMiddleware(A._, A._)) + .Returns(Task.CompletedTask); + + A.CallTo(() => StepExecutor.ExecuteStep(A._, A._)) + .ReturnsLazily(call => + call.Arguments[1].As().RunAsync( + call.Arguments[0].As())); + + //config logging + var loggerFactory = new LoggerFactory(); + //loggerFactory.AddConsole(LogLevel.Debug); + + Subject = new WorkflowExecutor(Registry, ServiceProvider, ScopeProvider, DateTimeProvider, ResultProcesser, EventHub, CancellationProcessor, Options, loggerFactory); + } + + [Fact(DisplayName = "Should execute active step")] + public void should_execute_active_step() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance { - builder - .StartWith(context => - { - StartStepTicker++; - return ExecutionResult.Next(); - }) - .WaitFor("MyEvent", data => "0"); - } + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => step1Body.RunAsync(A.Ignored)).MustHaveHappened(); + A.CallTo(() => ResultProcesser.ProcessExecutionResult(instance, A.Ignored, A.Ignored, step1, A.Ignored, A.Ignored)).MustHaveHappened(); } - class StepExecutionTestWorkflow : IWorkflow + [Fact(DisplayName = "Should call execute middleware when not completed")] + public void should_call_execute_middleware_when_not_completed() { - public static int Step1StepTicker = 0; - public static int Step2StepTicker = 0; - public string Id { get { return "StepExecutionTestWorkflow"; } } - public int Version { get { return 1; } } - public void Build(IWorkflowBuilder builder) + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance { - builder - .StartWith(context => - { - Step1StepTicker++; - return ExecutionResult.Next(); - }) - .Then(context => - { - Step2StepTicker++; - return ExecutionResult.Next(); - }); - } + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => MiddlewareRunner.RunExecuteMiddleware(instance, A.Ignored)).MustHaveHappened(); } - protected IWorkflowExecutor Subject; - protected IWorkflowHost Host; - protected IPersistenceProvider PersistenceProvider; - protected IWorkflowRegistry Registry; - protected IExecutionResultProcessor ResultProcesser; - protected WorkflowOptions Options; + [Fact(DisplayName = "Should not call post middleware when not completed")] + public void should_not_call_post_middleware_when_not_completed() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); - public WorkflowExecutorFixture() + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => MiddlewareRunner.RunPostMiddleware(instance, A.Ignored)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "Should trigger step hooks")] + public void should_trigger_step_hooks() { - //setup dependency injection - IServiceCollection services = new ServiceCollection(); - services.AddLogging(); + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); - //TODO: mock these dependencies to make true unit tests - Options = new WorkflowOptions(); - services.AddSingleton(Options); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; - Host = A.Fake(); - PersistenceProvider = A.Fake(); - var serviceProvider = services.BuildServiceProvider(); + //act + Subject.Execute(instance); - //config logging - var loggerFactory = serviceProvider.GetService(); - loggerFactory.AddConsole(LogLevel.Debug); + //assert + A.CallTo(() => step1.InitForExecution(A.Ignored, A.Ignored, A.Ignored, A.Ignored)).MustHaveHappened(); + A.CallTo(() => step1.BeforeExecute(A.Ignored, A.Ignored, A.Ignored, A.Ignored)).MustHaveHappened(); + A.CallTo(() => step1.AfterExecute(A.Ignored, A.Ignored, A.Ignored, A.Ignored)).MustHaveHappened(); + } - Registry = serviceProvider.GetService(); - ResultProcesser = serviceProvider.GetService(); + [Fact(DisplayName = "Should not execute inactive step")] + public void should_not_execute_inactive_step() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); - Subject = new WorkflowExecutor(Registry, serviceProvider, new DateTimeProvider(), ResultProcesser, Options, loggerFactory); + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = false, StepId = 0 } + }) + }; + + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => step1Body.RunAsync(A.Ignored)).MustNotHaveHappened(); } - [Fact] - public void EventSubscribe() + [Fact(DisplayName = "Should map inputs")] + public void should_map_inputs() { //arrange - var def = new EventSubscribeTestWorkflow(); - Registry.RegisterWorkflow(def); + var param = A.Fake(); + + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body, new List + { + param + } + , new List()); - var instance = new WorkflowInstance(); - instance.WorkflowDefinitionId = def.Id; - instance.Version = def.Version; - instance.Status = WorkflowStatus.Runnable; - instance.NextExecution = 0; - instance.Id = "001"; + Given1StepWorkflow(step1, "Workflow", 1); - var executionPointer = new ExecutionPointer() + var instance = new WorkflowInstance { - Active = true, - StepId = 1 + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + Data = new DataClass { Value1 = 5 }, + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) }; - instance.ExecutionPointers.Add(executionPointer); + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => param.AssignInput(A.Ignored, step1Body, A.Ignored)) + .MustHaveHappened(); + } + + [Fact(DisplayName = "Should map outputs")] + public void should_map_outputs() + { + //arrange + var param = A.Fake(); + + var step1Body = A.Fake(); + A.CallTo(() => step1Body.Property1).Returns(7); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); + WorkflowStep step1 = BuildFakeStep(step1Body, new List(), new List + { + param + } + ); + + Given1StepWorkflow(step1, "Workflow", 1); + + var data = new DataClass { Value1 = 5 }; + + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + Data = data, + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; //act Subject.Execute(instance); //assert - executionPointer.EventName.Should().Be("MyEvent"); - executionPointer.EventKey.Should().Be("0"); - executionPointer.Active.Should().Be(false); + A.CallTo(() => param.AssignOutput(data, step1Body, A.Ignored)) + .MustHaveHappened(); } - [Fact] - public void StepExecution() + + + [Fact(DisplayName = "Should handle step exception")] + public void should_handle_step_exception() { //arrange - var def = new StepExecutionTestWorkflow(); - Registry.RegisterWorkflow(def); + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Throws(); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; - var instance = new WorkflowInstance(); - instance.WorkflowDefinitionId = def.Id; - instance.Version = def.Version; - instance.Status = WorkflowStatus.Runnable; - instance.NextExecution = 0; - instance.Id = "001"; + //act + Subject.Execute(instance); + + //assert + A.CallTo(() => step1Body.RunAsync(A.Ignored)).MustHaveHappened(); + A.CallTo(() => ResultProcesser.HandleStepException(instance, A.Ignored, A.Ignored, step1, A.Ignored)).MustHaveHappened(); + A.CallTo(() => ResultProcesser.ProcessExecutionResult(instance, A.Ignored, A.Ignored, step1, A.Ignored, A.Ignored)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "Should process after execution iteration")] + public void should_process_after_execution_iteration() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Persist(null)); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); - instance.ExecutionPointers.Add(new ExecutionPointer() + var instance = new WorkflowInstance { - Active = true, - StepId = 0 - }); + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; //act Subject.Execute(instance); + + //assert + A.CallTo(() => step1.AfterWorkflowIteration(A.Ignored, A.Ignored, instance, A.Ignored)).MustHaveHappened(); + } + + [Fact(DisplayName = "Should process cancellations")] + public void should_process_cancellations() + { + //arrange + var step1Body = A.Fake(); + A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Persist(null)); + WorkflowStep step1 = BuildFakeStep(step1Body); + Given1StepWorkflow(step1, "Workflow", 1); + + var instance = new WorkflowInstance + { + WorkflowDefinitionId = "Workflow", + Version = 1, + Status = WorkflowStatus.Runnable, + NextExecution = 0, + Id = "001", + ExecutionPointers = new ExecutionPointerCollection(new List + { + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } + }) + }; + + //act Subject.Execute(instance); //assert - StepExecutionTestWorkflow.Step1StepTicker.Should().Be(1); - StepExecutionTestWorkflow.Step2StepTicker.Should().Be(1); - instance.Status.Should().Be(WorkflowStatus.Complete); + A.CallTo(() => CancellationProcessor.ProcessCancellations(instance, A.Ignored, A.Ignored)).MustHaveHappened(); + } + + + private void Given1StepWorkflow(WorkflowStep step1, string id, int version) + { + A.CallTo(() => Registry.GetDefinition(id, version)).Returns(new WorkflowDefinition + { + Id = id, + Version = version, + DataType = typeof(object), + Steps = new WorkflowStepCollection + { + step1 + } + + }); + } + + private WorkflowStep BuildFakeStep(IStepBody stepBody) + { + return BuildFakeStep(stepBody, new List(), new List()); + } + + private WorkflowStep BuildFakeStep(IStepBody stepBody, List inputs, List outputs) + { + var result = A.Fake(); + A.CallTo(() => result.Id).Returns(0); + A.CallTo(() => result.BodyType).Returns(stepBody.GetType()); + A.CallTo(() => result.ResumeChildrenAfterCompensation).Returns(true); + A.CallTo(() => result.RevertChildrenAfterCompensation).Returns(false); + A.CallTo(() => result.ConstructBody(ServiceProvider)).Returns(stepBody); + A.CallTo(() => result.Inputs).Returns(inputs); + A.CallTo(() => result.Outputs).Returns(outputs); + A.CallTo(() => result.Outcomes).Returns(new List()); + A.CallTo(() => result.InitForExecution(A.Ignored, A.Ignored, A.Ignored, A.Ignored)).Returns(ExecutionPipelineDirective.Next); + A.CallTo(() => result.BeforeExecute(A.Ignored, A.Ignored, A.Ignored, A.Ignored)).Returns(ExecutionPipelineDirective.Next); + return result; + } + + public interface IStepWithProperties : IStepBody + { + int Property1 { get; set; } + int Property2 { get; set; } + int Property3 { get; set; } + DataClass Property4 { get; set; } + } + + public class DataClass + { + public int Value1 { get; set; } + public int Value2 { get; set; } + public int Value3 { get; set; } + public object Value4 { get; set; } + } + + public class DynamicDataClass + { + public Dictionary Storage { get; set; } = new Dictionary(); + + public int this[string propertyName] + { + get => Storage[propertyName]; + set => Storage[propertyName] = value; + } } } } diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs new file mode 100644 index 000000000..4b58d965f --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/WorkflowMiddlewareRunnerTests.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Services; +using Xunit; +using Xunit.Abstractions; + +namespace WorkflowCore.UnitTests.Services +{ + public class WorkflowMiddlewareRunnerTests + { + protected List Middleware { get; } + protected WorkflowInstance Workflow { get; } + protected WorkflowDefinition Definition { get; } + protected IServiceProvider ServiceProvider { get; } + protected IWorkflowMiddlewareErrorHandler TopLevelErrorHandler { get; } + protected IDefLevelErrorHandler DefLevelErrorHandler { get; } + protected IWorkflowMiddlewareRunner Runner { get; } + protected ITestOutputHelper Out { get; } + + public WorkflowMiddlewareRunnerTests(ITestOutputHelper output) + { + Out = output; + Middleware = new List(); + Workflow = new WorkflowInstance(); + Definition = new WorkflowDefinition(); + TopLevelErrorHandler = A.Fake(); + DefLevelErrorHandler = A.Fake(); + ServiceProvider = new ServiceCollection() + .AddTransient(_ => TopLevelErrorHandler) + .AddTransient(_ => DefLevelErrorHandler) + .BuildServiceProvider(); + + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .Returns(Task.CompletedTask); + A + .CallTo(HandleMethodFor(DefLevelErrorHandler)) + .Returns(Task.CompletedTask); + + Runner = new WorkflowMiddlewareRunner(Middleware, ServiceProvider); + } + + + [Fact(DisplayName = "RunPreMiddleware should run nothing when no middleware")] + public void RunPreMiddleware_should_run_nothing_when_no_middleware() + { + // Act + Func action = async () => await Runner.RunPreMiddleware(Workflow, Definition); + + // Assert + action.ShouldNotThrow(); + } + + [Fact(DisplayName = "RunPreMiddleware should run middleware when one middleware")] + public async Task RunPreMiddleware_should_run_middleware_when_one_middleware() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow); + Middleware.Add(middleware); + + // Act + await Runner.RunPreMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = "RunPreMiddleware should run all middleware when multiple middleware")] + public async Task RunPreMiddleware_should_run_all_middleware_when_multiple_middleware() + { + // Arrange + var middleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 1); + var middleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 2); + var middleware3 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + + // Act + await Runner.RunPreMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware3)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(middleware2)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware1)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = "RunPreMiddleware should only run middleware in PreWorkflow phase")] + public async Task RunPreMiddleware_should_only_run_middleware_in_PreWorkflow_phase() + { + // Arrange + var preMiddleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 1); + var preMiddleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 2); + var postMiddleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 3); + var postMiddleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 4); + Middleware.AddRange(new[] { postMiddleware1, postMiddleware2, preMiddleware1, preMiddleware2 }); + + // Act + await Runner.RunPreMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(preMiddleware2)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(preMiddleware1)) + .MustHaveHappenedOnceExactly()); + + A.CallTo(HandleMethodFor(postMiddleware1)).MustNotHaveHappened(); + A.CallTo(HandleMethodFor(postMiddleware2)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "RunPostMiddleware should run nothing when no middleware")] + public void RunPostMiddleware_should_run_nothing_when_no_middleware() + { + // Act + Func action = async () => await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + action.ShouldNotThrow(); + } + + [Fact(DisplayName = "RunPostMiddleware should run middleware when one middleware")] + public async Task RunPostMiddleware_should_run_middleware_when_one_middleware() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow); + Middleware.Add(middleware); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = "RunPostMiddleware should run all middleware when multiple middleware")] + public async Task RunPostMiddleware_should_run_all_middleware_when_multiple_middleware() + { + // Arrange + var middleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 1); + var middleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 2); + var middleware3 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware3)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(middleware2)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware1)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = "RunPostMiddleware should only run middleware in PostWorkflow phase")] + public async Task RunPostMiddleware_should_only_run_middleware_in_PostWorkflow_phase() + { + // Arrange + var postMiddleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 1); + var postMiddleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 2); + var preMiddleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 3); + var preMiddleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 4); + Middleware.AddRange(new[] { preMiddleware1, postMiddleware1, preMiddleware2, postMiddleware2 }); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(postMiddleware2)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(postMiddleware1)) + .MustHaveHappenedOnceExactly()); + + A.CallTo(HandleMethodFor(preMiddleware1)).MustNotHaveHappened(); + A.CallTo(HandleMethodFor(preMiddleware1)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "RunPostMiddleware should call top level error handler when middleware throws")] + public async Task RunPostMiddleware_should_call_top_level_error_handler_when_middleware_throws() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = + "RunPostMiddleware should call error handler on workflow def when middleware throws and def has handler defined")] + public async Task + RunPostMiddleware_should_call_error_handler_on_workflow_def_when_middleware_throws_and_def_has_handler() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + Definition.OnPostMiddlewareError = typeof(IDefLevelErrorHandler); + + // Act + await Runner.RunPostMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustNotHaveHappened(); + A + .CallTo(HandleMethodFor(DefLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run nothing when no middleware")] + public void RunExecuteMiddleware_should_run_nothing_when_no_middleware() + { + // Act + Func action = async () => await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + action.ShouldNotThrow(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run middleware when one middleware")] + public async Task RunExecuteMiddleware_should_run_middleware_when_one_middleware() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow); + Middleware.Add(middleware); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should run all middleware when multiple middleware")] + public async Task RunExecuteMiddleware_should_run_all_middleware_when_multiple_middleware() + { + // Arrange + var middleware1 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + var middleware2 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 2); + var middleware3 = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 3); + Middleware.AddRange(new[] { middleware1, middleware2, middleware3 }); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(middleware3)) + .MustHaveHappenedOnceExactly() + .Then(A + .CallTo(HandleMethodFor(middleware2)) + .MustHaveHappenedOnceExactly()) + .Then(A + .CallTo(HandleMethodFor(middleware1)) + .MustHaveHappenedOnceExactly()); + } + + [Fact(DisplayName = "RunExecuteMiddleware should only run middleware in ExecuteWorkflow phase")] + public async Task RunExecuteMiddleware_should_only_run_middleware_in_ExecuteWorkflow_phase() + { + // Arrange + var executeMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + var postMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PostWorkflow, 2); + var preMiddleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.PreWorkflow, 3); + Middleware.AddRange(new[] { preMiddleware, postMiddleware, executeMiddleware }); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(executeMiddleware)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(HandleMethodFor(preMiddleware)).MustNotHaveHappened(); + A.CallTo(HandleMethodFor(postMiddleware)).MustNotHaveHappened(); + } + + [Fact(DisplayName = "RunExecuteMiddleware should call top level error handler when middleware throws")] + public async Task RunExecuteMiddleware_should_call_top_level_error_handler_when_middleware_throws() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + + [Fact(DisplayName = + "RunExecuteMiddleware should call error handler on workflow def when middleware throws and def has handler defined")] + public async Task + RunExecuteMiddleware_should_call_error_handler_on_workflow_def_when_middleware_throws_and_def_has_handler() + { + // Arrange + var middleware = BuildWorkflowMiddleware(WorkflowMiddlewarePhase.ExecuteWorkflow, 1); + A.CallTo(HandleMethodFor(middleware)).ThrowsAsync(new ApplicationException("Something went wrong")); + Middleware.AddRange(new[] { middleware }); + Definition.OnExecuteMiddlewareError = typeof(IDefLevelErrorHandler); + + // Act + await Runner.RunExecuteMiddleware(Workflow, Definition); + + // Assert + A + .CallTo(HandleMethodFor(TopLevelErrorHandler)) + .MustNotHaveHappened(); + A + .CallTo(HandleMethodFor(DefLevelErrorHandler)) + .MustHaveHappenedOnceExactly(); + } + + #region Helpers + + private IWorkflowMiddleware BuildWorkflowMiddleware( + WorkflowMiddlewarePhase phase, + int id = 0 + ) + { + var middleware = A.Fake(); + A.CallTo(() => middleware.Phase).Returns(phase); + A + .CallTo(HandleMethodFor(middleware)) + .ReturnsLazily(async call => + { + Out.WriteLine($@"Before workflow middleware {id}"); + await call.Arguments[1].As().Invoke(); + Out.WriteLine($@"After workflow middleware {id}"); + }); + + return middleware; + } + + private static Expression> HandleMethodFor(IWorkflowMiddleware middleware) => + () => middleware.HandleAsync( + A._, + A._); + + private static Expression> HandleMethodFor(IWorkflowMiddlewareErrorHandler errorHandler) => + () => errorHandler.HandleAsync(A._); + + public interface IDefLevelErrorHandler : IWorkflowMiddlewareErrorHandler + { + } + + #endregion + } +} diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs b/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs new file mode 100644 index 000000000..3fb0bb75d --- /dev/null +++ b/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs @@ -0,0 +1,56 @@ +using FakeItEasy; +using System; +using WorkflowCore.Models; +using WorkflowCore.Services; +using FluentAssertions; +using Xunit; + +namespace WorkflowCore.UnitTests.Services +{ + public class WorkflowRegistryFixture + { + protected IServiceProvider ServiceProvider { get; } + protected WorkflowRegistry Subject { get; } + protected WorkflowDefinition Definition { get; } + + public WorkflowRegistryFixture() + { + ServiceProvider = A.Fake(); + Subject = new WorkflowRegistry(ServiceProvider); + + Definition = new WorkflowDefinition{ + Id = "TestWorkflow", + Version = 1, + }; + Subject.RegisterWorkflow(Definition); + } + + [Fact(DisplayName = "Should return existing workflow")] + public void getdefinition_should_return_existing_workflow() + { + Subject.GetDefinition(Definition.Id).Should().Be(Definition); + Subject.GetDefinition(Definition.Id, Definition.Version).Should().Be(Definition); + } + + [Fact(DisplayName = "Should return null on unknown workflow")] + public void getdefinition_should_return_null_on_unknown() + { + Subject.GetDefinition("UnkownWorkflow").Should().BeNull(); + Subject.GetDefinition("UnkownWorkflow", 1).Should().BeNull(); + } + + [Fact(DisplayName = "Should return highest version of existing workflow")] + public void getdefinition_should_return_highest_version_workflow() + { + var definition2 = new WorkflowDefinition{ + Id = Definition.Id, + Version = Definition.Version + 1, + }; + Subject.RegisterWorkflow(definition2); + + Subject.GetDefinition(Definition.Id).Should().Be(definition2); + Subject.GetDefinition(Definition.Id, definition2.Version).Should().Be(definition2); + Subject.GetDefinition(Definition.Id, Definition.Version).Should().Be(Definition); + } + } +} \ No newline at end of file diff --git a/test/WorkflowCore.UnitTests/SingleNodeLockProviderTests/SingleNodeLockProviderTests.cs b/test/WorkflowCore.UnitTests/SingleNodeLockProviderTests/SingleNodeLockProviderTests.cs index d2cf041a5..f4f411ffb 100644 --- a/test/WorkflowCore.UnitTests/SingleNodeLockProviderTests/SingleNodeLockProviderTests.cs +++ b/test/WorkflowCore.UnitTests/SingleNodeLockProviderTests/SingleNodeLockProviderTests.cs @@ -1,8 +1,5 @@ -using FluentAssertions; -using NUnit.Framework; +using NUnit.Framework; using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Services; using WorkflowCore.TestAssets.LockProvider; diff --git a/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj b/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj index a3a91b2fc..3ca6d6038 100644 --- a/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj +++ b/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.UnitTests WorkflowCore.UnitTests true @@ -11,21 +10,11 @@ + - - - - - - - - - - -