diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..a7923835b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @danielgerlag @glucaci diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..21a4c65b6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [danielgerlag] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..471baed24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..30d77c8db --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ + +**Describe the change** +A clear and concise description of what the change is. Any PR submitted without a description of the change will not be reviewed. + +**Describe your implementation or design** +How did you go about implementing the change? + +**Tests** +Did you cover your changes with tests? + +**Breaking change** +Do you changes break compatibility with previous versions? + +**Additional context** +Any additional information you'd like to provide? \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 000000000..83e247208 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,162 @@ +name: .NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + Unit-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Unit Tests + run: dotnet test test/WorkflowCore.UnitTests --no-build --verbosity normal -p:ParallelizeTestCollections=false + Integration-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Integration Tests + run: dotnet test test/WorkflowCore.IntegrationTests --no-build --verbosity normal -p:ParallelizeTestCollections=false + MongoDB-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: MongoDB Tests + run: dotnet test test/WorkflowCore.Tests.MongoDB --no-build --verbosity normal -p:ParallelizeTestCollections=false + MySQL-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: MySQL Tests + run: dotnet test test/WorkflowCore.Tests.MySQL --no-build --verbosity normal -p:ParallelizeTestCollections=false + PostgreSQL-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: PostgreSQL Tests + run: dotnet test test/WorkflowCore.Tests.PostgreSQL --no-build --verbosity normal -p:ParallelizeTestCollections=false + Redis-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Redis Tests + run: dotnet test test/WorkflowCore.Tests.Redis --no-build --verbosity normal -p:ParallelizeTestCollections=false + SQLServer-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: SQL Server Tests + run: dotnet test test/WorkflowCore.Tests.SqlServer --no-build --verbosity normal -p:ParallelizeTestCollections=false + Elasticsearch-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Elasticsearch Tests + run: dotnet test test/WorkflowCore.Tests.Elasticsearch --no-build --verbosity normal -p:ParallelizeTestCollections=false + Oracle-Tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Oracle Tests + run: dotnet test test/WorkflowCore.Tests.Oracle --no-build --verbosity normal -p:ParallelizeTestCollections=false diff --git a/README.md b/README.md index 7faad2599..ab398cc2c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Workflow Core [![Build status](https://ci.appveyor.com/api/projects/status/xnby6p5v4ur04u76?svg=true)](https://ci.appveyor.com/project/danielgerlag/workflow-core) +[](https://api.gitsponsors.com/api/badge/link?p=xj6mObb7nZAJGyuABfd8nD5XWf3SE4oUfw0vmCgSiJeIfNlzJAej0FWX8oFdYm6D7bvZpCf6qANVBNPWid4dRQ==) Workflow Core is a light weight embeddable workflow engine targeting .NET Standard. Think: long running processes with multiple tasks that need to track state. It supports pluggable persistence and concurrency providers to allow for multi-node clusters. @@ -34,7 +35,7 @@ public class MyWorkflow : IWorkflow ## JSON / YAML Workflow Definitions -Define your workflows in JSON or YAML +Define your workflows in JSON or YAML, need to install WorkFlowCore.DSL ```json { @@ -65,7 +66,7 @@ Steps: StepType: MyApp.GoodbyeWorld, MyApp ``` -### Sample use cases +## Sample use cases * New user workflow ```c# @@ -132,12 +133,14 @@ There are several persistence providers available as separate Nuget packages. * MemoryPersistenceProvider *(Default provider, for demo and testing purposes)* * [MongoDB](src/providers/WorkflowCore.Persistence.MongoDB) +* [Cosmos DB](src/providers/WorkflowCore.Providers.Azure) * [Amazon DynamoDB](src/providers/WorkflowCore.Providers.AWS) * [SQL Server](src/providers/WorkflowCore.Persistence.SqlServer) * [PostgreSQL](src/providers/WorkflowCore.Persistence.PostgreSQL) * [Sqlite](src/providers/WorkflowCore.Persistence.Sqlite) * [MySQL](src/providers/WorkflowCore.Persistence.MySQL) * [Redis](src/providers/WorkflowCore.Providers.Redis) +* [Oracle](src/providers/WorkflowCore.Persistence.Oracle) ## Search @@ -160,6 +163,8 @@ These are also available as separate Nuget packages. * [Parallel ForEach](src/samples/WorkflowCore.Sample09) +* [Sync ForEach](src/samples/WorkflowCore.Sample09s) + * [While Loop](src/samples/WorkflowCore.Sample10) * [If Statement](src/samples/WorkflowCore.Sample11) @@ -180,12 +185,12 @@ These are also available as separate Nuget packages. * [Deferred execution & re-entrant steps](src/samples/WorkflowCore.Sample05) +* [Human(User) Workflow](src/samples/WorkflowCore.Sample08) + * [Looping](src/samples/WorkflowCore.Sample02) * [Exposing a REST API](src/samples/WebApiSample) -* [Human(User) Workflow](src/samples/WorkflowCore.Sample08) - * [Testing](src/samples/WorkflowCore.TestSample01) diff --git a/ReleaseNotes/3.3.0.md b/ReleaseNotes/3.3.0.md new file mode 100644 index 000000000..da825c7ee --- /dev/null +++ b/ReleaseNotes/3.3.0.md @@ -0,0 +1,284 @@ +# Workflow Core 3.3.0 + +# Workflow Middleware + +Workflows can be extended with Middleware that run before/after workflows start/complete as well as around workflow steps to provide flexibility in implementing cross-cutting concerns such as [log correlation](https://www.frakkingsweet.com/net-core-log-correlation-easy-access-to-headers/), [retries](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly), and other use-cases. + +This is done by implementing and registering `IWorkflowMiddleware` for workflows or `IWorkflowStepMiddleware` for steps. + +## Step Middleware + +Step middleware lets you run additional code around the execution of a given step and alter its behavior. Implementing a step middleware should look familiar to anyone familiar with [ASP.NET Core's middleware pipeline](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) or [`HttpClient`'s `DelegatingHandler` middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#outgoing-request-middleware). + +### Usage + +First, create your own middleware class that implements `IWorkflowStepMiddleware`. Here's an example of a middleware that adds workflow ID and step ID to the log correlation context of every workflow step in your app. + +**Important:** You must make sure to call `next()` as part of your middleware. If you do not do this, your step will never run. + +```cs +public class LogCorrelationStepMiddleware : IWorkflowStepMiddleware +{ + private readonly ILogger _log; + + public LogCorrelationStepMiddleware( + ILogger log) + { + _log = log; + } + + public async Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next) + { + var workflowId = context.Workflow.Id; + var stepId = context.Step.Id; + + // Uses log scope to add a few attributes to the scope + using (_log.BeginScope("{@WorkflowId}", workflowId)) + using (_log.BeginScope("{@StepId}", stepId)) + { + // Calling next ensures step gets executed + return await next(); + } + } +} +``` + +Here's another example of a middleware that uses the [Polly](https://github.com/App-vNext/Polly) dotnet resiliency library to implement retries on workflow steps based off a custom retry policy. + +```cs +public class PollyRetryStepMiddleware : IWorkflowStepMiddleware +{ + private const string StepContextKey = "WorkflowStepContext"; + private const int MaxRetries = 3; + private readonly ILogger _log; + + public PollyRetryMiddleware(ILogger log) + { + _log = log; + } + + // Consult Polly's docs for more information on how to build + // retry policies: + // https://github.com/App-vNext/Polly + public IAsyncPolicy GetRetryPolicy() => + Policy + .Handle() + .RetryAsync( + MaxRetries, + (result, retryCount, context) => + UpdateRetryCount( + result.Exception, + retryCount, + context[StepContextKey] as IStepExecutionContext) + ); + + public async Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next + ) + { + return await GetRetryPolicy().ExecuteAsync( + ctx => next(), + // The step execution context gets passed down so that + // the step is accessible within the retry policy + new Dictionary + { + { StepContextKey, context } + }); + } + + private Task UpdateRetryCount( + Exception exception, + int retryCount, + IStepExecutionContext stepContext) + { + var stepInstance = stepContext.ExecutionPointer; + stepInstance.RetryCount = retryCount; + return Task.CompletedTask; + } +} +``` + +## Pre/Post Workflow Middleware + +Workflow middleware run either before a workflow starts or after a workflow completes and can be used to hook into the workflow lifecycle or alter the workflow itself before it is started. + +### Pre Workflow Middleware + +These middleware get run before the workflow is started and can potentially alter properties on the `WorkflowInstance`. + +The following example illustrates setting the `Description` property on the `WorkflowInstance` using a middleware that interprets the data on the passed workflow. This is useful in cases where you want the description of the workflow to be derived from the data passed to the workflow. + +Note that you use `WorkflowMiddlewarePhase.PreWorkflow` to specify that it runs before the workflow starts. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +// AddDescriptionWorkflowMiddleware.cs +public class AddDescriptionWorkflowMiddleware : IWorkflowMiddleware +{ + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.PreWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next + ) + { + if (workflow.Data is IDescriptiveWorkflowParams descriptiveParams) + { + workflow.Description = descriptiveParams.Description; + } + + return next(); + } +} + +// IDescriptiveWorkflowParams.cs +public interface IDescriptiveWorkflowParams +{ + string Description { get; } +} + +// MyWorkflowParams.cs +public MyWorkflowParams : IDescriptiveWorkflowParams +{ + public string Description => $"Run task '{TaskName}'"; + + public string TaskName { get; set; } +} +``` + +### Exception Handling in Pre Workflow Middleware + +Pre workflow middleware exception handling gets treated differently from post workflow middleware. Since the middleware runs before the workflow starts, any exceptions thrown within a pre workflow middleware will bubble up to the `StartWorkflow` method and it is up to the caller of `StartWorkflow` to handle the exception and act accordingly. + +```cs +public async Task MyMethodThatStartsAWorkflow() +{ + try + { + await host.StartWorkflow("HelloWorld", 1, null); + } + catch(Exception ex) + { + // Handle the exception appropriately + } +} +``` + +### Post Workflow Middleware + +These middleware get run after the workflow has completed and can be used to perform additional actions for all workflows in your app. + +The following example illustrates how you can use a post workflow middleware to print a summary of the workflow to console. + +Note that you use `WorkflowMiddlewarePhase.PostWorkflow` to specify that it runs after the workflow completes. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +public class PrintWorkflowSummaryMiddleware : IWorkflowMiddleware +{ + private readonly ILogger _log; + + public PrintWorkflowSummaryMiddleware( + ILogger log + ) + { + _log = log; + } + + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.PostWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next + ) + { + if (!workflow.CompleteTime.HasValue) + { + return next(); + } + + var duration = workflow.CompleteTime.Value - workflow.CreateTime; + _log.LogInformation($@"Workflow {workflow.Description} completed in {duration:g}"); + + foreach (var step in workflow.ExecutionPointers) + { + var stepName = step.StepName; + var stepDuration = (step.EndTime - step.StartTime) ?? TimeSpan.Zero; + _log.LogInformation($" - Step {stepName} completed in {stepDuration:g}"); + } + + return next(); + } +} +``` + +### Exception Handling in Post Workflow Middleware + +Post workflow middleware exception handling gets treated differently from pre workflow middleware. At the time that the workflow completes, your workflow has ran already so an uncaught exception would be difficult to act on. + +By default, if a workflow middleware throws an exception, it will be logged and the workflow will complete as normal. This behavior can be changed, however. + +To override the default post workflow error handling for all workflows in your app, just register a new `IWorkflowMiddlewareErrorHandler` in the dependency injection framework with your custom behavior as follows. + +```cs +// CustomMiddlewareErrorHandler.cs +public class CustomHandler : IWorkflowMiddlewareErrorHandler +{ + public Task HandleAsync(Exception ex) + { + // Handle your error asynchronously + } +} + +// Startup.cs +public void ConfigureServices(IServiceCollection services) +{ + // Other workflow configuration + services.AddWorkflow(); + + // Should go after .AddWorkflow() + services.AddTransient(); +} +``` + +## Registering Middleware + +In order for middleware to take effect, they must be registered with the built-in dependency injection framework using the convenience helpers. + +**Note:** Middleware will be run in the order that they are registered with middleware that are registered earlier running earlier in the chain and finishing later in the chain. For pre/post workflow middleware, all pre middleware will be run before a workflow starts and all post middleware will be run after a workflow completes. + +```cs +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + ... + + // Add workflow middleware + services.AddWorkflowMiddleware(); + services.AddWorkflowMiddleware(); + + // Add step middleware + services.AddWorkflowStepMiddleware(); + services.AddWorkflowStepMiddleware(); + + ... + } +} +``` + +## More Information + +See the [Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) sample for full examples of workflow middleware in action. + + +Many thanks to Danil Flores @dflor003 for this contribution! \ No newline at end of file diff --git a/ReleaseNotes/3.4.0.md b/ReleaseNotes/3.4.0.md new file mode 100644 index 000000000..e3708be5d --- /dev/null +++ b/ReleaseNotes/3.4.0.md @@ -0,0 +1,64 @@ +# Workflow Core 3.4.0 + +## Execute Workflow Middleware + +These middleware get run after each workflow execution and can be used to perform additional actions or build metrics/statistics for all workflows in your app. + +The following example illustrates how you can use a execute workflow middleware to build [prometheus](https://prometheus.io/) metrics. + +Note that you use `WorkflowMiddlewarePhase.ExecuteWorkflow` to specify that it runs after each workflow execution. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +public class MetricsMiddleware : IWorkflowMiddleware +{ + private readonly ConcurrentHashSet() _suspendedWorkflows = + new ConcurrentHashSet(); + + private readonly Counter _completed; + private readonly Counter _suspended; + + public MetricsMiddleware() + { + _completed = Prometheus.Metrics.CreateCounter( + "workflow_completed", "Workflow completed"); + + _suspended = Prometheus.Metrics.CreateCounter( + "workflow_suspended", "Workflow suspended"); + } + + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.ExecuteWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next) + { + switch (workflow.Status) + { + case WorkflowStatus.Complete: + TryDecrementSuspended(workflow); + _completed.Inc(); + break; + case WorkflowStatus.Suspended: + _suspendedWorkflows.Add(workflow.Id); + _suspended.Inc(); + break; + default: + TryDecrementSuspended(workflow); + break; + } + + return next(); + } + + private void TryDecrementSuspended(WorkflowInstance workflow) + { + if (_suspendedWorkflows.TryRemove(workflow.Id)) + { + _suspended.Dec(); + } + } +} +``` \ No newline at end of file diff --git a/ReleaseNotes/3.6.0.md b/ReleaseNotes/3.6.0.md new file mode 100644 index 000000000..697976a46 --- /dev/null +++ b/ReleaseNotes/3.6.0.md @@ -0,0 +1,8 @@ +# Workflow Core 3.6.0 + +## Scheduled Commands + +Introduces the ability to schedule delayed commands to process a workflow or event, by persisting them to storage. +This is the first step toward removing constant polling of the DB. It also filters out duplicate work items on the queue which is the current problem the greylist tries to solve. +Initial implementation is supported by MongoDb, SQL Server, PostgeSQL, MySQL and SQLite. +Additional support from the other persistence providers to follow. diff --git a/WorkflowCore.sln b/WorkflowCore.sln index 047ccf0c3..25c2016d2 100644 --- a/WorkflowCore.sln +++ b/WorkflowCore.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29509.3 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF47161E-E399-451C-BDE8-E92AAD3BD761}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6AC9AEB-24EF-475A-B190-AA4D9E01270A}" ProjectSection(SolutionItems) = preProject - readme.md = readme.md + README.md = README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5080DB09-CBE8-4C45-9957-C3BB7651755E}" @@ -103,11 +103,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReleaseNotes", "ReleaseNote ReleaseNotes\2.1.2.md = ReleaseNotes\2.1.2.md ReleaseNotes\3.0.0.md = ReleaseNotes\3.0.0.md ReleaseNotes\3.1.0.md = ReleaseNotes\3.1.0.md + ReleaseNotes\3.3.0.md = ReleaseNotes\3.3.0.md + ReleaseNotes\3.4.0.md = ReleaseNotes\3.4.0.md + ReleaseNotes\3.6.0.md = ReleaseNotes\3.6.0.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample14", "src\samples\WorkflowCore.Sample14\WorkflowCore.Sample14.csproj", "{6BC66637-B42A-4334-ADFB-DBEC9F29D293}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Testing", "test\WorkflowCore.Testing\WorkflowCore.Testing.csproj", "{62A9709E-27DA-42EE-B94F-5AF431D86354}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Testing", "src\WorkflowCore.Testing\WorkflowCore.Testing.csproj", "{62A9709E-27DA-42EE-B94F-5AF431D86354}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.TestSample01", "src\samples\WorkflowCore.TestSample01\WorkflowCore.TestSample01.csproj", "{0E3C1496-8E7C-411A-A536-C7C9CE4EED4E}" EndProject @@ -141,6 +144,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.DSL", "src\Wor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample18", "src\samples\WorkflowCore.Sample18\WorkflowCore.Sample18.csproj", "{5BE6D628-B9DB-4C76-AAEB-8F3800509A84}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample09s", "src\samples\WorkflowCore.Sample09s\WorkflowCore.Sample09s.csproj", "{E32CF21A-29CC-46D1-8044-FCC327F2B281}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "test\ScratchPad\ScratchPad.csproj", "{51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.QueueProviders.RabbitMQ", "test\WorkflowCore.Tests.QueueProviders.RabbitMQ\WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj", "{54DE20BA-EBA7-4BF0-9BD9-F03766849716}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Sample19", "src\samples\WorkflowCore.Sample19\WorkflowCore.Sample19.csproj", "{1223ED47-3E5E-4960-B70D-DFAF550F6666}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.RavenDB", "src\providers\WorkflowCore.Persistence.RavenDB\WorkflowCore.Persistence.RavenDB.csproj", "{AF205715-C8B7-42EF-BF14-AFC9E7F27242}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Persistence.Oracle", "src\providers\WorkflowCore.Persistence.Oracle\WorkflowCore.Persistence.Oracle.csproj", "{635629BC-9D5C-40C6-BBD0-060550ECE290}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowCore.Tests.Oracle", "test\WorkflowCore.Tests.Oracle\WorkflowCore.Tests.Oracle.csproj", "{A2837F1C-3740-4375-9069-81AE32C867CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +360,34 @@ Global {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Debug|Any CPU.Build.0 = Debug|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BE6D628-B9DB-4C76-AAEB-8F3800509A84}.Release|Any CPU.Build.0 = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E32CF21A-29CC-46D1-8044-FCC327F2B281}.Release|Any CPU.Build.0 = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C}.Release|Any CPU.Build.0 = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54DE20BA-EBA7-4BF0-9BD9-F03766849716}.Release|Any CPU.Build.0 = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1223ED47-3E5E-4960-B70D-DFAF550F6666}.Release|Any CPU.Build.0 = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF205715-C8B7-42EF-BF14-AFC9E7F27242}.Release|Any CPU.Build.0 = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Debug|Any CPU.Build.0 = Debug|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.ActiveCfg = Release|Any CPU + {635629BC-9D5C-40C6-BBD0-060550ECE290}.Release|Any CPU.Build.0 = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2837F1C-3740-4375-9069-81AE32C867CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -400,6 +445,13 @@ Global {78217204-B873-40B9-8875-E3925B2FBCEC} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} {20B98905-08CB-4854-8E2C-A31A078383E9} = {EF47161E-E399-451C-BDE8-E92AAD3BD761} {5BE6D628-B9DB-4C76-AAEB-8F3800509A84} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} + {E32CF21A-29CC-46D1-8044-FCC327F2B281} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} + {51BB7DCD-01DD-453D-A1E7-17E5E3DBB14C} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {54DE20BA-EBA7-4BF0-9BD9-F03766849716} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} + {1223ED47-3E5E-4960-B70D-DFAF550F6666} = {5080DB09-CBE8-4C45-9957-C3BB7651755E} + {AF205715-C8B7-42EF-BF14-AFC9E7F27242} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {635629BC-9D5C-40C6-BBD0-060550ECE290} = {2EEE6ABD-EE9B-473F-AF2D-6DABB85D7BA2} + {A2837F1C-3740-4375-9069-81AE32C867CA} = {E6CEAD8D-F565-471E-A0DC-676F54EAEDEB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC0FA8D3-6449-4FDA-BB46-ECF58FAD23B4} diff --git a/docs/elastic-search.md b/docs/elastic-search.md index ce187de80..e921f958d 100644 --- a/docs/elastic-search.md +++ b/docs/elastic-search.md @@ -21,7 +21,7 @@ dotnet add package WorkflowCore.Providers.Elasticsearch Use the `.UseElasticsearch` extension method on `IServiceCollection` when building your service provider -```C# +``` using Nest; ... services.AddWorkflow(cfg => @@ -50,7 +50,7 @@ This will do a full text search on the following default fields In addition you can search data within your own custom data object if it implements `ISearchable` - ```c# + ``` using WorkflowCore.Interfaces; ... public class MyData : ISearchable @@ -72,7 +72,7 @@ This will do a full text search on the following default fields ##### Examples Search all fields for "puppies" - ```c# + ``` searchIndex.Search("puppies", 0, 10); ``` @@ -96,7 +96,7 @@ The following filter types are available ##### Examples Filtering by reference - ```c# + ``` using WorkflowCore.Models.Search; ... @@ -104,22 +104,22 @@ The following filter types are available ``` Filtering by workflows started after a date - ```c# + ``` searchIndex.Search("", 0, 10, DateRangeFilter.After(x => x.CreateTime, startDate)); ``` Filtering by workflows completed within a period - ```c# + ``` searchIndex.Search("", 0, 10, DateRangeFilter.Between(x => x.CompleteTime, startDate, endDate)); ``` Filtering by workflows in a state - ```c# + ``` searchIndex.Search("", 0, 10, StatusFilter.Equals(WorkflowStatus.Complete)); ``` Filtering against your own custom data class - ```c# + ``` class MyData { diff --git a/docs/getting-started.md b/docs/getting-started.md index 48ee3b904..26bc01879 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -20,7 +20,7 @@ public class HelloWorld : StepBody ``` *The `StepBody` and `StepBodyAsync` class implementations are constructed by the workflow host which first tries to use IServiceProvider for dependency injection, if it can't construct it with this method, it will search for a parameterless constructor* -### Then we define the workflow structure by composing a chain of steps. The is done by implementing the IWorkflow interface +### Then we define the workflow structure by composing a chain of steps. This is done by implementing the IWorkflow interface ```C# public class HelloWorldWorkflow : IWorkflow @@ -142,7 +142,7 @@ public class MyDataClass { public int Value1 { get; set; } public int Value2 { get; set; } - public int Value3 { get; set; } + public int Answer { get; set; } } //Our workflow definition with strongly typed internal data and mapped inputs & outputs @@ -154,9 +154,9 @@ public class PassingDataWorkflow : IWorkflow .StartWith() .Input(step => step.Input1, data => data.Value1) .Input(step => step.Input2, data => data.Value2) - .Output(data => data.Value3, step => step.Output) + .Output(data => data.Answer, step => step.Output) .Then() - .Input(step => step.Message, data => "The answer is " + data.Value3.ToString()); + .Input(step => step.Message, data => "The answer is " + data.Answer.ToString()); } ... } @@ -175,8 +175,8 @@ or in jSON format "StepType": "MyApp.AddNumbers, MyApp", "NextStepId": "ShowResult", "Inputs": { - "Value1": "data.Value1", - "Value2": "data.Value2" + "Input1": "data.Value1", + "Input2": "data.Value2" }, "Outputs": { "Answer": "step.Output" @@ -186,7 +186,7 @@ or in jSON format "Id": "ShowResult", "StepType": "MyApp.CustomMessage, MyApp", "Inputs": { - "Message": "\"The answer is \" + data.Value1" + "Message": "\"The answer is \" + data.Answer" } } ] @@ -203,14 +203,14 @@ Steps: StepType: MyApp.AddNumbers, MyApp NextStepId: ShowResult Inputs: - Value1: data.Value1 - Value2: data.Value2 + Input1: data.Value1 + Input2: data.Value2 Outputs: Answer: step.Output - Id: ShowResult StepType: MyApp.CustomMessage, MyApp Inputs: - Message: '"The answer is " + data.Value1' + Message: '"The answer is " + data.Answer' ``` diff --git a/docs/images/performance-test-workflows-latency.png b/docs/images/performance-test-workflows-latency.png new file mode 100644 index 000000000..621fc2760 Binary files /dev/null and b/docs/images/performance-test-workflows-latency.png differ diff --git a/docs/images/performance-test-workflows-per-second.png b/docs/images/performance-test-workflows-per-second.png new file mode 100644 index 000000000..8c4b61754 Binary files /dev/null and b/docs/images/performance-test-workflows-per-second.png differ diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 000000000..77fa625a6 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,87 @@ +# Performance Test + +Workflow-core version 3.7.0 was put under test to evaluate its performance. The setup used was single node with the default MemoryPersistenceProvider persistence provider. + +## Methodology + +- Test Environment - Test were run on following two environments one after the other to see how workflow-core performance with a lower vs higher hardware configuration. + - Lower configuration + - Cores: 8 vCPU ([Standard_D8s_v3](https://learn.microsoft.com/azure/virtual-machines/dv3-dsv3-series)) + - RAM: 32 GB + - OS: Linux Ubuntu 20.04 + - dotNet 6 + - Higher configuration + - Cores: 32 vCPU ([Standard_D32as_v4](https://learn.microsoft.com/azure/virtual-machines/dav4-dasv4-series)) + - RAM: 128 GB + - OS: Linux Ubuntu 20.04 + - dotNet 6 +- Test Workflow: Workflow consist of 3 basic steps. These 3 simple steps were chosen to test the performance of the workflow engine with minimal yet sufficient complexity and to avoid any external dependencies. + - Step1 : Generate a [random number](https://learn.microsoft.com/dotnet/api/system.random?view=net-6.0) between 1 to 10 and print it on standard output. + - Step2 : [Conditional step](https://github.com/danielgerlag/workflow-core/blob/master/docs/control-structures.md) + - Step 2.1: If value generate in step1 is > 5 then print it on standard output. + - Step 2.2: If value generate in step1 is <= 5 then print it on standard output. + - Step3: Prints a good bye message on standard output. +- Test tools: + - [NBomber](https://nbomber.com/docs/getting-started/overview/) was used as performance testing framework with C# console app as base. + +- Test scenarios: + - Each type of test run executed for 20 minutes. + - NBomber Load Simulation of type [KeepConstant](https://nbomber.com/docs/using-nbomber/basic-api/load-simulation#keep-constant) copies was used. This type of simulation keep a constant amount of Scenario copies(instances) for a specific period. + - Concurrent copies [1,2,3,4,5,6,7,8,10,12,14,16,32,64,128,256,512,1024] were tested. + - For example if we take Concurrent copies=4 and Duration=20 minutes this means that NBomber will ensure that we have 4 instance of Test Workflow running in parallel for 20 minutes. + +## Results + +- Workflow per seconds - Below tables shows how many workflows we are able to execute per second on two different environment with increasing number of concurrent copies. + +| **Concurrent Copies** | **8 vCPU** | **32 vCPU** | +| :-------------------: | :--------: | :---------: | +| **1** | 300.6 | 504.7 | +| **2** | 310.3 | 513.1 | +| **3** | 309.6 | 519.3 | +| **4** | 314.7 | 521.3 | +| **5** | 312.4 | 519.0 | +| **6** | 314.7 | 517.7 | +| **7** | 318.9 | 516.7 | +| **8** | 318.4 | 517.5 | +| **10** | 322.6 | 517.1 | +| **12** | 319.7 | 517.6 | +| **14** | 322.4 | 518.1 | +| **16** | 327.0 | 515.5 | +| **32** | 327.7 | 515.8 | +| **64** | 330.7 | 523.7 | +| **128** | 332.8 | 526.9 | +| **256** | 332.8 | 529.1 | +| **512** | 332.8 | 529.1 | +| **1024** | 341.3 | 529.1 | + +![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 index 81a880c63..00799090a 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -9,4 +9,6 @@ There are several persistence providers available as separate Nuget packages. * [PostgreSQL](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.PostgreSQL) * [Sqlite](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.Sqlite) * [Amazon DynamoDB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.AWS) +* [Cosmos DB](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.Azure) * [Redis](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Providers.Redis) +* [Oracle](https://github.com/danielgerlag/workflow-core/tree/master/src/providers/WorkflowCore.Persistence.Oracle) \ No newline at end of file diff --git a/docs/samples.md b/docs/samples.md index a1b862799..f69290c57 100644 --- a/docs/samples.md +++ b/docs/samples.md @@ -32,4 +32,6 @@ [Exposing a REST API](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WebApiSample) -[Human(User) Workflow](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample08) \ No newline at end of file +[Human(User) Workflow](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample08) + +[Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) diff --git a/docs/workflow-middleware.md b/docs/workflow-middleware.md new file mode 100644 index 000000000..488d0c7ad --- /dev/null +++ b/docs/workflow-middleware.md @@ -0,0 +1,279 @@ +# Workflow Middleware + +Workflows can be extended with Middleware that run before/after workflows start/complete as well as around workflow steps to provide flexibility in implementing cross-cutting concerns such as [log correlation](https://www.frakkingsweet.com/net-core-log-correlation-easy-access-to-headers/), [retries](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly), and other use-cases. + +This is done by implementing and registering `IWorkflowMiddleware` for workflows or `IWorkflowStepMiddleware` for steps. + +## Step Middleware + +Step middleware lets you run additional code around the execution of a given step and alter its behavior. Implementing a step middleware should look familiar to anyone familiar with [ASP.NET Core's middleware pipeline](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) or [`HttpClient`'s `DelegatingHandler` middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#outgoing-request-middleware). + +### Usage + +First, create your own middleware class that implements `IWorkflowStepMiddleware`. Here's an example of a middleware that adds workflow ID and step ID to the log correlation context of every workflow step in your app. + +**Important:** You must make sure to call `next()` as part of your middleware. If you do not do this, your step will never run. + +```cs +public class LogCorrelationStepMiddleware : IWorkflowStepMiddleware +{ + private readonly ILogger _log; + + public LogCorrelationStepMiddleware( + ILogger log) + { + _log = log; + } + + public async Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next) + { + var workflowId = context.Workflow.Id; + var stepId = context.Step.Id; + + // Uses log scope to add a few attributes to the scope + using (_log.BeginScope("{@WorkflowId}", workflowId)) + using (_log.BeginScope("{@StepId}", stepId)) + { + // Calling next ensures step gets executed + return await next(); + } + } +} +``` + +Here's another example of a middleware that uses the [Polly](https://github.com/App-vNext/Polly) dotnet resiliency library to implement retries on workflow steps based off a custom retry policy. + +```cs +public class PollyRetryStepMiddleware : IWorkflowStepMiddleware +{ + private const string StepContextKey = "WorkflowStepContext"; + private const int MaxRetries = 3; + private readonly ILogger _log; + + public PollyRetryMiddleware(ILogger log) + { + _log = log; + } + + // Consult Polly's docs for more information on how to build + // retry policies: + // https://github.com/App-vNext/Polly + public IAsyncPolicy GetRetryPolicy() => + Policy + .Handle() + .RetryAsync( + MaxRetries, + (result, retryCount, context) => + UpdateRetryCount( + result.Exception, + retryCount, + context[StepContextKey] as IStepExecutionContext) + ); + + public async Task HandleAsync( + IStepExecutionContext context, + IStepBody body, + WorkflowStepDelegate next + ) + { + return await GetRetryPolicy().ExecuteAsync( + ctx => next(), + // The step execution context gets passed down so that + // the step is accessible within the retry policy + new Dictionary + { + { StepContextKey, context } + }); + } + + private Task UpdateRetryCount( + Exception exception, + int retryCount, + IStepExecutionContext stepContext) + { + var stepInstance = stepContext.ExecutionPointer; + stepInstance.RetryCount = retryCount; + return Task.CompletedTask; + } +} +``` + +## Pre/Post Workflow Middleware + +Workflow middleware run either before a workflow starts or after a workflow completes and can be used to hook into the workflow lifecycle or alter the workflow itself before it is started. + +### Pre Workflow Middleware + +These middleware get run before the workflow is started and can potentially alter properties on the `WorkflowInstance`. + +The following example illustrates setting the `Description` property on the `WorkflowInstance` using a middleware that interprets the data on the passed workflow. This is useful in cases where you want the description of the workflow to be derived from the data passed to the workflow. + +Note that you use `WorkflowMiddlewarePhase.PreWorkflow` to specify that it runs before the workflow starts. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +// AddDescriptionWorkflowMiddleware.cs +public class AddDescriptionWorkflowMiddleware : IWorkflowMiddleware +{ + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.PreWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next + ) + { + if (workflow.Data is IDescriptiveWorkflowParams descriptiveParams) + { + workflow.Description = descriptiveParams.Description; + } + + return next(); + } +} + +// IDescriptiveWorkflowParams.cs +public interface IDescriptiveWorkflowParams +{ + string Description { get; } +} + +// MyWorkflowParams.cs +public MyWorkflowParams : IDescriptiveWorkflowParams +{ + public string Description => $"Run task '{TaskName}'"; + + public string TaskName { get; set; } +} +``` + +### Exception Handling in Pre Workflow Middleware + +Pre workflow middleware exception handling gets treated differently from post workflow middleware. Since the middleware runs before the workflow starts, any exceptions thrown within a pre workflow middleware will bubble up to the `StartWorkflow` method and it is up to the caller of `StartWorkflow` to handle the exception and act accordingly. + +```cs +public async Task MyMethodThatStartsAWorkflow() +{ + try + { + await host.StartWorkflow("HelloWorld", 1, null); + } + catch(Exception ex) + { + // Handle the exception appropriately + } +} +``` + +### Post Workflow Middleware + +These middleware get run after the workflow has completed and can be used to perform additional actions for all workflows in your app. + +The following example illustrates how you can use a post workflow middleware to print a summary of the workflow to console. + +Note that you use `WorkflowMiddlewarePhase.PostWorkflow` to specify that it runs after the workflow completes. + +**Important:** You should call `next` as part of the workflow middleware to ensure that the next workflow in the chain runs. + +```cs +public class PrintWorkflowSummaryMiddleware : IWorkflowMiddleware +{ + private readonly ILogger _log; + + public PrintWorkflowSummaryMiddleware( + ILogger log + ) + { + _log = log; + } + + public WorkflowMiddlewarePhase Phase => + WorkflowMiddlewarePhase.PostWorkflow; + + public Task HandleAsync( + WorkflowInstance workflow, + WorkflowDelegate next + ) + { + if (!workflow.CompleteTime.HasValue) + { + return next(); + } + + var duration = workflow.CompleteTime.Value - workflow.CreateTime; + _log.LogInformation($@"Workflow {workflow.Description} completed in {duration:g}"); + + foreach (var step in workflow.ExecutionPointers) + { + var stepName = step.StepName; + var stepDuration = (step.EndTime - step.StartTime) ?? TimeSpan.Zero; + _log.LogInformation($" - Step {stepName} completed in {stepDuration:g}"); + } + + return next(); + } +} +``` + +### Exception Handling in Post Workflow Middleware + +Post workflow middleware exception handling gets treated differently from pre workflow middleware. At the time that the workflow completes, your workflow has ran already so an uncaught exception would be difficult to act on. + +By default, if a workflow middleware throws an exception, it will be logged and the workflow will complete as normal. This behavior can be changed, however. + +To override the default post workflow error handling for all workflows in your app, just register a new `IWorkflowMiddlewareErrorHandler` in the dependency injection framework with your custom behavior as follows. + +```cs +// CustomMiddlewareErrorHandler.cs +public class CustomHandler : IWorkflowMiddlewareErrorHandler +{ + public Task HandleAsync(Exception ex) + { + // Handle your error asynchronously + } +} + +// Startup.cs +public void ConfigureServices(IServiceCollection services) +{ + // Other workflow configuration + services.AddWorkflow(); + + // Should go after .AddWorkflow() + services.AddTransient(); +} +``` + +## Registering Middleware + +In order for middleware to take effect, they must be registered with the built-in dependency injection framework using the convenience helpers. + +**Note:** Middleware will be run in the order that they are registered with middleware that are registered earlier running earlier in the chain and finishing later in the chain. For pre/post workflow middleware, all pre middleware will be run before a workflow starts and all post middleware will be run after a workflow completes. + +```cs +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + ... + + // Add workflow middleware + services.AddWorkflowMiddleware(); + services.AddWorkflowMiddleware(); + + // Add step middleware + services.AddWorkflowStepMiddleware(); + services.AddWorkflowStepMiddleware(); + + ... + } +} +``` + +## More Information + +See the [Workflow Middleware](https://github.com/danielgerlag/workflow-core/tree/master/src/samples/WorkflowCore.Sample19) sample for full examples of workflow middleware in action. diff --git a/mkdocs.yml b/mkdocs.yml index 1081d0f05..57ed12c94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,15 +4,16 @@ nav: - Getting started: getting-started.md - External events: external-events.md - Activity workers: activities.md - - Error handing: error-handling.md + - Error handling: error-handling.md - Control structures: control-structures.md - Saga transactions: sagas.md - JSON / YAML Definitions: json-yaml.md - Persistence: persistence.md + - Middleware: workflow-middleware.md - Multi-node clusters: multi-node-clusters.md - ASP.NET Core: using-with-aspnet-core.md - Elasticsearch plugin: elastic-search.md - Test helpers: test-helpers.md - Extensions: extensions.md - Samples: samples.md -theme: readthedocs \ No newline at end of file +theme: readthedocs diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..219e6f838 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,13 @@ + + + https://github.com/danielgerlag/workflow-core + https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md + git + https://github.com/danielgerlag/workflow-core.git + 3.16.0 + 3.16.0.0 + 3.16.0.0 + https://github.com/danielgerlag/workflow-core/raw/master/src/logo.png + 3.16.0 + + diff --git a/src/WorkflowCore.DSL/Interface/ITypeResolver.cs b/src/WorkflowCore.DSL/Interface/ITypeResolver.cs new file mode 100644 index 000000000..e9e54e49b --- /dev/null +++ b/src/WorkflowCore.DSL/Interface/ITypeResolver.cs @@ -0,0 +1,10 @@ +using System; +using System.Linq; + +namespace WorkflowCore.Interface +{ + public interface ITypeResolver + { + Type FindType(string name); + } +} \ No newline at end of file diff --git a/src/WorkflowCore.DSL/Models/DefinitionSource.cs b/src/WorkflowCore.DSL/Models/DefinitionSource.cs index 5d2bf0333..ec23f81c4 100644 --- a/src/WorkflowCore.DSL/Models/DefinitionSource.cs +++ b/src/WorkflowCore.DSL/Models/DefinitionSource.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.DefinitionStorage { diff --git a/src/WorkflowCore.DSL/Models/Envelope.cs b/src/WorkflowCore.DSL/Models/Envelope.cs index 77448f446..7258a6d5d 100644 --- a/src/WorkflowCore.DSL/Models/Envelope.cs +++ b/src/WorkflowCore.DSL/Models/Envelope.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.DefinitionStorage { diff --git a/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs index 1de64c35e..46f0521ca 100644 --- a/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs +++ b/src/WorkflowCore.DSL/Models/v1/DefinitionSourceV1.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.DefinitionStorage.v1 { diff --git a/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs index 94c21e3be..d4d920544 100644 --- a/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs +++ b/src/WorkflowCore.DSL/Models/v1/MappingSourceV1.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.DefinitionStorage.v1 { diff --git a/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs index 82969d765..07480225a 100644 --- a/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs +++ b/src/WorkflowCore.DSL/Models/v1/StepSourceV1.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Dynamic; -using System.Text; namespace WorkflowCore.Models.DefinitionStorage.v1 { public class StepSourceV1 { public string StepType { get; set; } - + public string Id { get; set; } public string Name { get; set; } @@ -30,8 +29,9 @@ public class StepSourceV1 public ExpandoObject Inputs { get; set; } = new ExpandoObject(); public Dictionary Outputs { get; set; } = new Dictionary(); - + public Dictionary SelectNextStep { get; set; } = new Dictionary(); + public bool ProceedOnCancel { get; set; } = false; } } diff --git a/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs b/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs index 113236e91..a4958e6b4 100644 --- a/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore.DSL/ServiceCollectionExtensions.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Services.DefinitionStorage; @@ -12,6 +9,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddWorkflowDSL(this IServiceCollection services) { + services.AddTransient(); services.AddTransient(); return services; } diff --git a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs index 810d6338b..aa22988eb 100644 --- a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs +++ b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs @@ -1,17 +1,15 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; using System.Reflection; -using System.Text; +using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Primitives; -using WorkflowCore.Models.DefinitionStorage; using WorkflowCore.Models.DefinitionStorage.v1; using WorkflowCore.Exceptions; @@ -20,10 +18,46 @@ namespace WorkflowCore.Services.DefinitionStorage public class DefinitionLoader : IDefinitionLoader { private readonly IWorkflowRegistry _registry; + private readonly ITypeResolver _typeResolver; + + // ParsingConfig to allow access to commonly used .NET methods like object.Equals + private static readonly ParsingConfig ParsingConfig = new ParsingConfig + { + AllowNewToEvaluateAnyType = true, + AreContextKeywordsEnabled = true + }; + + // Transform expressions to be compatible with System.Linq.Dynamic.Core 1.6.0+ + private static string TransformExpression(string expression) + { + if (string.IsNullOrEmpty(expression)) + return expression; + + // Transform object.Equals(a, b) to Convert.ToBoolean(a) == Convert.ToBoolean(b) + // This is a simple regex replacement for the common pattern + var objectEqualsPattern = @"object\.Equals\s*\(\s*([^,]+)\s*,\s*([^)]+)\s*\)"; + var transformed = Regex.Replace(expression, objectEqualsPattern, + match => + { + var arg1 = match.Groups[1].Value.Trim(); + var arg2 = match.Groups[2].Value.Trim(); + + // If arg2 is a boolean literal, convert arg1 to boolean and compare + if (arg2 == "true" || arg2 == "false") + { + return $"Convert.ToBoolean({arg1}) == {arg2}"; + } + // Otherwise, convert both to strings for comparison + return $"Convert.ToString({arg1}) == Convert.ToString({arg2})"; + }); + + return transformed; + } - public DefinitionLoader(IWorkflowRegistry registry) + public DefinitionLoader(IWorkflowRegistry registry, ITypeResolver typeResolver) { _registry = registry; + _typeResolver = typeResolver; } public WorkflowDefinition LoadDefinition(string source, Func deserializer) @@ -68,8 +102,22 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty var nextStep = stack.Pop(); var stepType = FindType(nextStep.StepType); - var containerType = typeof(WorkflowStep<>).MakeGenericType(stepType); - var targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep); + + WorkflowStep targetStep; + + Type containerType; + if (stepType.GetInterfaces().Contains(typeof(IStepBody))) + { + containerType = typeof(WorkflowStep<>).MakeGenericType(stepType); + + targetStep = (containerType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep); + } + else + { + targetStep = stepType.GetConstructor(new Type[] { }).Invoke(null) as WorkflowStep; + if (targetStep != null) + stepType = targetStep.BodyType; + } if (nextStep.Saga) { @@ -81,7 +129,7 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty { var cancelExprType = typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(dataType, typeof(bool))); var dataParameter = Expression.Parameter(dataType, "data"); - var cancelExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter }, typeof(bool), nextStep.CancelCondition); + var cancelExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter }, typeof(bool), TransformExpression(nextStep.CancelCondition)); targetStep.CancelCondition = cancelExpr; } @@ -90,10 +138,11 @@ private WorkflowStepCollection ConvertSteps(ICollection source, Ty targetStep.ErrorBehavior = nextStep.ErrorBehavior; targetStep.RetryInterval = nextStep.RetryInterval; targetStep.ExternalId = $"{nextStep.Id}"; + targetStep.ProceedOnCancel = nextStep.ProceedOnCancel; AttachInputs(nextStep, dataType, stepType, targetStep); AttachOutputs(nextStep, dataType, stepType, targetStep); - + if (nextStep.Do != null) { foreach (var branch in nextStep.Do) @@ -203,47 +252,134 @@ private void AttachOutputs(StepSourceV1 source, Type dataType, Type stepType, Wo foreach (var output in source.Outputs) { var stepParameter = Expression.Parameter(stepType, "step"); - var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { stepParameter }, typeof(object), output.Value); + var sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { stepParameter }, typeof(object), TransformExpression(output.Value)); var dataParameter = Expression.Parameter(dataType, "data"); - Expression targetProperty; - // Check if our datatype has a matching property - var propertyInfo = dataType.GetProperty(output.Key); - if (propertyInfo != null) + + if (output.Key.Contains(".") || output.Key.Contains("[")) { - targetProperty = Expression.Property(dataParameter, propertyInfo); - var targetExpr = Expression.Lambda(targetProperty, dataParameter); - step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr)); + AttachNestedOutput(output, step, source, sourceExpr, dataParameter); } else { - // If we did not find a matching property try to find a Indexer with string parameter - propertyInfo = dataType.GetProperty("Item"); - targetProperty = Expression.Property(dataParameter, propertyInfo, Expression.Constant(output.Key)); + AttachDirectlyOutput(output, step, dataType, sourceExpr, dataParameter); + } + } + } + + private void AttachDirectlyOutput(KeyValuePair output, WorkflowStep step, Type dataType, LambdaExpression sourceExpr, ParameterExpression dataParameter) + { + Expression targetProperty; + + // Check if our datatype has a matching property + var propertyInfo = dataType.GetProperty(output.Key); + if (propertyInfo != null) + { + targetProperty = Expression.Property(dataParameter, propertyInfo); + var targetExpr = Expression.Lambda(targetProperty, dataParameter); + step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr)); + } + else + { + // If we did not find a matching property try to find a Indexer with string parameter + propertyInfo = dataType.GetProperty("Item"); + targetProperty = Expression.Property(dataParameter, propertyInfo, Expression.Constant(output.Key)); + + Action acn = (pStep, pData) => + { + object resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); ; + propertyInfo.SetValue(pData, resolvedValue, new object[] { output.Key }); + }; + + step.Outputs.Add(new ActionParameter(acn)); + } + + } + + private void AttachNestedOutput(KeyValuePair output, WorkflowStep step, StepSourceV1 source, LambdaExpression sourceExpr, ParameterExpression dataParameter) + { + PropertyInfo propertyInfo = null; + String[] paths = output.Key.Split('.'); + + Expression targetProperty = dataParameter; + + bool hasAddOutput = false; + + foreach (String propertyName in paths) + { + if (hasAddOutput) + { + throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}"); + } + + if (targetProperty == null) + { + break; + } + + if (propertyName.Contains("[")) + { + String[] items = propertyName.Split('['); + + if (items.Length != 2) + { + throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}"); + } + + items[1] = items[1].Trim().TrimEnd(']').Trim().Trim('"'); + + MemberExpression memberExpression = Expression.Property(targetProperty, items[0]); + + if (memberExpression == null) + { + throw new ArgumentException($"Unknown property for output {output.Key} on {source.Id}"); + } + propertyInfo = ((PropertyInfo)memberExpression.Member).PropertyType.GetProperty("Item"); Action acn = (pStep, pData) => { + var targetExpr = Expression.Lambda(memberExpression, dataParameter); + object data = targetExpr.Compile().DynamicInvoke(pData); object resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); ; - propertyInfo.SetValue(pData, resolvedValue, new object[] { output.Key }); + propertyInfo.SetValue(data, resolvedValue, new object[] { items[1] }); }; step.Outputs.Add(new ActionParameter(acn)); + hasAddOutput = true; + } + else + { + try + { + targetProperty = Expression.Property(targetProperty, propertyName); + } + catch + { + targetProperty = null; + break; + } } } + + if (targetProperty != null && !hasAddOutput) + { + var targetExpr = Expression.Lambda(targetProperty, dataParameter); + step.Outputs.Add(new MemberMapParameter(sourceExpr, targetExpr)); + } } private void AttachOutcomes(StepSourceV1 source, Type dataType, WorkflowStep step) { if (!string.IsNullOrEmpty(source.NextStepId)) - step.Outcomes.Add(new ValueOutcome() { ExternalNextStepId = $"{source.NextStepId}" }); + step.Outcomes.Add(new ValueOutcome { ExternalNextStepId = $"{source.NextStepId}" }); var dataParameter = Expression.Parameter(dataType, "data"); var outcomeParameter = Expression.Parameter(typeof(object), "outcome"); foreach (var nextStep in source.SelectNextStep) - { - var sourceDelegate = DynamicExpressionParser.ParseLambda(new[] { dataParameter, outcomeParameter }, typeof(object), nextStep.Value).Compile(); + { + var sourceDelegate = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, outcomeParameter }, typeof(object), TransformExpression(nextStep.Value)).Compile(); Expression> sourceExpr = (data, outcome) => System.Convert.ToBoolean(sourceDelegate.DynamicInvoke(data, outcome)); step.Outcomes.Add(new ExpressionOutcome(sourceExpr) { @@ -254,13 +390,13 @@ private void AttachOutcomes(StepSourceV1 source, Type dataType, WorkflowStep ste private Type FindType(string name) { - return Type.GetType(name, true, true); + return _typeResolver.FindType(name); } 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); + var sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), TransformExpression(expr)); void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) { @@ -293,7 +429,7 @@ void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) { if (prop.Name.StartsWith("@")) { - var sourceExpr = DynamicExpressionParser.ParseLambda(new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), prop.Value.ToString()); + var sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), TransformExpression(prop.Value.ToString())); object resolvedValue = sourceExpr.Compile().DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); subobj.Remove(prop.Name); subobj.Add(prop.Name.TrimStart('@'), JToken.FromObject(resolvedValue)); diff --git a/src/WorkflowCore.DSL/Services/TypeResolver.cs b/src/WorkflowCore.DSL/Services/TypeResolver.cs new file mode 100644 index 000000000..992d38948 --- /dev/null +++ b/src/WorkflowCore.DSL/Services/TypeResolver.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WorkflowCore.Interface; + +namespace WorkflowCore.Services.DefinitionStorage +{ + public class TypeResolver : ITypeResolver + { + public Type FindType(string name) + { + return Type.GetType(name, true, true); + } + } +} diff --git a/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj b/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj index 913c643f4..9385da3d0 100644 --- a/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj +++ b/src/WorkflowCore.DSL/WorkflowCore.DSL.csproj @@ -1,20 +1,17 @@ - + netstandard2.0 - 3.1.5 DSL extenstion for Workflow Core provding support for JSON and YAML workflow definitions. Daniel Gerlag WorkflowCore - https://github.com/danielgerlag/workflow-core - https://github.com/danielgerlag/workflow-core.git - git - + + diff --git a/test/WorkflowCore.Testing/JsonWorkflowTest.cs b/src/WorkflowCore.Testing/JsonWorkflowTest.cs similarity index 95% rename from test/WorkflowCore.Testing/JsonWorkflowTest.cs rename to src/WorkflowCore.Testing/JsonWorkflowTest.cs index 16fd7c232..4e8f4281b 100644 --- a/test/WorkflowCore.Testing/JsonWorkflowTest.cs +++ b/src/WorkflowCore.Testing/JsonWorkflowTest.cs @@ -1,8 +1,6 @@ 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; @@ -17,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() @@ -34,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(); @@ -41,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, @@ -106,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 86% rename from test/WorkflowCore.Testing/WorkflowCore.Testing.csproj rename to src/WorkflowCore.Testing/WorkflowCore.Testing.csproj index 32f9c1363..6102c9892 100644 --- a/test/WorkflowCore.Testing/WorkflowCore.Testing.csproj +++ b/src/WorkflowCore.Testing/WorkflowCore.Testing.csproj @@ -2,9 +2,9 @@ netstandard2.0 - 2.3.0 - 2.3.0.0 - 2.3.0.0 + 3.5.2 + 3.5.2.0 + 3.5.2.0 Facilitates testing of workflows built on Workflow-Core diff --git a/test/WorkflowCore.Testing/WorkflowTest.cs b/src/WorkflowCore.Testing/WorkflowTest.cs similarity index 79% rename from test/WorkflowCore.Testing/WorkflowTest.cs rename to src/WorkflowCore.Testing/WorkflowTest.cs index 1a0dc8d81..bf0eb97ab 100644 --- a/test/WorkflowCore.Testing/WorkflowTest.cs +++ b/src/WorkflowCore.Testing/WorkflowTest.cs @@ -1,9 +1,8 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; @@ -18,7 +17,7 @@ public abstract class WorkflowTest : IDisposable protected IWorkflowHost Host; protected IPersistenceProvider PersistenceProvider; protected List UnhandledStepErrors = new List(); - + protected virtual void Setup() { //setup dependency injection @@ -28,10 +27,6 @@ protected virtual void Setup() var serviceProvider = services.BuildServiceProvider(); - //config logging - var loggerFactory = serviceProvider.GetService(); - //loggerFactory.AddConsole(LogLevel.Debug); - PersistenceProvider = serviceProvider.GetService(); Host = serviceProvider.GetService(); Host.RegisterWorkflow(); @@ -41,7 +36,7 @@ protected virtual void Setup() protected void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) { - UnhandledStepErrors.Add(new StepError() + UnhandledStepErrors.Add(new StepError { Exception = exception, Step = step, @@ -51,7 +46,7 @@ protected void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Ex protected virtual void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(); + services.AddWorkflow(options => options.UsePollInterval(TimeSpan.FromSeconds(3))); } public string StartWorkflow(TData data) @@ -61,6 +56,13 @@ public string StartWorkflow(TData data) 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); @@ -73,6 +75,20 @@ protected void WaitForWorkflowToComplete(string workflowId, TimeSpan timeOut) } } + 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; diff --git a/test/WorkflowCore.Testing/YamlWorkflowTest.cs b/src/WorkflowCore.Testing/YamlWorkflowTest.cs similarity index 95% rename from test/WorkflowCore.Testing/YamlWorkflowTest.cs rename to src/WorkflowCore.Testing/YamlWorkflowTest.cs index 58cbb3cad..11b8e9d93 100644 --- a/test/WorkflowCore.Testing/YamlWorkflowTest.cs +++ b/src/WorkflowCore.Testing/YamlWorkflowTest.cs @@ -1,8 +1,6 @@ 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; @@ -17,6 +15,7 @@ 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() @@ -34,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(); @@ -41,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, @@ -106,5 +106,4 @@ public void Dispose() Host.Stop(); } } - } 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/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/Interface/IActivityController.cs b/src/WorkflowCore/Interface/IActivityController.cs index 99e5c1737..46464e8e8 100644 --- a/src/WorkflowCore/Interface/IActivityController.cs +++ b/src/WorkflowCore/Interface/IActivityController.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace WorkflowCore.Interface diff --git a/src/WorkflowCore/Interface/ICancellationProcessor.cs b/src/WorkflowCore/Interface/ICancellationProcessor.cs index 09d37969b..f6cbb31fa 100644 --- a/src/WorkflowCore/Interface/ICancellationProcessor.cs +++ b/src/WorkflowCore/Interface/ICancellationProcessor.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Models; namespace WorkflowCore.Interface 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/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 index 70663effc..6ce3fb177 100644 --- a/src/WorkflowCore/Interface/ILifeCycleEventHub.cs +++ b/src/WorkflowCore/Interface/ILifeCycleEventHub.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using WorkflowCore.Models.LifeCycleEvents; diff --git a/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs b/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs index 273c26456..db703659a 100644 --- a/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs +++ b/src/WorkflowCore/Interface/ILifeCycleEventPublisher.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Interface diff --git a/src/WorkflowCore/Interface/IScopeProvider.cs b/src/WorkflowCore/Interface/IScopeProvider.cs index c69351731..bab5f94d2 100644 --- a/src/WorkflowCore/Interface/IScopeProvider.cs +++ b/src/WorkflowCore/Interface/IScopeProvider.cs @@ -12,6 +12,6 @@ public interface IScopeProvider /// Create a new service scope /// /// - IServiceScope CreateScope(); + IServiceScope CreateScope(IStepExecutionContext context); } } \ No newline at end of file diff --git a/src/WorkflowCore/Interface/ISearchIndex.cs b/src/WorkflowCore/Interface/ISearchIndex.cs index 64bd075f6..0aea590a8 100644 --- a/src/WorkflowCore/Interface/ISearchIndex.cs +++ b/src/WorkflowCore/Interface/ISearchIndex.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using WorkflowCore.Models; using WorkflowCore.Models.Search; diff --git a/src/WorkflowCore/Interface/ISearchable.cs b/src/WorkflowCore/Interface/ISearchable.cs index 9ceec14e2..e769f916c 100644 --- a/src/WorkflowCore/Interface/ISearchable.cs +++ b/src/WorkflowCore/Interface/ISearchable.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Interface { diff --git a/src/WorkflowCore/Interface/IStepBuilder.cs b/src/WorkflowCore/Interface/IStepBuilder.cs index db28b5887..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 { @@ -28,36 +26,6 @@ public interface IStepBuilder /// IStepBuilder Id(string id); - /// - /// 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); - /// /// Specify the next step in the workflow by Id /// @@ -129,26 +97,6 @@ public interface IStepBuilder /// 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); - - /// - /// 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; /// @@ -165,76 +113,6 @@ public interface IStepBuilder /// IStepBuilder EndWorkflow(); - /// - /// 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); - - /// - /// 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 /// @@ -270,16 +148,5 @@ public interface IStepBuilder /// /// IStepBuilder CancelCondition(Expression> cancelCondition, bool proceedAfterCancel = false); - - /// - /// 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); - } } 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/IStepParameter.cs b/src/WorkflowCore/Interface/IStepParameter.cs index 049ecdb25..5bbbb2fbf 100644 --- a/src/WorkflowCore/Interface/IStepParameter.cs +++ b/src/WorkflowCore/Interface/IStepParameter.cs @@ -1,7 +1,4 @@ -using WorkflowCore.Interface; -using WorkflowCore.Models; - -namespace WorkflowCore.Interface +namespace WorkflowCore.Interface { public interface IStepParameter { diff --git a/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs b/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs index 2fe69771b..051b59e91 100644 --- a/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs +++ b/src/WorkflowCore/Interface/ISyncWorkflowRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Models; @@ -8,5 +9,8 @@ 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 404a359f3..b7b371882 100644 --- a/src/WorkflowCore/Interface/IWorkflowBuilder.cs +++ b/src/WorkflowCore/Interface/IWorkflowBuilder.cs @@ -9,7 +9,7 @@ public interface IWorkflowBuilder { List Steps { get; } - int LastStep { get; } + int LastStep { get; } IWorkflowBuilder UseData(); @@ -20,8 +20,8 @@ public interface IWorkflowBuilder 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); @@ -33,6 +33,5 @@ public interface IWorkflowBuilder : IWorkflowBuilder 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 757351351..924925d2c 100644 --- a/src/WorkflowCore/Interface/IWorkflowController.cs +++ b/src/WorkflowCore/Interface/IWorkflowController.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace WorkflowCore.Interface @@ -13,8 +11,8 @@ public interface IWorkflowController 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 index 479176627..fa0734118 100644 --- a/src/WorkflowCore/Interface/IWorkflowErrorHandler.cs +++ b/src/WorkflowCore/Interface/IWorkflowErrorHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Models; namespace WorkflowCore.Interface 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/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 index a85d9a0bf..42f6ba85f 100644 --- a/src/WorkflowCore/Interface/IWorkflowPurger.cs +++ b/src/WorkflowCore/Interface/IWorkflowPurger.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Models; @@ -6,6 +7,6 @@ namespace WorkflowCore.Interface { public interface IWorkflowPurger { - Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan); + 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 6f798b8e7..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 { @@ -10,5 +11,6 @@ public interface IWorkflowRegistry 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 index 63abdd43c..75651c24b 100644 --- a/src/WorkflowCore/Interface/Persistence/IEventRepository.cs +++ b/src/WorkflowCore/Interface/Persistence/IEventRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Models; @@ -7,17 +8,17 @@ namespace WorkflowCore.Interface { public interface IEventRepository { - Task CreateEvent(Event newEvent); + Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default); - Task GetEvent(string id); + Task GetEvent(string id, CancellationToken cancellationToken = default); - Task> GetRunnableEvents(DateTime asAt); + Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default); - Task> GetEvents(string eventName, string eventKey, DateTime asOf); + Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); - Task MarkEventProcessed(string id); + Task MarkEventProcessed(string id, CancellationToken cancellationToken = default); - Task MarkEventUnprocessed(string id); + Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default); } } diff --git a/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs b/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs index 165dc7cbb..4b83f5920 100644 --- a/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs +++ b/src/WorkflowCore/Interface/Persistence/IPersistenceProvider.cs @@ -1,14 +1,15 @@ 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 + public interface IPersistenceProvider : IWorkflowRepository, ISubscriptionRepository, IEventRepository, IScheduledCommandRepository { - Task PersistErrors(IEnumerable errors); + 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 index c37cf2f32..2f22a45f4 100644 --- a/src/WorkflowCore/Interface/Persistence/ISubscriptionRepository.cs +++ b/src/WorkflowCore/Interface/Persistence/ISubscriptionRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Models; @@ -7,19 +8,19 @@ namespace WorkflowCore.Interface { public interface ISubscriptionRepository { - Task CreateEventSubscription(EventSubscription subscription); + Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default); - Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf); + Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); - Task TerminateSubscription(string eventSubscriptionId); + Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default); - Task GetSubscription(string eventSubscriptionId); + Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default); - Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf); + Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default); - Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry); + Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default); - Task ClearSubscriptionToken(string eventSubscriptionId, string token); + 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 index 333423561..09842af7a 100644 --- a/src/WorkflowCore/Interface/Persistence/IWorkflowRepository.cs +++ b/src/WorkflowCore/Interface/Persistence/IWorkflowRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Models; @@ -7,18 +8,20 @@ namespace WorkflowCore.Interface { public interface IWorkflowRepository { - Task CreateNewWorkflow(WorkflowInstance workflow); + Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default); - Task PersistWorkflow(WorkflowInstance workflow); + Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default); - Task> GetRunnableInstances(DateTime asAt); + 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); + Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default); - Task> GetWorkflowInstances(IEnumerable ids); + Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default); } } diff --git a/src/WorkflowCore/Models/ActionParameter.cs b/src/WorkflowCore/Models/ActionParameter.cs index f6a3e4723..762a865e8 100644 --- a/src/WorkflowCore/Models/ActionParameter.cs +++ b/src/WorkflowCore/Models/ActionParameter.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; using WorkflowCore.Interface; namespace WorkflowCore.Models diff --git a/src/WorkflowCore/Models/ActivityResult.cs b/src/WorkflowCore/Models/ActivityResult.cs index 212ca92d8..f3a828a08 100644 --- a/src/WorkflowCore/Models/ActivityResult.cs +++ b/src/WorkflowCore/Models/ActivityResult.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models { diff --git a/src/WorkflowCore/Models/ExecutionPointer.cs b/src/WorkflowCore/Models/ExecutionPointer.cs index 53938d76a..6c12afda7 100644 --- a/src/WorkflowCore/Models/ExecutionPointer.cs +++ b/src/WorkflowCore/Models/ExecutionPointer.cs @@ -63,6 +63,7 @@ public enum PointerStatus WaitingForEvent = 5, Failed = 6, Compensated = 7, - Cancelled = 8 + Cancelled = 8, + PendingPredecessor = 9 } } diff --git a/src/WorkflowCore/Models/ExecutionPointerCollection.cs b/src/WorkflowCore/Models/ExecutionPointerCollection.cs index a81e9fcf8..f125db5a8 100644 --- a/src/WorkflowCore/Models/ExecutionPointerCollection.cs +++ b/src/WorkflowCore/Models/ExecutionPointerCollection.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; namespace WorkflowCore.Models { @@ -97,6 +96,12 @@ 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 54c2c55f2..14c562802 100644 --- a/src/WorkflowCore/Models/ExecutionResult.cs +++ b/src/WorkflowCore/Models/ExecutionResult.cs @@ -35,7 +35,7 @@ public ExecutionResult(object outcome) public static ExecutionResult Outcome(object value) { - return new ExecutionResult() + return new ExecutionResult { Proceed = true, OutcomeValue = value @@ -44,7 +44,7 @@ public static ExecutionResult Outcome(object value) public static ExecutionResult Next() { - return new ExecutionResult() + return new ExecutionResult { Proceed = true, OutcomeValue = null @@ -53,7 +53,7 @@ public static ExecutionResult Next() public static ExecutionResult Persist(object persistenceData) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, PersistenceData = persistenceData @@ -62,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, @@ -72,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, @@ -82,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, @@ -93,7 +93,7 @@ public static ExecutionResult WaitForEvent(string eventName, string eventKey, Da public static ExecutionResult WaitForActivity(string activityName, object subscriptionData, DateTime effectiveDate) { - return new ExecutionResult() + return new ExecutionResult { Proceed = false, EventName = Event.EventTypeActivity, 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 index afab488b0..f72283e9a 100755 --- a/src/WorkflowCore/Models/LifeCycleEvents/LifeCycleEvent.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/LifeCycleEvent.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs b/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs index 38a68697d..3d44be288 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/StepCompleted.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs b/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs index a76c50e0f..183dd1a7b 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/StepStarted.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs index 4b3d8a2e2..e83da1827 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowCompleted.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs index 55f273d63..e24849fcf 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowError.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs index 88f2ee603..1c400b84d 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowResumed.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs index c461ee8aa..60b906de8 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowStarted.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs index 39d19a7a8..ccadff82d 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowSuspended.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs index 07ff8f75f..778c23493 100644 --- a/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs +++ b/src/WorkflowCore/Models/LifeCycleEvents/WorkflowTerminated.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.LifeCycleEvents { diff --git a/src/WorkflowCore/Models/MemberMapParameter.cs b/src/WorkflowCore/Models/MemberMapParameter.cs index 052f0bc1a..e5273986d 100644 --- a/src/WorkflowCore/Models/MemberMapParameter.cs +++ b/src/WorkflowCore/Models/MemberMapParameter.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using WorkflowCore.Interface; namespace WorkflowCore.Models 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 index 63658fd37..412688a28 100644 --- a/src/WorkflowCore/Models/Search/Page.cs +++ b/src/WorkflowCore/Models/Search/Page.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.Search { diff --git a/src/WorkflowCore/Models/Search/SearchFilter.cs b/src/WorkflowCore/Models/Search/SearchFilter.cs index fd7d8c7a4..e7491806c 100644 --- a/src/WorkflowCore/Models/Search/SearchFilter.cs +++ b/src/WorkflowCore/Models/Search/SearchFilter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq.Expressions; -using System.Text; namespace WorkflowCore.Models.Search { @@ -21,13 +19,13 @@ public class ScalarFilter : SearchFilter { public object Value { get; set; } - public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter() + public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter { Property = property, Value = value }; - public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter() + public static SearchFilter Equals(Expression> property, object value) => new ScalarFilter { IsData = true, DataType = typeof(T), @@ -41,26 +39,26 @@ public class DateRangeFilter : SearchFilter public DateTime? BeforeValue { get; set; } public DateTime? AfterValue { get; set; } - public static DateRangeFilter Before(Expression> property, DateTime value) => new DateRangeFilter() + public static DateRangeFilter Before(Expression> property, DateTime value) => new DateRangeFilter { Property = property, BeforeValue = value }; - public static DateRangeFilter After(Expression> property, DateTime value) => new DateRangeFilter() + 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() + 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() + public static DateRangeFilter Before(Expression> property, DateTime value) => new DateRangeFilter { IsData = true, DataType = typeof(T), @@ -68,7 +66,7 @@ public class DateRangeFilter : SearchFilter BeforeValue = value }; - public static DateRangeFilter After(Expression> property, DateTime value) => new DateRangeFilter() + public static DateRangeFilter After(Expression> property, DateTime value) => new DateRangeFilter { IsData = true, DataType = typeof(T), @@ -76,7 +74,7 @@ public class DateRangeFilter : SearchFilter AfterValue = value }; - public static DateRangeFilter Between(Expression> property, DateTime start, DateTime end) => new DateRangeFilter() + public static DateRangeFilter Between(Expression> property, DateTime start, DateTime end) => new DateRangeFilter { IsData = true, DataType = typeof(T), @@ -91,26 +89,26 @@ public class NumericRangeFilter : SearchFilter public double? LessValue { get; set; } public double? GreaterValue { get; set; } - public static NumericRangeFilter LessThan(Expression> property, double value) => new NumericRangeFilter() + public static NumericRangeFilter LessThan(Expression> property, double value) => new NumericRangeFilter { Property = property, LessValue = value }; - public static NumericRangeFilter GreaterThan(Expression> property, double value) => new NumericRangeFilter() + 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() + 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() + public static NumericRangeFilter LessThan(Expression> property, double value) => new NumericRangeFilter { IsData = true, DataType = typeof(T), @@ -118,7 +116,7 @@ public class NumericRangeFilter : SearchFilter LessValue = value }; - public static NumericRangeFilter GreaterThan(Expression> property, double value) => new NumericRangeFilter() + public static NumericRangeFilter GreaterThan(Expression> property, double value) => new NumericRangeFilter { IsData = true, DataType = typeof(T), @@ -126,7 +124,7 @@ public class NumericRangeFilter : SearchFilter GreaterValue = value }; - public static NumericRangeFilter Between(Expression> property, double start, double end) => new NumericRangeFilter() + public static NumericRangeFilter Between(Expression> property, double start, double end) => new NumericRangeFilter { IsData = true, DataType = typeof(T), @@ -144,7 +142,7 @@ protected StatusFilter() Property = lambda; } - public static StatusFilter Equals(WorkflowStatus value) => new StatusFilter() + 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 index b17ccedd0..6b9a97d8f 100644 --- a/src/WorkflowCore/Models/Search/StepInfo.cs +++ b/src/WorkflowCore/Models/Search/StepInfo.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Models.Search { diff --git a/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs b/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs index 6870d36ab..1cb6a3c9a 100644 --- a/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs +++ b/src/WorkflowCore/Models/Search/WorkflowSearchResult.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; -using WorkflowCore.Interface; namespace WorkflowCore.Models.Search { 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/WorkflowDefinition.cs b/src/WorkflowCore/Models/WorkflowDefinition.cs index f39a563ed..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 @@ -18,14 +17,17 @@ public class WorkflowDefinition 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 b75b284fc..71f1e3c28 100644 --- a/src/WorkflowCore/Models/WorkflowInstance.cs +++ b/src/WorkflowCore/Models/WorkflowInstance.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; namespace WorkflowCore.Models @@ -36,11 +35,11 @@ public bool IsBranchComplete(string parentId) } } - 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 635223af5..a4432afff 100644 --- a/src/WorkflowCore/Models/WorkflowOptions.cs +++ b/src/WorkflowCore/Models/WorkflowOptions.cs @@ -8,7 +8,7 @@ namespace WorkflowCore.Models { public class WorkflowOptions { - internal Func PersistanceFactory; + internal Func PersistenceFactory; internal Func QueueFactory; internal Func LockFactory; internal Func EventHubFactory; @@ -16,7 +16,7 @@ public class WorkflowOptions internal TimeSpan PollInterval; internal TimeSpan IdleTime; internal TimeSpan ErrorRetryInterval; - internal int MaxConcurrentWorkflows = Math.Max(Environment.ProcessorCount, 2); + internal int MaxConcurrentWorkflows = Math.Max(Environment.ProcessorCount, 4); public IServiceCollection Services { get; private set; } @@ -29,14 +29,20 @@ public WorkflowOptions(IServiceCollection services) QueueFactory = new Func(sp => new SingleNodeQueueProvider()); LockFactory = new Func(sp => new SingleNodeLockProvider()); - PersistanceFactory = new Func(sp => new TransientMemoryPersistenceProvider(sp.GetService())); + PersistenceFactory = 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; + PersistenceFactory = factory; } public void UseDistributedLockManager(Func factory) @@ -69,6 +75,11 @@ 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 08ad1c525..814219916 100644 --- a/src/WorkflowCore/Models/WorkflowStep.cs +++ b/src/WorkflowCore/Models/WorkflowStep.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using System.Reflection; using WorkflowCore.Interface; namespace WorkflowCore.Models diff --git a/src/WorkflowCore/Models/WorkflowStepCollection.cs b/src/WorkflowCore/Models/WorkflowStepCollection.cs index 90c2e4710..68346620e 100644 --- a/src/WorkflowCore/Models/WorkflowStepCollection.cs +++ b/src/WorkflowCore/Models/WorkflowStepCollection.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; namespace WorkflowCore.Models { 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/ContainerStepBody.cs b/src/WorkflowCore/Primitives/ContainerStepBody.cs index 30ad9b644..46da6a0b0 100644 --- a/src/WorkflowCore/Primitives/ContainerStepBody.cs +++ b/src/WorkflowCore/Primitives/ContainerStepBody.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Collections.Generic; using WorkflowCore.Models; namespace WorkflowCore.Primitives diff --git a/src/WorkflowCore/Primitives/Foreach.cs b/src/WorkflowCore/Primitives/Foreach.cs index 23cecc947..ef4c39fe0 100644 --- a/src/WorkflowCore/Primitives/Foreach.cs +++ b/src/WorkflowCore/Primitives/Foreach.cs @@ -8,28 +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)) { - if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) + if (!RunParallel) { - return ExecutionResult.Next(); + var values = Collection.Cast(); + persistenceData.Index++; + if (persistenceData.Index < values.Count()) + { + return ExecutionResult.Branch(new List(new object[] { values.ElementAt(persistenceData.Index) }), persistenceData); + } } + + 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 2aaaad963..b3950bf7c 100644 --- a/src/WorkflowCore/Primitives/If.cs +++ b/src/WorkflowCore/Primitives/If.cs @@ -15,7 +15,7 @@ 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(); diff --git a/src/WorkflowCore/Primitives/OutcomeSwitch.cs b/src/WorkflowCore/Primitives/OutcomeSwitch.cs index 74c2cf1de..53cdc4729 100644 --- a/src/WorkflowCore/Primitives/OutcomeSwitch.cs +++ b/src/WorkflowCore/Primitives/OutcomeSwitch.cs @@ -12,7 +12,7 @@ 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; } 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 9c74f2a97..7eb63c8c1 100644 --- a/src/WorkflowCore/Primitives/Schedule.cs +++ b/src/WorkflowCore/Primitives/Schedule.cs @@ -13,14 +13,14 @@ 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 }); } if (context.Workflow.IsBranchComplete(context.ExecutionPointer.Id)) diff --git a/src/WorkflowCore/Primitives/Sequence.cs b/src/WorkflowCore/Primitives/Sequence.cs index 20140af47..d3e0b827d 100644 --- a/src/WorkflowCore/Primitives/Sequence.cs +++ b/src/WorkflowCore/Primitives/Sequence.cs @@ -11,7 +11,7 @@ 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)) diff --git a/src/WorkflowCore/Primitives/When.cs b/src/WorkflowCore/Primitives/When.cs index ca9434f66..a2ab32252 100644 --- a/src/WorkflowCore/Primitives/When.cs +++ b/src/WorkflowCore/Primitives/When.cs @@ -25,7 +25,7 @@ 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)) diff --git a/src/WorkflowCore/Primitives/While.cs b/src/WorkflowCore/Primitives/While.cs index 154d7cf87..7716ed0b0 100644 --- a/src/WorkflowCore/Primitives/While.cs +++ b/src/WorkflowCore/Primitives/While.cs @@ -15,7 +15,7 @@ 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(); 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 c241107c2..f38f44a5b 100644 --- a/src/WorkflowCore/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore/ServiceCollectionExtensions.cs @@ -1,12 +1,8 @@ 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; @@ -24,10 +20,10 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A 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.AddTransient(options.PersistenceFactory); + services.AddTransient(options.PersistenceFactory); + services.AddTransient(options.PersistenceFactory); + services.AddTransient(options.PersistenceFactory); services.AddSingleton(options.QueueFactory); services.AddSingleton(options.LockFactory); services.AddSingleton(options.EventHubFactory); @@ -35,12 +31,28 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A services.AddSingleton(); services.AddSingleton(options); - services.AddSingleton(); + 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(); services.AddTransient(sp => sp.GetService()); services.AddTransient(); @@ -48,9 +60,13 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A services.AddTransient(); services.AddTransient(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -68,6 +84,40 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A 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 index e8d1d727b..491f9c47e 100644 --- a/src/WorkflowCore/Services/ActivityController.cs +++ b/src/WorkflowCore/Services/ActivityController.cs @@ -1,5 +1,4 @@ using System; -using System.Dynamic; using System.Linq; using System.Text; using System.Threading; @@ -28,14 +27,14 @@ public ActivityController(ISubscriptionRepository subscriptionRepository, IWorkf public async Task GetPendingActivity(string activityName, string workerId, TimeSpan? timeout = null) { - var endTime = DateTime.UtcNow.Add(timeout ?? TimeSpan.Zero); + var endTime = _dateTimeProvider.UtcNow.Add(timeout ?? TimeSpan.Zero); var firstPass = true; EventSubscription subscription = null; - while ((subscription == null && DateTime.UtcNow < endTime) || firstPass) + while ((subscription == null && _dateTimeProvider.UtcNow < endTime) || firstPass) { if (!firstPass) await Task.Delay(100); - subscription = await _subscriptionRepository.GetFirstOpenSubscription(Event.EventTypeActivity, activityName, _dateTimeProvider.Now); + subscription = await _subscriptionRepository.GetFirstOpenSubscription(Event.EventTypeActivity, activityName, _dateTimeProvider.UtcNow); if (subscription != null) if (!await _lockProvider.AcquireLock($"sub:{subscription.Id}", CancellationToken.None)) subscription = null; @@ -47,12 +46,12 @@ public async Task GetPendingActivity(string activityName, strin try { var token = Token.Create(subscription.Id, subscription.EventKey); - var result = new PendingActivity() + var result = new PendingActivity { Token = token.Encode(), ActivityName = subscription.EventKey, Parameters = subscription.SubscriptionData, - TokenExpiry = DateTime.MaxValue + TokenExpiry = new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc) }; if (!await _subscriptionRepository.SetSubscriptionToken(subscription.Id, result.Token, workerId, result.TokenExpiry)) @@ -75,7 +74,7 @@ public async Task ReleaseActivityToken(string token) public async Task SubmitActivitySuccess(string token, object result) { - await SubmitActivityResult(token, new ActivityResult() + await SubmitActivityResult(token, new ActivityResult { Data = result, Status = ActivityResult.StatusType.Success @@ -84,7 +83,7 @@ public async Task SubmitActivitySuccess(string token, object result) public async Task SubmitActivityFailure(string token, object result) { - await SubmitActivityResult(token, new ActivityResult() + await SubmitActivityResult(token, new ActivityResult { Data = result, Status = ActivityResult.StatusType.Fail @@ -120,7 +119,7 @@ public string Encode() public static Token Create(string subscriptionId, string activityName) { - return new Token() + return new Token { SubscriptionId = subscriptionId, ActivityName = activityName, diff --git a/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs index 06682431a..24a631513 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/EventConsumer.cs @@ -16,13 +16,15 @@ internal class EventConsumer : QueueConsumer, IBackgroundTask private readonly IEventRepository _eventRepository; private readonly IDistributedLockProvider _lockProvider; private readonly IDateTimeProvider _datetimeProvider; - protected override int MaxConcurrentItems => 2; + private readonly IGreyList _greylist; + protected override QueueType Queue => QueueType.Event; - public EventConsumer(IWorkflowRepository workflowRepository, ISubscriptionRepository subscriptionRepository, IEventRepository eventRepository, 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) { _workflowRepository = workflowRepository; + _greylist = greylist; _subscriptionRepository = subscriptionRepository; _eventRepository = eventRepository; _lockProvider = lockProvider; @@ -40,34 +42,47 @@ protected override async Task ProcessItem(string itemId, CancellationToken cance try { cancellationToken.ThrowIfCancellationRequested(); - var evt = await _eventRepository.GetEvent(itemId); + var evt = await _eventRepository.GetEvent(itemId, cancellationToken); + + WorkflowActivity.Enrich(evt); + if (evt.IsProcessed) + { + _greylist.Add($"evt:{evt.Id}"); + return; + } if (evt.EventTime <= _datetimeProvider.UtcNow) { IEnumerable subs = null; if (evt.EventData is ActivityResult) { - var activity = await _subscriptionRepository.GetSubscription((evt.EventData as ActivityResult).SubscriptionId); + var activity = await _subscriptionRepository.GetSubscription((evt.EventData as ActivityResult).SubscriptionId, cancellationToken); if (activity == null) { Logger.LogWarning($"Activity already processed - {(evt.EventData as ActivityResult).SubscriptionId}"); - await _eventRepository.MarkEventProcessed(itemId); + await _eventRepository.MarkEventProcessed(itemId, cancellationToken); return; } - subs = new List() { activity }; + subs = new List { activity }; } else { - subs = await _subscriptionRepository.GetSubscriptions(evt.EventName, evt.EventKey, evt.EventTime); + subs = await _subscriptionRepository.GetSubscriptions(evt.EventName, evt.EventKey, evt.EventTime, cancellationToken); } - var toQueue = new List(); + 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); + { + await _eventRepository.MarkEventProcessed(itemId, cancellationToken); + } + else + { + _greylist.Remove($"evt:{evt.Id}"); + } foreach (var eventId in toQueue) await QueueProvider.QueueWork(eventId, QueueType.Event); @@ -79,14 +94,14 @@ protected override async Task ProcessItem(string itemId, CancellationToken cance } } - private async Task SeedSubscription(Event evt, EventSubscription sub, List toQueue, CancellationToken cancellationToken) - { - foreach (var eventId in await _eventRepository.GetEvents(sub.EventName, sub.EventKey, sub.SubscribeAsOf)) + private async Task SeedSubscription(Event evt, EventSubscription sub, HashSet toQueue, CancellationToken cancellationToken) + { + foreach (var eventId in await _eventRepository.GetEvents(sub.EventName, sub.EventKey, sub.SubscribeAsOf, cancellationToken)) { if (eventId == evt.Id) continue; - var siblingEvent = await _eventRepository.GetEvent(eventId); + var siblingEvent = await _eventRepository.GetEvent(eventId, cancellationToken); if ((!siblingEvent.IsProcessed) && (siblingEvent.EventTime < evt.EventTime)) { await QueueProvider.QueueWork(eventId, QueueType.Event); @@ -99,13 +114,13 @@ private async Task SeedSubscription(Event evt, EventSubscription sub, List if (!await _lockProvider.AcquireLock(sub.WorkflowId, cancellationToken)) { - Logger.LogInformation("Workflow locked {0}", sub.WorkflowId); + Logger.LogInformation("Workflow locked {WorkflowId}", sub.WorkflowId); return false; } try { - var workflow = await _workflowRepository.GetWorkflowInstance(sub.WorkflowId); + var workflow = await _workflowRepository.GetWorkflowInstance(sub.WorkflowId, cancellationToken); IEnumerable pointers = null; if (!string.IsNullOrEmpty(sub.ExecutionPointerId)) @@ -120,8 +135,8 @@ private async Task SeedSubscription(Event evt, EventSubscription sub, List p.Active = true; } workflow.NextExecution = 0; - await _workflowRepository.PersistWorkflow(workflow); - await _subscriptionRepository.TerminateSubscription(sub.Id); + await _workflowRepository.PersistWorkflow(workflow, cancellationToken); + await _subscriptionRepository.TerminateSubscription(sub.Id, cancellationToken); return true; } catch (Exception ex) @@ -136,4 +151,4 @@ private async Task SeedSubscription(Event evt, EventSubscription sub, List } } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs index 10899af10..29565e647 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/IndexConsumer.cs @@ -32,6 +32,8 @@ protected override async Task ProcessItem(string itemId, CancellationToken cance try { var workflow = await FetchWorkflow(itemId); + + WorkflowActivity.Enrich(workflow, "index"); await _searchIndex.IndexWorkflow(workflow); lock (_errorCounts) { diff --git a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs index 47097bcc8..75a7583ad 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/QueueConsumer.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ConcurrentCollections; using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -20,12 +23,17 @@ internal abstract class QueueConsumer : IBackgroundTask 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); @@ -38,78 +46,75 @@ public virtual void Start() } _cancellationTokenSource = new CancellationTokenSource(); - - DispatchTask = new Task(Execute, TaskCreationOptions.LongRunning); - 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 activeTasks = new Dictionary(); - var secondPasses = new HashSet(); + var cancelToken = _cancellationTokenSource.Token; while (!cancelToken.IsCancellationRequested) { + Activity activity = default; try { - if (activeTasks.Count >= MaxConcurrentItems) + 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 (activeTasks.ContainsKey(item)) + + activity?.EnrichWithDequeuedItem(item); + + var hasTask = false; + lock (_activeTasks) + { + hasTask = _activeTasks.ContainsKey(item); + } + if (hasTask) { - secondPasses.Add(item); + _secondPasses.Add(item); if (!EnableSecondPasses) await QueueProvider.QueueWork(item, Queue); + activity?.Dispose(); continue; } - secondPasses.Remove(item); + _secondPasses.TryRemove(item); - var task = new Task(async (object data) => - { - try - { - await ExecuteItem((string)data); - while (EnableSecondPasses && secondPasses.Contains(item)) - { - secondPasses.Remove(item); - await ExecuteItem((string)data); - } - } - finally - { - lock (activeTasks) - { - activeTasks.Remove((string)data); - } - } - }, item); - lock (activeTasks) + var waitHandle = new ManualResetEvent(false); + lock (_activeTasks) { - activeTasks.Add(item, task); + _activeTasks.Add(item, waitHandle); } - - task.Start(); + var task = ExecuteItem(item, waitHandle, activity); } catch (OperationCanceledException) { @@ -117,18 +122,34 @@ private async void Execute() catch (Exception ex) { Logger.LogError(ex, ex.Message); + activity?.AddException(ex); } + finally + { + activity?.Dispose(); + } + } + + List toComplete; + lock (_activeTasks) + { + toComplete = _activeTasks.Values.ToList(); } - foreach (var task in activeTasks.Values) - task.Wait(); + 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) { @@ -137,6 +158,15 @@ private async Task ExecuteItem(string itemId) catch (Exception ex) { Logger.LogError(default(EventId), ex, $"Error executing item {itemId} - {ex.Message}"); + activity?.AddException(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 559c123f0..fc7c0887f 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?.AddException(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); } } @@ -68,20 +108,54 @@ private async void PollRunnables(object target) catch (Exception ex) { _logger.LogError(ex, ex.Message); + activity?.AddException(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?.AddException(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 @@ -93,7 +167,55 @@ private async void PollRunnables(object target) catch (Exception ex) { _logger.LogError(ex, ex.Message); + activity?.AddException(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?.AddException(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 f9222c715..c7d13138b 100644 --- a/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs +++ b/src/WorkflowCore/Services/BackgroundTasks/WorkflowConsumer.cs @@ -1,28 +1,35 @@ 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; namespace WorkflowCore.Services.BackgroundTasks { + /// + /// Background task responsible for consuming workflow items from the queue and processing them. + /// This consumer ensures that workflows are removed from the greylist after processing, + /// regardless of their status, to prevent workflows from getting stuck in "Pending" state. + /// 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; } @@ -31,74 +38,124 @@ protected override async Task ProcessItem(string itemId, CancellationToken cance { if (!await _lockProvider.AcquireLock(itemId, cancellationToken)) { - Logger.LogInformation("Workflow locked {0}", itemId); + Logger.LogInformation("Workflow locked {ItemId}", itemId); return; } - + WorkflowInstance workflow = null; WorkflowExecutorResult result = null; - var persistenceStore = _persistenceStorePool.Get(); + try { - try + cancellationToken.ThrowIfCancellationRequested(); + workflow = await _persistenceStore.GetWorkflowInstance(itemId, cancellationToken); + + WorkflowActivity.Enrich(workflow, "process"); + if (workflow.Status == WorkflowStatus.Runnable) { - cancellationToken.ThrowIfCancellationRequested(); - workflow = await persistenceStore.GetWorkflowInstance(itemId); - if (workflow.Status == WorkflowStatus.Runnable) + try { - var executor = _executorPool.Get(); - try - { - result = await executor.Execute(workflow); - } - finally - { - _executorPool.Return(executor); - await persistenceStore.PersistWorkflow(workflow); - await QueueProvider.QueueWork(itemId, QueueType.Index); - } + result = await _executor.Execute(workflow, cancellationToken); + } + finally + { + WorkflowActivity.Enrich(result); + await _persistenceStore.PersistWorkflow(workflow, result?.Subscriptions, cancellationToken); + await QueueProvider.QueueWork(itemId, QueueType.Index); } } - finally + else + { + Logger.LogDebug("Workflow {ItemId} is not runnable, status: {Status}", itemId, workflow.Status); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error processing workflow {ItemId}", itemId); + throw; + } + finally + { + // Always remove from greylist regardless of workflow status + // This prevents workflows from being stuck in greylist when they can't be processed + Logger.LogDebug("Removing workflow {ItemId} from greylist", itemId); + _greylist.Remove($"wf:{itemId}"); + + await _lockProvider.ReleaseLock(itemId); + if ((workflow != null) && (result != null)) { - await _lockProvider.ReleaseLock(itemId); - if ((workflow != null) && (result != null)) + foreach (var sub in result.Subscriptions) { - foreach (var sub in result.Subscriptions) - { - await SubscribeEvent(sub, persistenceStore); - } + await TryProcessSubscription(sub, _persistenceStore, cancellationToken); + } - await persistenceStore.PersistErrors(result.Errors); + await _persistenceStore.PersistErrors(result.Errors, cancellationToken); + if ((workflow.Status == WorkflowStatus.Runnable) && workflow.NextExecution.HasValue) + { var readAheadTicks = _datetimeProvider.UtcNow.Add(Options.PollInterval).Ticks; - - if ((workflow.Status == WorkflowStatus.Runnable) && workflow.NextExecution.HasValue && workflow.NextExecution.Value < readAheadTicks) + if (workflow.NextExecution.Value < readAheadTicks) { new Task(() => FutureQueue(workflow, cancellationToken)).Start(); } + else + { + if (_persistenceStore.SupportsScheduledCommands) + { + await _persistenceStore.ScheduleCommand(new ScheduledCommand() + { + CommandName = ScheduledCommand.ProcessWorkflow, + Data = workflow.Id, + ExecuteTime = workflow.NextExecution.Value + }); + } + } } } } - finally - { - _persistenceStorePool.Return(persistenceStore); - } + } - - 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); if (subscription.EventName != Event.EventTypeActivity) { - var events = await persistenceStore.GetEvents(subscription.EventName, subscription.EventKey, subscription.SubscribeAsOf); + var events = await persistenceStore.GetEvents(subscription.EventName, subscription.EventKey, subscription.SubscribeAsOf, cancellationToken); + foreach (var evt in events) { - await persistenceStore.MarkEventUnprocessed(evt); - await QueueProvider.QueueWork(evt, QueueType.Event); + 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); + } + } } } } @@ -126,4 +183,4 @@ private async void FutureQueue(WorkflowInstance workflow, CancellationToken canc } } } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/CancellationProcessor.cs b/src/WorkflowCore/Services/CancellationProcessor.cs index 74c7a8a60..4e56de9ea 100644 --- a/src/WorkflowCore/Services/CancellationProcessor.cs +++ b/src/WorkflowCore/Services/CancellationProcessor.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -12,11 +10,13 @@ public class CancellationProcessor : ICancellationProcessor { protected readonly ILogger _logger; private readonly IExecutionResultProcessor _executionResultProcessor; + private readonly IDateTimeProvider _dateTimeProvider; - public CancellationProcessor(IExecutionResultProcessor executionResultProcessor, ILoggerFactory logFactory) + public CancellationProcessor(IExecutionResultProcessor executionResultProcessor, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) { _executionResultProcessor = executionResultProcessor; _logger = logFactory.CreateLogger(); + _dateTimeProvider = dateTimeProvider; } public void ProcessCancellations(WorkflowInstance workflow, WorkflowDefinition workflowDef, WorkflowExecutorResult executionResult) @@ -44,13 +44,13 @@ public void ProcessCancellations(WorkflowInstance workflow, WorkflowDefinition w _executionResultProcessor.ProcessExecutionResult(workflow, workflowDef, ptr, step, ExecutionResult.Next(), executionResult); } - ptr.EndTime = DateTime.Now.ToUniversalTime(); + 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 = DateTime.Now.ToUniversalTime(); + descendent.EndTime = _dateTimeProvider.UtcNow; descendent.Active = false; descendent.Status = PointerStatus.Cancelled; } diff --git a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs index fd27db8ca..cb69c8791 100644 --- a/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/MemoryPersistenceProvider.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -24,7 +24,9 @@ public class MemoryPersistenceProvider : ISingletonMemoryProvider private readonly List _events = new List(); private readonly List _errors = new List(); - public async Task CreateNewWorkflow(WorkflowInstance workflow) + public bool SupportsScheduledCommands => false; + + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken _ = default) { lock (_instances) { @@ -34,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) { @@ -44,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) { @@ -53,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) { @@ -61,7 +82,7 @@ public async Task GetWorkflowInstance(string Id) } } - public async Task> GetWorkflowInstances(IEnumerable ids) + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken _ = default) { if (ids == null) { @@ -105,7 +126,7 @@ public async Task> GetWorkflowInstances(WorkflowSt } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) { lock (_subscriptions) { @@ -115,7 +136,7 @@ public async Task CreateEventSubscription(EventSubscription subscription } } - public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf) + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) { lock (_subscriptions) { @@ -124,7 +145,7 @@ public async Task> GetSubscriptions(string eventN } } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) { lock (_subscriptions) { @@ -133,7 +154,7 @@ public async Task TerminateSubscription(string eventSubscriptionId) } } - public Task GetSubscription(string eventSubscriptionId) + public Task GetSubscription(string eventSubscriptionId, CancellationToken _ = default) { lock (_subscriptions) { @@ -142,7 +163,7 @@ public Task GetSubscription(string eventSubscriptionId) } } - public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf) + public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) { lock (_subscriptions) { @@ -152,7 +173,7 @@ public Task GetFirstOpenSubscription(string eventName, string } } - public Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry) + public Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken _ = default) { lock (_subscriptions) { @@ -165,7 +186,7 @@ public Task SetSubscriptionToken(string eventSubscriptionId, string token, } } - public Task ClearSubscriptionToken(string eventSubscriptionId, string token) + public Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken _ = default) { lock (_subscriptions) { @@ -184,7 +205,7 @@ public void EnsureStoreExists() { } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken _ = default) { lock (_events) { @@ -194,7 +215,7 @@ public async Task CreateEvent(Event newEvent) } } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken _ = default) { lock (_events) { @@ -204,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) { @@ -216,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) { @@ -224,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) { @@ -236,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) { @@ -248,13 +269,23 @@ 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 diff --git a/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs b/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs index db66ccf57..40d13fa9c 100644 --- a/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs +++ b/src/WorkflowCore/Services/DefaultProviders/NullSearchIndex.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs b/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs index 40a826b91..007b51efb 100644 --- a/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs +++ b/src/WorkflowCore/Services/DefaultProviders/SingleNodeEventHub.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; 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 8e0bd8dda..56f63b550 100644 --- a/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/SingleNodeQueueProvider.cs @@ -14,7 +14,7 @@ namespace WorkflowCore.Services public class SingleNodeQueueProvider : IQueueProvider { - private readonly Dictionary> _queues = new Dictionary>() + private readonly Dictionary> _queues = new Dictionary> { [QueueType.Workflow] = new BlockingCollection(), [QueueType.Event] = new BlockingCollection(), diff --git a/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs b/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs index 6b3084a89..31ec6dbe2 100644 --- a/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs +++ b/src/WorkflowCore/Services/DefaultProviders/TransientMemoryPersistenceProvider.cs @@ -1,6 +1,6 @@ -using System; +using System; using System.Collections.Generic; -using System.Text; +using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -11,50 +11,72 @@ public class TransientMemoryPersistenceProvider : IPersistenceProvider { private readonly ISingletonMemoryProvider _innerService; + public bool SupportsScheduledCommands => false; + public TransientMemoryPersistenceProvider(ISingletonMemoryProvider innerService) { _innerService = innerService; } - public Task CreateEvent(Event newEvent) => _innerService.CreateEvent(newEvent); + public Task CreateEvent(Event newEvent, CancellationToken _ = default) => _innerService.CreateEvent(newEvent); - public Task CreateEventSubscription(EventSubscription subscription) => _innerService.CreateEventSubscription(subscription); + public Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) => _innerService.CreateEventSubscription(subscription); - public Task CreateNewWorkflow(WorkflowInstance workflow) => _innerService.CreateNewWorkflow(workflow); + public Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken _ = default) => _innerService.CreateNewWorkflow(workflow); public void EnsureStoreExists() => _innerService.EnsureStoreExists(); - public Task GetEvent(string id) => _innerService.GetEvent(id); + public Task GetEvent(string id, CancellationToken _ = default) => _innerService.GetEvent(id); - public Task> GetEvents(string eventName, string eventKey, DateTime asOf) => _innerService.GetEvents(eventName, eventKey, asOf); + public Task> GetEvents(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetEvents(eventName, eventKey, asOf); - public Task> GetRunnableEvents(DateTime asAt) => _innerService.GetRunnableEvents(asAt); + public Task> GetRunnableEvents(DateTime asAt, CancellationToken _ = default) => _innerService.GetRunnableEvents(asAt); - public Task> GetRunnableInstances(DateTime asAt) => _innerService.GetRunnableInstances(asAt); + public Task> GetRunnableInstances(DateTime asAt, CancellationToken _ = default) => _innerService.GetRunnableInstances(asAt); - public Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf) => _innerService.GetSubscriptions(eventName, eventKey, asOf); + public Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetSubscriptions(eventName, eventKey, asOf); - public Task GetWorkflowInstance(string Id) => _innerService.GetWorkflowInstance(Id); + public Task GetWorkflowInstance(string Id, CancellationToken _ = default) => _innerService.GetWorkflowInstance(Id); - public Task> GetWorkflowInstances(IEnumerable ids) => _innerService.GetWorkflowInstances(ids); + 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) => _innerService.MarkEventProcessed(id); + 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 MarkEventUnprocessed(string id) => _innerService.MarkEventUnprocessed(id); + public Task PersistWorkflow(WorkflowInstance workflow, CancellationToken _ = default) => _innerService.PersistWorkflow(workflow); - public Task PersistErrors(IEnumerable errors) => _innerService.PersistErrors(errors); + 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 PersistWorkflow(WorkflowInstance workflow) => _innerService.PersistWorkflow(workflow); + public Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) => _innerService.TerminateSubscription(eventSubscriptionId); + public Task GetSubscription(string eventSubscriptionId, CancellationToken _ = default) => _innerService.GetSubscription(eventSubscriptionId); - public Task TerminateSubscription(string eventSubscriptionId) => _innerService.TerminateSubscription(eventSubscriptionId); - public Task GetSubscription(string eventSubscriptionId) => _innerService.GetSubscription(eventSubscriptionId); + public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken _ = default) => _innerService.GetFirstOpenSubscription(eventName, eventKey, asOf); - public Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf) => _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 SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry) => _innerService.SetSubscriptionToken(eventSubscriptionId, token, workerId, expiry); + public Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken _ = default) => _innerService.ClearSubscriptionToken(eventSubscriptionId, token); - public Task ClearSubscriptionToken(string eventSubscriptionId, string token) => _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/ErrorHandlers/CompensateHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs index 12b62342a..d8990c160 100644 --- a/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs +++ b/src/WorkflowCore/Services/ErrorHandlers/CompensateHandler.cs @@ -1,26 +1,22 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services.ErrorHandlers { public class CompensateHandler : IWorkflowErrorHandler { - private readonly ILifeCycleEventPublisher _eventPublisher; private readonly IExecutionPointerFactory _pointerFactory; private readonly IDateTimeProvider _datetimeProvider; private readonly WorkflowOptions _options; public WorkflowErrorHandling Type => WorkflowErrorHandling.Compensate; - public CompensateHandler(IExecutionPointerFactory pointerFactory, ILifeCycleEventPublisher eventPublisher, IDateTimeProvider datetimeProvider, WorkflowOptions options) + public CompensateHandler(IExecutionPointerFactory pointerFactory, IDateTimeProvider datetimeProvider, WorkflowOptions options) { _pointerFactory = pointerFactory; - _eventPublisher = eventPublisher; _datetimeProvider = datetimeProvider; _options = options; } @@ -29,7 +25,8 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP { var scope = new Stack(exceptionPointer.Scope.Reverse()); scope.Push(exceptionPointer.Id); - + ExecutionPointer compensationPointer = null; + while (scope.Any()) { var pointerId = scope.Pop(); @@ -67,7 +64,14 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP { scopePointer.Status = PointerStatus.Compensated; - var compensationPointer = _pointerFactory.BuildCompensationPointer(def, scopePointer, exceptionPointer, scopeStep.CompensationStepId.Value); + 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) @@ -89,8 +93,16 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP var siblingStep = def.Steps.FindById(siblingPointer.StepId); if (siblingStep.CompensationStepId.HasValue) { - var compensationPointer = _pointerFactory.BuildCompensationPointer(def, siblingPointer, exceptionPointer, siblingStep.CompensationStepId.Value); - workflow.ExecutionPointers.Add(compensationPointer); + 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 index 0a4d54810..84fbe5bd3 100644 --- a/src/WorkflowCore/Services/ErrorHandlers/RetryHandler.cs +++ b/src/WorkflowCore/Services/ErrorHandlers/RetryHandler.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services.ErrorHandlers { diff --git a/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs b/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs index 1cf6f7941..3cc3279ec 100755 --- a/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs +++ b/src/WorkflowCore/Services/ErrorHandlers/SuspendHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Models.LifeCycleEvents; @@ -22,7 +21,7 @@ public SuspendHandler(ILifeCycleEventPublisher eventPublisher, IDateTimeProvider public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue) { workflow.Status = WorkflowStatus.Suspended; - _eventPublisher.PublishNotification(new WorkflowSuspended() + _eventPublisher.PublishNotification(new WorkflowSuspended { EventTimeUtc = _datetimeProvider.UtcNow, Reference = workflow.Reference, @@ -30,6 +29,8 @@ public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionP 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 index d89f7f470..6cafe5ece 100755 --- a/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs +++ b/src/WorkflowCore/Services/ErrorHandlers/TerminateHandler.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Models.LifeCycleEvents; @@ -10,21 +9,23 @@ namespace WorkflowCore.Services.ErrorHandlers public class TerminateHandler : IWorkflowErrorHandler { private readonly ILifeCycleEventPublisher _eventPublisher; - private readonly IDateTimeProvider _datetimeProvider; + private readonly IDateTimeProvider _dateTimeProvider; public WorkflowErrorHandling Type => WorkflowErrorHandling.Terminate; - public TerminateHandler(ILifeCycleEventPublisher eventPublisher, IDateTimeProvider datetimeProvider) + public TerminateHandler(ILifeCycleEventPublisher eventPublisher, IDateTimeProvider dateTimeProvider) { _eventPublisher = eventPublisher; - _datetimeProvider = datetimeProvider; + _dateTimeProvider = dateTimeProvider; } public void Handle(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception, Queue bubbleUpQueue) { workflow.Status = WorkflowStatus.Terminated; - _eventPublisher.PublishNotification(new WorkflowTerminated() + workflow.CompleteTime = _dateTimeProvider.UtcNow; + + _eventPublisher.PublishNotification(new WorkflowTerminated { - EventTimeUtc = _datetimeProvider.UtcNow, + EventTimeUtc = _dateTimeProvider.UtcNow, Reference = workflow.Reference, WorkflowInstanceId = workflow.Id, WorkflowDefinitionId = workflow.WorkflowDefinitionId, diff --git a/src/WorkflowCore/Services/ExecutionPointerFactory.cs b/src/WorkflowCore/Services/ExecutionPointerFactory.cs index 65f5a2f8f..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; @@ -24,7 +23,7 @@ public ExecutionPointer BuildGenesisPointer(WorkflowDefinition def) public ExecutionPointer BuildNextPointer(WorkflowDefinition def, ExecutionPointer pointer, IStepOutcome outcomeTarget) { var nextId = GenerateId(); - return new ExecutionPointer() + return new ExecutionPointer { Id = nextId, PredecessorId = pointer.Id, @@ -44,7 +43,7 @@ public ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPoint childScope.Insert(0, pointer.Id); pointer.Children.Add(childPointerId); - return new ExecutionPointer() + return new ExecutionPointer { Id = childPointerId, PredecessorId = pointer.Id, @@ -60,7 +59,7 @@ public ExecutionPointer BuildChildPointer(WorkflowDefinition def, ExecutionPoint 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, diff --git a/src/WorkflowCore/Services/ExecutionResultProcessor.cs b/src/WorkflowCore/Services/ExecutionResultProcessor.cs index 448506823..3c684945f 100755 --- a/src/WorkflowCore/Services/ExecutionResultProcessor.cs +++ b/src/WorkflowCore/Services/ExecutionResultProcessor.cs @@ -44,7 +44,7 @@ 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, @@ -67,7 +67,17 @@ public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, pointer, outcomeTarget)); } - _eventPublisher.PublishNotification(new StepCompleted() + 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, @@ -92,7 +102,7 @@ public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception) { - _eventPublisher.PublishNotification(new WorkflowError() + _eventPublisher.PublishNotification(new WorkflowError { EventTimeUtc = _datetimeProvider.UtcNow, Reference = workflow.Reference, diff --git a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs index 93f965eba..9e41f15ca 100644 --- a/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs +++ b/src/WorkflowCore/Services/FluentBuilders/StepBuilder.cs @@ -45,7 +45,7 @@ public IStepBuilder Then(Action> } newStep.Name = newStep.Name ?? typeof(TStep).Name; - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -53,18 +53,18 @@ public IStepBuilder Then(Action> public IStepBuilder Then(IStepBuilder newStep) where TStep : IStepBody { - Step.Outcomes.Add(new ValueOutcome() { 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 ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -74,13 +74,13 @@ 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 ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } public IStepBuilder Attach(string id) { - Step.Outcomes.Add(new ValueOutcome() + Step.Outcomes.Add(new ValueOutcome { ExternalNextStepId = id }); @@ -100,7 +100,7 @@ public IStepOutcomeBuilder When(object outcomeValue, string label = null) var outcomeBuilder = new StepOutcomeBuilder(WorkflowBuilder, result); return outcomeBuilder; } - + public IStepBuilder Branch(object outcomeValue, IStepBuilder branch) where TStep : IStepBody { if (branch.WorkflowBuilder.Steps.Count == 0) @@ -123,10 +123,10 @@ public IStepBuilder Branch(Expression(outcomeExpression) - { + { NextStep = branch.WorkflowBuilder.Steps[0].Id }); @@ -155,7 +155,7 @@ public IStepBuilder Input(Action(action)); return this; - } + } public IStepBuilder Output(Expression> dataProperty, Expression> value) { @@ -184,7 +184,7 @@ public IStepBuilder WaitFor(string eventName, Expression step.EffectiveDate, effectiveDate); } - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -203,10 +203,10 @@ public IStepBuilder WaitFor(string eventName, Expression step.EffectiveDate, effectiveDate); } - Step.Outcomes.Add(new ValueOutcome() { 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); @@ -259,7 +259,7 @@ public IStepBuilder EndWorkflow() { EndStep newStep = new EndStep(); WorkflowBuilder.AddStep(newStep); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return this; } @@ -272,7 +272,7 @@ public IStepBuilder Delay(Expression> period WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -286,7 +286,7 @@ public IStepBuilder Decide(Expression> expres WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -294,14 +294,50 @@ public IStepBuilder Decide(Expression> expres public IContainerStepBuilder ForEach(Expression> collection) { var newStep = new WorkflowStep(); - + + Expression> inputExpr = (x => x.Collection); + newStep.Inputs.Add(new MemberMapParameter(collection, inputExpr)); + + 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)); + 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 ValueOutcome() { 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; } @@ -316,7 +352,22 @@ public IContainerStepBuilder While(Expression(WorkflowBuilder, newStep); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + 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 ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -331,11 +382,26 @@ public IContainerStepBuilder If(Expression> con WorkflowBuilder.AddStep(newStep); var stepBuilder = new StepBuilder(WorkflowBuilder, newStep); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + 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 ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } - + public IContainerStepBuilder When(Expression> outcomeValue, string label = null) { var newStep = new WorkflowStep(); @@ -348,7 +414,7 @@ public IContainerStepBuilder When(Expression(); WorkflowBuilder.AddStep(switchStep); - Step.Outcomes.Add(new ValueOutcome() + Step.Outcomes.Add(new ValueOutcome { NextStep = switchStep.Id, Label = label @@ -359,7 +425,7 @@ public IContainerStepBuilder When(Expression); } - + WorkflowBuilder.AddStep(newStep); var stepBuilder = new ReturnStepBuilder(WorkflowBuilder, newStep, switchBuilder); switchBuilder.Step.Children.Add(newStep.Id); @@ -372,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 ValueOutcome() { 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 @@ -386,7 +452,7 @@ public IParallelStepBuilder Parallel() WorkflowBuilder.AddStep(newStep); var stepBuilder = new ParallelStepBuilder(WorkflowBuilder, newBuilder, newBuilder); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -399,7 +465,7 @@ public IContainerStepBuilder Schedule(Expression(WorkflowBuilder, newStep, this); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -416,7 +482,7 @@ public IContainerStepBuilder Recur(Expression(WorkflowBuilder, newStep, this); - Step.Outcomes.Add(new ValueOutcome() { NextStep = newStep.Id }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } @@ -493,14 +559,33 @@ public IStepBuilder Activity(string activityName, Expression(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 }); + Step.Outcomes.Add(new ValueOutcome { NextStep = newStep.Id }); return stepBuilder; } } diff --git a/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs b/src/WorkflowCore/Services/FluentBuilders/WorkflowBuilder.cs index 2621f80d3..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; @@ -63,6 +65,33 @@ 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; @@ -74,6 +103,17 @@ public void AttachBranch(IWorkflowBuilder branch) 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; + } } } @@ -148,7 +188,122 @@ 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 index 25334bcd2..e73c6bf4f 100644 --- a/src/WorkflowCore/Services/LifeCycleEventPublisher.cs +++ b/src/WorkflowCore/Services/LifeCycleEventPublisher.cs @@ -1,10 +1,9 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using WorkflowCore.Interface; +using WorkflowCore.Models; using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services @@ -12,20 +11,22 @@ namespace WorkflowCore.Services public class LifeCycleEventPublisher : ILifeCycleEventPublisher, IDisposable { private readonly ILifeCycleEventHub _eventHub; + private readonly WorkflowOptions _workflowOptions; private readonly ILogger _logger; - private readonly BlockingCollection _outbox; + private BlockingCollection _outbox; private Task _dispatchTask; - public LifeCycleEventPublisher(ILifeCycleEventHub eventHub, ILoggerFactory loggerFactory) + 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) + if (_outbox.IsAddingCompleted || !_workflowOptions.EnableLifeCycleEventsPublisher) return; _outbox.Add(evt); @@ -38,6 +39,11 @@ public void Start() throw new InvalidOperationException(); } + if (_outbox.IsAddingCompleted) + { + _outbox = new BlockingCollection(); + } + _dispatchTask = new Task(Execute); _dispatchTask.Start(); } diff --git a/src/WorkflowCore/Services/ScopeProvider.cs b/src/WorkflowCore/Services/ScopeProvider.cs index ad1ae98d2..5c52d8eb9 100644 --- a/src/WorkflowCore/Services/ScopeProvider.cs +++ b/src/WorkflowCore/Services/ScopeProvider.cs @@ -6,20 +6,20 @@ namespace WorkflowCore.Services { /// /// A concrete implementation for the IScopeProvider interface - /// Largely to get around the problems of unit testing an extension method (CreateScope()) + /// Could be used for context-aware scope creation customization /// public class ScopeProvider : IScopeProvider { - private readonly IServiceProvider provider; + private readonly IServiceScopeFactory _serviceScopeFactory; - public ScopeProvider(IServiceProvider provider) + public ScopeProvider(IServiceScopeFactory serviceScopeFactory) { - this.provider = provider; + _serviceScopeFactory = serviceScopeFactory; } - public IServiceScope CreateScope() + public IServiceScope CreateScope(IStepExecutionContext context) { - return provider.CreateScope(); + 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 index ab1b84469..5317a8966 100644 --- a/src/WorkflowCore/Services/SyncWorkflowRunner.cs +++ b/src/WorkflowCore/Services/SyncWorkflowRunner.cs @@ -1,11 +1,9 @@ using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using WorkflowCore.Exceptions; using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Models.LifeCycleEvents; namespace WorkflowCore.Services { @@ -18,8 +16,9 @@ public class SyncWorkflowRunner : ISyncWorkflowRunner 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) + public SyncWorkflowRunner(IWorkflowHost host, IWorkflowExecutor executor, IDistributedLockProvider lockService, IWorkflowRegistry registry, IPersistenceProvider persistenceStore, IExecutionPointerFactory pointerFactory, IQueueProvider queueService, IDateTimeProvider dateTimeProvider) { _host = host; _executor = executor; @@ -28,9 +27,18 @@ public SyncWorkflowRunner(IWorkflowHost host, IWorkflowExecutor executor, IDistr _persistenceStore = persistenceStore; _pointerFactory = pointerFactory; _queueService = queueService; + _dateTimeProvider = dateTimeProvider; } - public async Task RunWorkflowSync(string workflowId, int version, TData data, string reference, TimeSpan timeOut, bool persistSate = true) + 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); @@ -46,7 +54,7 @@ public async Task RunWorkflowSync(string workflowId, in Data = data, Description = def.Description, NextExecution = 0, - CreateTime = DateTime.Now.ToUniversalTime(), + CreateTime = _dateTimeProvider.UtcNow, Status = WorkflowStatus.Suspended, Reference = reference }; @@ -61,12 +69,10 @@ public async Task RunWorkflowSync(string workflowId, in wf.ExecutionPointers.Add(_pointerFactory.BuildGenesisPointer(def)); - var stopWatch = new Stopwatch(); - var id = Guid.NewGuid().ToString(); if (persistSate) - id = await _persistenceStore.CreateNewWorkflow(wf); + id = await _persistenceStore.CreateNewWorkflow(wf, token); else wf.Id = id; @@ -79,17 +85,15 @@ public async Task RunWorkflowSync(string workflowId, in try { - stopWatch.Start(); - while ((wf.Status == WorkflowStatus.Runnable) && (timeOut.TotalMilliseconds > stopWatch.ElapsedMilliseconds)) + while ((wf.Status == WorkflowStatus.Runnable) && !token.IsCancellationRequested) { - await _executor.Execute(wf); + await _executor.Execute(wf, token); if (persistSate) - await _persistenceStore.PersistWorkflow(wf); + await _persistenceStore.PersistWorkflow(wf, token); } } finally { - stopWatch.Stop(); await _lockService.ReleaseLock(id); } diff --git a/src/WorkflowCore/Services/WorkflowActivity.cs b/src/WorkflowCore/Services/WorkflowActivity.cs new file mode 100644 index 000000000..3fe3b720c --- /dev/null +++ b/src/WorkflowCore/Services/WorkflowActivity.cs @@ -0,0 +1,132 @@ +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; + + if (string.IsNullOrEmpty(activity.DisplayName)) + { + activity.DisplayName = $"step {stepName}"; + } + else + { + 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(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 index fc7c7b810..79272e084 100755 --- a/src/WorkflowCore/Services/WorkflowController.cs +++ b/src/WorkflowCore/Services/WorkflowController.cs @@ -1,8 +1,9 @@ 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; @@ -19,9 +20,11 @@ public class WorkflowController : IWorkflowController 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, ILifeCycleEventHub eventHub, 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; @@ -29,7 +32,9 @@ public WorkflowController(IPersistenceProvider persistenceStore, IDistributedLoc _queueProvider = queueProvider; _pointerFactory = pointerFactory; _eventHub = eventHub; + _serviceProvider = serviceProvider; _logger = loggerFactory.CreateLogger(); + _dateTimeProvider = dateTimeProvider; } public Task StartWorkflow(string workflowId, object data = null, string reference=null) @@ -42,10 +47,10 @@ public Task StartWorkflow(string workflowId, int? version, object data = return StartWorkflow(workflowId, version, data, reference); } - public Task StartWorkflow(string workflowId, TData data = null, string reference=null) + public Task StartWorkflow(string workflowId, TData data = null, string reference = null) where TData : class, new() { - return StartWorkflow(workflowId, null, data, reference); + return StartWorkflow(workflowId, null, data, reference); } public async Task StartWorkflow(string workflowId, int? version, TData data = null, string reference=null) @@ -65,7 +70,7 @@ public async Task StartWorkflow(string workflowId, int? version, Data = data, Description = def.Description, NextExecution = 0, - CreateTime = DateTime.Now.ToUniversalTime(), + CreateTime = _dateTimeProvider.UtcNow, Status = WorkflowStatus.Runnable, Reference = reference }; @@ -80,12 +85,18 @@ public async Task StartWorkflow(string workflowId, int? version, 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() + await _eventHub.PublishNotification(new WorkflowStarted { - EventTimeUtc = DateTime.UtcNow, + EventTimeUtc = _dateTimeProvider.UtcNow, Reference = reference, WorkflowInstanceId = id, WorkflowDefinitionId = def.Id, @@ -96,13 +107,13 @@ await _eventHub.PublishNotification(new WorkflowStarted() 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; @@ -126,9 +137,9 @@ 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() + await _eventHub.PublishNotification(new WorkflowSuspended { - EventTimeUtc = DateTime.UtcNow, + EventTimeUtc = _dateTimeProvider.UtcNow, Reference = wf.Reference, WorkflowInstanceId = wf.Id, WorkflowDefinitionId = wf.WorkflowDefinitionId, @@ -162,9 +173,9 @@ public async Task ResumeWorkflow(string workflowId) await _persistenceStore.PersistWorkflow(wf); requeue = true; await _queueProvider.QueueWork(workflowId, QueueType.Index); - await _eventHub.PublishNotification(new WorkflowResumed() + await _eventHub.PublishNotification(new WorkflowResumed { - EventTimeUtc = DateTime.UtcNow, + EventTimeUtc = _dateTimeProvider.UtcNow, Reference = wf.Reference, WorkflowInstanceId = wf.Id, WorkflowDefinitionId = wf.WorkflowDefinitionId, @@ -193,12 +204,15 @@ 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() + await _eventHub.PublishNotification(new WorkflowTerminated { - EventTimeUtc = DateTime.UtcNow, + EventTimeUtc = _dateTimeProvider.UtcNow, Reference = wf.Reference, WorkflowInstanceId = wf.Id, WorkflowDefinitionId = wf.WorkflowDefinitionId, @@ -213,18 +227,18 @@ await _eventHub.PublishNotification(new WorkflowTerminated() } 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 index 7597e7490..da3e9cd85 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -37,7 +39,7 @@ public WorkflowExecutor(IWorkflowRegistry registry, IServiceProvider serviceProv _executionResultProcessor = executionResultProcessor; } - public async Task Execute(WorkflowInstance workflow) + public async Task Execute(WorkflowInstance workflow, CancellationToken cancellationToken = default) { var wfResult = new WorkflowExecutorResult(); @@ -45,10 +47,10 @@ public async Task Execute(WorkflowInstance workflow) 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) @@ -59,9 +61,9 @@ public async Task Execute(WorkflowInstance workflow) var step = def.Steps.FindById(pointer.StepId); if (step == null) { - _logger.LogError("Unable to find step {0} in workflow definition", pointer.StepId); + _logger.LogError("Unable to find step {StepId} in workflow definition", pointer.StepId); pointer.SleepUntil = _datetimeProvider.UtcNow.Add(_options.ErrorRetryInterval); - wfResult.Errors.Add(new ExecutionError() + wfResult.Errors.Add(new ExecutionError { WorkflowId = workflow.Id, ExecutionPointerId = pointer.Id, @@ -70,32 +72,39 @@ public async Task Execute(WorkflowInstance workflow) }); continue; } - + + WorkflowActivity.Enrich(step); try { - if (!InitializeStep(workflow, step, wfResult, def, pointer)) + if (!InitializeStep(workflow, step, wfResult, def, pointer)) continue; - await ExecuteStep(workflow, step, pointer, wfResult, def); + await ExecuteStep(workflow, step, pointer, wfResult, def, cancellationToken); } catch (Exception ex) { - _logger.LogError(ex, "Workflow {0} raised error on step {1} Message: {2}", workflow.Id, pointer.StepId, ex.Message); - wfResult.Errors.Add(new ExecutionError() + _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.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; } @@ -115,7 +124,7 @@ private bool InitializeStep(WorkflowInstance workflow, WorkflowStep step, Workfl if (pointer.Status != PointerStatus.Running) { pointer.Status = PointerStatus.Running; - _publisher.PublishNotification(new StepStarted() + _publisher.PublishNotification(new StepStarted { EventTimeUtc = _datetimeProvider.UtcNow, Reference = workflow.Reference, @@ -135,37 +144,39 @@ private bool InitializeStep(WorkflowInstance workflow, WorkflowStep step, Workfl return true; } - private async Task ExecuteStep(WorkflowInstance workflow, WorkflowStep step, ExecutionPointer pointer, WorkflowExecutorResult wfResult, WorkflowDefinition def) + private async Task ExecuteStep(WorkflowInstance workflow, WorkflowStep step, ExecutionPointer pointer, WorkflowExecutorResult wfResult, WorkflowDefinition def, CancellationToken cancellationToken = default) { - using (var scope = _scopeProvider.CreateScope()) + IStepExecutionContext context = new StepExecutionContext + { + Workflow = workflow, + Step = step, + PersistenceData = pointer.PersistenceData, + ExecutionPointer = pointer, + Item = pointer.ContextItem, + CancellationToken = cancellationToken + }; + + using (var scope = _scopeProvider.CreateScope(context)) { - _logger.LogDebug("Starting step {0} on workflow {1}", step.Name, workflow.Id); + _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 {0}", step.BodyType.ToString()); + _logger.LogError("Unable to construct step body {BodyType}", step.BodyType.ToString()); pointer.SleepUntil = _datetimeProvider.UtcNow.Add(_options.ErrorRetryInterval); - wfResult.Errors.Add(new ExecutionError() + wfResult.Errors.Add(new ExecutionError { WorkflowId = workflow.Id, ExecutionPointerId = pointer.Id, ErrorTime = _datetimeProvider.UtcNow, - Message = $"Unable to construct step body {step.BodyType.ToString()}" + Message = $"Unable to construct step body {step.BodyType}" }); return; } - IStepExecutionContext context = new StepExecutionContext() - { - Workflow = workflow, - Step = step, - PersistenceData = pointer.PersistenceData, - ExecutionPointer = pointer, - Item = pointer.ContextItem - }; - foreach (var input in step.Inputs) input.AssignInput(workflow.Data, body, context); @@ -179,7 +190,7 @@ private async Task ExecuteStep(WorkflowInstance workflow, WorkflowStep step, Exe return; } - var result = await body.RunAsync(context); + var result = await stepExecutor.ExecuteStep(context, body); if (result.Proceed) { @@ -203,13 +214,15 @@ private void ProcessAfterExecutionIteration(WorkflowInstance workflow, WorkflowD } } - 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)) { @@ -223,30 +236,36 @@ private void DetermineNextExecutionTime(WorkflowInstance workflow) 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) - { - workflow.NextExecution = 0; - return; - } + if (!workflow.ExecutionPointers.FindByScope(pointer.Id).All(x => x.EndTime.HasValue)) + continue; - var pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; - workflow.NextExecution = Math.Min(pointerSleep, workflow.NextExecution ?? pointerSleep); + if (!pointer.SleepUntil.HasValue) + { + workflow.NextExecution = 0; + return; } + + var pointerSleep = pointer.SleepUntil.Value.ToUniversalTime().Ticks; + workflow.NextExecution = Math.Min(pointerSleep, workflow.NextExecution ?? pointerSleep); } - if ((workflow.NextExecution != null) || (workflow.ExecutionPointers.Any(x => x.EndTime == null))) + if ((workflow.NextExecution != null) || (workflow.ExecutionPointers.Any(x => x.EndTime == null))) + { return; - + } + workflow.Status = WorkflowStatus.Complete; workflow.CompleteTime = _datetimeProvider.UtcNow; - _publisher.PublishNotification(new WorkflowCompleted() + + using (var scope = _serviceProvider.CreateScope()) + { + var middlewareRunner = scope.ServiceProvider.GetRequiredService(); + await middlewareRunner.RunPostMiddleware(workflow, def); + } + + _publisher.PublishNotification(new WorkflowCompleted { EventTimeUtc = _datetimeProvider.UtcNow, Reference = workflow.Reference, @@ -255,6 +274,5 @@ private void DetermineNextExecutionTime(WorkflowInstance workflow) Version = workflow.Version }); } - } -} +} \ No newline at end of file diff --git a/src/WorkflowCore/Services/WorkflowHost.cs b/src/WorkflowCore/Services/WorkflowHost.cs index 9092e6487..d2a09ec70 100644 --- a/src/WorkflowCore/Services/WorkflowHost.cs +++ b/src/WorkflowCore/Services/WorkflowHost.cs @@ -4,10 +4,9 @@ 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 @@ -49,7 +48,6 @@ public WorkflowHost(IPersistenceProvider persistenceStore, IQueueProvider queueP _searchIndex = searchIndex; _activityController = activityController; _lifeCycleEventHub = lifeCycleEventHub; - _lifeCycleEventHub.Subscribe(HandleLifeCycleEvent); } public Task StartWorkflow(string workflowId, object data = null, string reference=null) @@ -86,17 +84,34 @@ public void Start() public async Task StartAsync(CancellationToken cancellationToken) { - _shutdown = false; - PersistenceStore.EnsureStoreExists(); - await QueueProvider.Start(); - await LockProvider.Start(); - await _lifeCycleEventHub.Start(); - await _searchIndex.Start(); - - Logger.LogInformation("Starting background tasks"); - - foreach (var task in _backgroundTasks) - task.Start(); + 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.AddException(ex); + throw; + } + finally + { + activity?.Dispose(); + } } public void Stop() @@ -121,18 +136,16 @@ public async Task StopAsync(CancellationToken cancellationToken) } public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + where TWorkflow : IWorkflow { - TWorkflow wf = new TWorkflow(); - Registry.RegisterWorkflow(wf); + _workflowController.RegisterWorkflow(); } public void RegisterWorkflow() - where TWorkflow : IWorkflow, new() + where TWorkflow : IWorkflow where TData : new() { - TWorkflow wf = new TWorkflow(); - Registry.RegisterWorkflow(wf); + _workflowController.RegisterWorkflow(); } public Task SuspendWorkflow(string workflowId) @@ -185,5 +198,10 @@ 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 8ed6a6f7b..beed19c0e 100644 --- a/src/WorkflowCore/Services/WorkflowRegistry.cs +++ b/src/WorkflowCore/Services/WorkflowRegistry.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -9,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) { @@ -21,68 +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); - // TODO: What in the heck does Item3 mean? - return entry?.Item3; + if (!_registry.ContainsKey($"{workflowId}-{version}")) + return default; + return _registry[$"{workflowId}-{version}"]; } else { - var entry = _registry.Where(x => x.Item1 == workflowId).OrderByDescending(x => x.Item2) - .FirstOrDefault(); - return entry?.Item3; + if (!_lastestVersion.ContainsKey(workflowId)) + return default; + return _lastestVersion[workflowId]; } } public void DeregisterWorkflow(string workflowId, int version) { - var definition = _registry.Find(x => x.Item1 == workflowId && x.Item2 == version); - if (definition != null) + if (!_registry.ContainsKey($"{workflowId}-{version}")) + return; + + lock (_registry) { - _registry.Remove(definition); + _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; + } } } public void RegisterWorkflow(IWorkflow workflow) { - if (_registry.Any(x => x.Item1 == workflow.Id && x.Item2 == workflow.Version)) - { - throw new InvalidOperationException($"Workflow {workflow.Id} version {workflow.Version} is already registered"); - } - - var builder = _serviceProvider.GetService().UseData(); + var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); - _registry.Add(Tuple.Create(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(Tuple.Create(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 registered"); - } - var builder = _serviceProvider.GetService().UseData(); workflow.Build(builder); var def = builder.Build(workflow.Id, workflow.Version); - _registry.Add(Tuple.Create(workflow.Id, workflow.Version, def)); + RegisterWorkflow(def); } public bool IsRegistered(string workflowId, int version) { - var definition = _registry.Find(x => x.Item1 == workflowId && x.Item2 == version); - return (definition != null); + 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 27960af9e..ab0607213 100644 --- a/src/WorkflowCore/WorkflowCore.csproj +++ b/src/WorkflowCore/WorkflowCore.csproj @@ -7,35 +7,30 @@ 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. - 3.1.5 - 3.1.5.0 - 3.1.5.0 - - https://github.com/danielgerlag/workflow-core/raw/master/src/logo.png - 3.1.5 + - - + + + + + <_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 901bd28b7..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; @@ -39,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 bdd8c6518..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; diff --git a/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs b/src/extensions/WorkflowCore.Users/Primitives/EscalateStep.cs index 7ed21b795..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 diff --git a/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs b/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs index 5f67ca443..7772a894c 100644 --- a/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs +++ b/src/extensions/WorkflowCore.Users/Primitives/UserTask.cs @@ -50,7 +50,7 @@ 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; } @@ -74,7 +74,7 @@ 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(), 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 44e9e1ce7..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 ValueOutcome() { 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 ValueOutcome() { 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 ce241ab52..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; @@ -53,7 +51,7 @@ public IUserTaskBuilder WithEscalation(Expression> var lastStep = WorkflowBuilder.LastStep; action.Invoke(WorkflowBuilder); if (WorkflowBuilder.LastStep > lastStep) - newStep.Outcomes.Add(new ValueOutcome() { 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 742f848f5..66c96f4ec 100644 --- a/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj +++ b/src/extensions/WorkflowCore.Users/WorkflowCore.Users.csproj @@ -15,9 +15,6 @@ false false Provides extensions for Workflow Core to enable human workflows. - 2.1.0 - 2.1.0.0 - 2.1.0.0 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 f4200b13c..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; 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 a3fb9bbb7..f6eb6ea92 100644 --- a/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj +++ b/src/extensions/WorkflowCore.WebAPI/WorkflowCore.WebAPI.csproj @@ -14,7 +14,6 @@ false false false - 2.0.0 WebAPI wrapper for Workflow host 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..8bb0cf5dc 100644 --- a/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs +++ b/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs @@ -1,11 +1,10 @@ using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Threading.Tasks; using Microsoft.Extensions.Logging; 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 ceb51046d..4d9702579 100644 --- a/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj +++ b/src/providers/WorkflowCore.LockProviders.SqlServer/WorkflowCore.LockProviders.SqlServer.csproj @@ -5,11 +5,10 @@ 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 - 2.0.0 - + diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs index cf3f9183c..536ccc7be 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,12 +9,16 @@ 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, + ObjectCreationHandling = ObjectCreationHandling.Replace + }; internal static PersistedWorkflow ToPersistable(this WorkflowInstance instance, PersistedWorkflow persistable = null) { - if (persistable == null) - persistable = new PersistedWorkflow(); + if (persistable == null) + persistable = new PersistedWorkflow(); persistable.Data = JsonConvert.SerializeObject(instance.Data, SerializerSettings); persistable.Description = instance.Description; @@ -26,19 +29,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.FindById(ep.Id); - + if (persistedEP == null) { persistedEP = new PersistedExecutionPointer(); persistedEP.Id = ep.Id ?? Guid.NewGuid().ToString(); persistable.ExecutionPointers.Add(persistedEP); - } - + } + persistedEP.StepId = ep.StepId; persistedEP.Active = ep.Active; persistedEP.SleepUntil = ep.SleepUntil; @@ -84,7 +87,7 @@ internal static PersistedWorkflow ToPersistable(this WorkflowInstance instance, internal static PersistedExecutionError ToPersistable(this ExecutionError instance) { - var result = new PersistedExecutionError(); + var result = new PersistedExecutionError(); result.ErrorTime = instance.ErrorTime; result.Message = instance.Message; result.ExecutionPointerId = instance.ExecutionPointerId; @@ -95,7 +98,7 @@ internal static PersistedExecutionError ToPersistable(this ExecutionError instan internal static PersistedSubscription ToPersistable(this EventSubscription instance) { - PersistedSubscription result = new PersistedSubscription(); + PersistedSubscription result = new PersistedSubscription(); result.SubscriptionId = new Guid(instance.Id); result.EventKey = instance.EventKey; result.EventName = instance.EventName; @@ -107,7 +110,7 @@ internal static PersistedSubscription ToPersistable(this EventSubscription insta result.ExternalToken = instance.ExternalToken; result.ExternalTokenExpiry = instance.ExternalTokenExpiry; result.ExternalWorkerId = instance.ExternalWorkerId; - + return result; } @@ -124,6 +127,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(); @@ -143,7 +156,7 @@ internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow insta foreach (var ep in instance.ExecutionPointers) { - var pointer = new ExecutionPointer(); + var pointer = new ExecutionPointer(); pointer.Id = ep.Id; pointer.StepId = ep.StepId; @@ -220,5 +233,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 index 574f9c39f..61dfb5214 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Interfaces/IWorkflowDbContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Interfaces/IWorkflowDbContextFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Persistence.EntityFramework.Services; namespace WorkflowCore.Persistence.EntityFramework.Interfaces 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 index c8950803b..cbff64146 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointerCollection.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedExecutionPointerCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Text; namespace WorkflowCore.Persistence.EntityFramework.Models { 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 f8fe0cca0..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 { diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Models/PersistedWorkflow.cs index b212bc0c9..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 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 3af7354de..22ba680f5 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/EntityFrameworkPersistenceProvider.cs @@ -2,15 +2,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; 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 { @@ -20,6 +17,8 @@ public class EntityFrameworkPersistenceProvider : IPersistenceProvider private readonly bool _canMigrateDB; private readonly IWorkflowDbContextFactory _contextFactory; + public bool SupportsScheduledCommands => true; + public EntityFrameworkPersistenceProvider(IWorkflowDbContextFactory contextFactory, bool canCreateDB, bool canMigrateDB) { _contextFactory = contextFactory; @@ -27,31 +26,31 @@ public EntityFrameworkPersistenceProvider(IWorkflowDbContextFactory contextFacto _canMigrateDB = canMigrateDB; } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { subscription.Id = Guid.NewGuid().ToString(); var persistable = subscription.ToPersistable(); var result = db.Set().Add(persistable); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); return subscription.Id; } } - public async Task CreateNewWorkflow(WorkflowInstance workflow) + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { workflow.Id = Guid.NewGuid().ToString(); var persistable = workflow.ToPersistable(); var result = db.Set().Add(persistable); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); return workflow.Id; } } - public async Task> GetRunnableInstances(DateTime asAt) + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -59,7 +58,7 @@ public async Task> GetRunnableInstances(DateTime asAt) 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(); } @@ -97,7 +96,7 @@ public async Task> GetWorkflowInstances(WorkflowSt } } - public async Task GetWorkflowInstance(string Id) + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -106,7 +105,7 @@ public async Task GetWorkflowInstance(string Id) .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; @@ -115,7 +114,7 @@ public async Task GetWorkflowInstance(string Id) } } - public async Task> GetWorkflowInstances(IEnumerable ids) + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) { if (ids == null) { @@ -131,11 +130,11 @@ public async Task> GetWorkflowInstances(IEnumerabl .Include(wf => wf.ExecutionPointers) .Where(x => uids.Contains(x.InstanceId)); - return (await raw.ToListAsync()).Select(i => i.ToWorkflowInstance()); + return (await raw.ToListAsync(cancellationToken)).Select(i => i.ToWorkflowInstance()); } } - public async Task PersistWorkflow(WorkflowInstance workflow) + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -146,21 +145,47 @@ public async Task PersistWorkflow(WorkflowInstance workflow) .ThenInclude(ep => ep.ExtensionAttributes) .Include(wf => wf.ExecutionPointers) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); var persistable = workflow.ToPersistable(existingEntity); - await db.SaveChangesAsync(); + 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); + + var workflowPersistable = workflow.ToPersistable(existingEntity); + + foreach (var subscription in subscriptions) + { + subscription.Id = Guid.NewGuid().ToString(); + var subscriptionPersistable = subscription.ToPersistable(); + db.Set().Add(subscriptionPersistable); + } + + await db.SaveChangesAsync(cancellationToken); } } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { var uid = new Guid(eventSubscriptionId); - var existing = await db.Set().FirstAsync(x => x.SubscriptionId == uid); + var existing = await db.Set().FirstAsync(x => x.SubscriptionId == uid, cancellationToken); db.Set().Remove(existing); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); } } @@ -182,38 +207,38 @@ public virtual void EnsureStoreExists() } } - public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf) + public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { asOf = asOf.ToUniversalTime(); 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(); } } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { newEvent.Id = Guid.NewGuid().ToString(); var persistable = newEvent.ToPersistable(); var result = db.Set().Add(persistable); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); return newEvent.Id; } } - public async Task GetEvent(string id) + public async Task GetEvent(string id, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { Guid uid = new Guid(id); var raw = await db.Set() - .FirstAsync(x => x.EventId == uid); + .FirstAsync(x => x.EventId == uid, cancellationToken); if (raw == null) return null; @@ -222,7 +247,7 @@ public async Task GetEvent(string id) } } - public async Task> GetRunnableEvents(DateTime asAt) + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) { var now = asAt.ToUniversalTime(); using (var db = ConstructDbContext()) @@ -232,13 +257,13 @@ public async Task> GetRunnableEvents(DateTime asAt) .Where(x => !x.IsProcessed) .Where(x => x.EventTime <= now) .Select(x => x.EventId) - .ToListAsync(); + .ToListAsync(cancellationToken); return raw.Select(s => s.ToString()).ToList(); } } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -246,14 +271,14 @@ public async Task MarkEventProcessed(string id) var existingEntity = await db.Set() .Where(x => x.EventId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); existingEntity.IsProcessed = true; - await db.SaveChangesAsync(); + 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) { using (var db = ConstructDbContext()) { @@ -261,7 +286,7 @@ public async Task> GetEvents(string eventName, string eventK .Where(x => x.EventName == eventName && x.EventKey == eventKey) .Where(x => x.EventTime >= asOf) .Select(x => x.EventId) - .ToListAsync(); + .ToListAsync(cancellationToken); var result = new List(); @@ -272,7 +297,7 @@ public async Task> GetEvents(string eventName, string eventK } } - public async Task MarkEventUnprocessed(string id) + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -280,14 +305,14 @@ public async Task MarkEventUnprocessed(string id) var existingEntity = await db.Set() .Where(x => x.EventId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); existingEntity.IsProcessed = false; - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); } } - public async Task PersistErrors(IEnumerable errors) + public async Task PersistErrors(IEnumerable errors, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -298,7 +323,7 @@ public async Task PersistErrors(IEnumerable errors) { db.Set().Add(error.ToPersistable()); } - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); } } @@ -309,28 +334,28 @@ private WorkflowDbContext ConstructDbContext() return _contextFactory.Build(); } - public async Task GetSubscription(string eventSubscriptionId) + 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); + var raw = await db.Set().FirstOrDefaultAsync(x => x.SubscriptionId == uid, cancellationToken); return raw?.ToEventSubscription(); } } - public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf) + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { - var raw = await db.Set().FirstOrDefaultAsync(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf && x.ExternalToken == null); + 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) + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -338,18 +363,18 @@ public async Task SetSubscriptionToken(string eventSubscriptionId, string var existingEntity = await db.Set() .Where(x => x.SubscriptionId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); existingEntity.ExternalToken = token; existingEntity.ExternalWorkerId = workerId; existingEntity.ExternalTokenExpiry = expiry; - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); return true; } } - public async Task ClearSubscriptionToken(string eventSubscriptionId, string token) + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) { using (var db = ConstructDbContext()) { @@ -357,7 +382,7 @@ public async Task ClearSubscriptionToken(string eventSubscriptionId, string toke var existingEntity = await db.Set() .Where(x => x.SubscriptionId == uid) .AsTracking() - .FirstAsync(); + .FirstAsync(cancellationToken); if (existingEntity.ExternalToken != token) throw new InvalidOperationException(); @@ -365,7 +390,49 @@ public async Task ClearSubscriptionToken(string eventSubscriptionId, string toke existingEntity.ExternalToken = null; existingEntity.ExternalWorkerId = null; existingEntity.ExternalTokenExpiry = null; - await db.SaveChangesAsync(); + 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 index 69f83cf96..53e0967e7 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowDbContext.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowDbContext.cs @@ -1,13 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using WorkflowCore.Interface; -using WorkflowCore.Models; using WorkflowCore.Persistence.EntityFramework.Models; namespace WorkflowCore.Persistence.EntityFramework.Services @@ -20,6 +14,7 @@ public abstract class WorkflowDbContext : DbContext 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) { @@ -44,12 +39,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) 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) diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs index e75caaed0..904895809 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Services/WorkflowPurger.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Linq.Dynamic.Core; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using WorkflowCore.Interface; @@ -19,12 +19,12 @@ public WorkflowPurger(IWorkflowDbContextFactory contextFactory) _contextFactory = contextFactory; } - public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan) + 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(); + 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) @@ -39,7 +39,7 @@ public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan) db.Remove(wf); } - await db.SaveChangesAsync(); + await db.SaveChangesAsync(cancellationToken); } } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj b/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj index ed8392a62..8351a579e 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/WorkflowCore.Persistence.EntityFramework.csproj @@ -3,32 +3,39 @@ Workflow Core EntityFramework Core Persistence Provider Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0;net8.0 WorkflowCore.Persistence.EntityFramework WorkflowCore.Persistence.EntityFramework workflow;.NET;Core;state machine;WorkflowCore;EntityFramework;EntityFrameworkCore https://github.com/danielgerlag/workflow-core https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md git - https://github.com/danielgerlag/workflow-core.git + https://github.com/danielgerlag/workflow-core.git false false false - 3.1.0 Base package for Workflow-core peristence providers using entity framework - 3.1.0.0 - 3.1.0.0 - 3.1.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..9668e393d 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 serialization 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 a5d3ebb5c..9534fe0b2 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/ServiceCollectionExtensions.cs @@ -1,8 +1,5 @@ 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; @@ -11,21 +8,50 @@ 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 client = new MongoClient(mongoUrl); + 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 a01a770d9..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.Objects, }; - + public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { if (context.Reader.CurrentBsonType == BsonType.String) @@ -27,18 +24,103 @@ 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) { - var str = JsonConvert.SerializeObject(value, SerializerSettings); - var doc = BsonDocument.Parse(str); - ConvertMetaFormat(doc); + 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(); @@ -73,4 +155,4 @@ private static void ConvertMetaFormat(BsonDocument root) } } } -} +} \ 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 8edd69615..a72340d68 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/MongoPersistenceProvider.cs @@ -7,9 +7,11 @@ 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 { @@ -21,7 +23,6 @@ public class MongoPersistenceProvider : IPersistenceProvider public MongoPersistenceProvider(IMongoDatabase database) { _database = database; - CreateIndexes(this); } static MongoPersistenceProvider() @@ -44,7 +45,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); @@ -79,6 +81,9 @@ static MongoPersistenceProvider() 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; @@ -87,8 +92,11 @@ static void CreateIndexes(MongoPersistenceProvider instance) if (!indexesCreated) { instance.WorkflowInstances.Indexes.CreateOne(new CreateIndexModel( - Builders.IndexKeys.Ascending(x => x.NextExecution), - new CreateIndexOptions {Background = true, Name = "idx_nextExec"})); + 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), @@ -107,6 +115,17 @@ static void CreateIndexes(MongoPersistenceProvider instance) .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; } } @@ -119,47 +138,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> GetRunnableInstances(DateTime asAt) + 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, 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) + 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)); - return await result.ToListAsync(); + 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) { - IMongoQueryable result = WorkflowInstances.AsQueryable(); + IQueryable result = WorkflowInstances.AsQueryable(); if (status.HasValue) result = result.Where(x => x.Status == status.Value); @@ -176,116 +214,158 @@ public async Task> GetWorkflowInstances(WorkflowSt 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); + await EventSubscriptions.DeleteOneAsync(x => x.Id == eventSubscriptionId, cancellationToken); } - public async Task GetSubscription(string eventSubscriptionId) + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) { - var result = await EventSubscriptions.FindAsync(x => x.Id == eventSubscriptionId); - return await result.FirstOrDefaultAsync(); + 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) + 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(); + return await query.FirstOrDefaultAsync(cancellationToken); } - public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry) + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) { 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); + 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) + 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); + await EventSubscriptions.UpdateOneAsync(x => x.Id == eventSubscriptionId && x.ExternalToken == token, update, cancellationToken: cancellationToken); } public void EnsureStoreExists() { - + CreateIndexes(this); } - public async Task> GetSubscriptions(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 index 85b1be31b..56343d3f6 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/Services/WorkflowPurger.cs +++ b/src/providers/WorkflowCore.Persistence.MongoDB/Services/WorkflowPurger.cs @@ -3,24 +3,26 @@ 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); + private IMongoCollection WorkflowInstances => _database.GetCollection(MongoPersistenceProvider.WorkflowCollectionName); public WorkflowPurger(IMongoDatabase database) { _database = database; } - public async Task PurgeWorkflows(WorkflowStatus status, DateTime olderThan) + 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); + 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 b762cb930..5dfdf800f 100644 --- a/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj +++ b/src/providers/WorkflowCore.Persistence.MongoDB/WorkflowCore.Persistence.MongoDB.csproj @@ -3,7 +3,7 @@ Workflow Core MongoDB Persistence Provider Daniel Gerlag - netstandard2.0 + netstandard2.1 WorkflowCore.Persistence.MongoDB WorkflowCore.Persistence.MongoDB workflow;.NET;Core;state machine;WorkflowCore;MongoDB;Mongo @@ -14,11 +14,7 @@ false false false - 3.0.2 Provides support to persist workflows running on Workflow Core to a MongoDB database. - 3.0.2.0 - 3.0.2.0 - 3.0.2 @@ -26,8 +22,8 @@ - - + + 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 index 17a84e5d7..59d48fac7 100644 --- a/src/providers/WorkflowCore.Persistence.MySQL/Migrations/MysqlPersistenceProviderModelSnapshot.cs +++ b/src/providers/WorkflowCore.Persistence.MySQL/Migrations/MysqlPersistenceProviderModelSnapshot.cs @@ -14,8 +14,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.2") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 64) +#if NETSTANDARD2_1 + .HasAnnotation("ProductVersion", "5.0.1") +#elif NET6_0 + .HasAnnotation("ProductVersion", "7.0.0") +#elif NET8_0 + .HasAnnotation("ProductVersion", "9.0.9") +#else + .HasAnnotation("ProductVersion", "9.0.9") +#endif + ; modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => { @@ -24,18 +33,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bigint"); b.Property("EventData") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("EventId") .HasColumnType("char(36)"); b.Property("EventKey") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("EventName") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("EventTime") .HasColumnType("datetime(6)"); @@ -67,15 +76,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("ExecutionPointerId") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("Message") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("WorkflowId") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.HasKey("PersistenceId"); @@ -92,47 +101,47 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("tinyint(1)"); b.Property("Children") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ContextItem") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("EndTime") .HasColumnType("datetime(6)"); b.Property("EventData") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("EventKey") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("EventName") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("EventPublished") .HasColumnType("tinyint(1)"); b.Property("Id") - .HasColumnType("varchar(50) CHARACTER SET utf8mb4") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("varchar(50)"); b.Property("Outcome") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("PersistenceData") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("PredecessorId") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("RetryCount") .HasColumnType("int"); b.Property("Scope") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("SleepUntil") .HasColumnType("datetime(6)"); @@ -147,8 +156,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("StepName") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("WorkflowId") .HasColumnType("bigint"); @@ -167,11 +176,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bigint"); b.Property("AttributeKey") - .HasColumnType("varchar(100) CHARACTER SET utf8mb4") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("varchar(100)"); b.Property("AttributeValue") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("ExecutionPointerId") .HasColumnType("bigint"); @@ -183,6 +192,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -190,27 +226,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bigint"); b.Property("EventKey") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("EventName") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("ExecutionPointerId") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("ExternalToken") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("ExternalTokenExpiry") .HasColumnType("datetime(6)"); b.Property("ExternalWorkerId") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("StepId") .HasColumnType("int"); @@ -219,15 +255,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("SubscriptionData") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("SubscriptionId") - .HasColumnType("char(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("char(200)"); b.Property("WorkflowId") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.HasKey("PersistenceId"); @@ -254,22 +290,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("Data") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Description") - .HasColumnType("varchar(500) CHARACTER SET utf8mb4") - .HasMaxLength(500); + .HasMaxLength(500) + .HasColumnType("varchar(500)"); b.Property("InstanceId") - .HasColumnType("char(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("char(200)"); b.Property("NextExecution") .HasColumnType("bigint"); b.Property("Reference") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.Property("Status") .HasColumnType("int"); @@ -278,8 +314,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("WorkflowDefinitionId") - .HasColumnType("varchar(200) CHARACTER SET utf8mb4") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("varchar(200)"); b.HasKey("PersistenceId"); @@ -298,6 +334,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("WorkflowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Workflow"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -307,6 +345,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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 index dbf4a8085..2b65f640d 100644 --- a/src/providers/WorkflowCore.Persistence.MySQL/MysqlContext.cs +++ b/src/providers/WorkflowCore.Persistence.MySQL/MysqlContext.cs @@ -21,7 +21,11 @@ public MysqlContext(string connectionString, Action builder) @@ -59,5 +63,11 @@ protected override void ConfigureEventStorage(EntityTypeBuilder 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/WorkflowCore.Persistence.MySQL.csproj b/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj index a031e5fba..c0fc07851 100644 --- a/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj +++ b/src/providers/WorkflowCore.Persistence.MySQL/WorkflowCore.Persistence.MySQL.csproj @@ -4,7 +4,7 @@ Workflow Core MySQL Persistence Provider 1.0.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0;net8.0 WorkflowCore.Persistence.MySQL WorkflowCore.Persistence.MySQL workflow;.NET;Core;state machine;WorkflowCore;MySQL @@ -16,12 +16,9 @@ false false Provides support to persist workflows running on Workflow Core to a MySQL database. - 3.0.1 - 3.0.1.0 - 3.0.1.0 - + all runtime; build; native; contentfiles; analyzers @@ -29,6 +26,30 @@ + + + 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.Oracle/MigrationContextFactory.cs b/src/providers/WorkflowCore.Persistence.Oracle/MigrationContextFactory.cs new file mode 100644 index 000000000..f5a37a5f9 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/MigrationContextFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore.Design; + +namespace WorkflowCore.Persistence.Oracle +{ + public class MigrationContextFactory : IDesignTimeDbContextFactory + { + public OracleContext CreateDbContext(string[] args) + { + return new OracleContext(@"Server=127.0.0.1;Database=myDataBase;Uid=myUsername;Pwd=myPassword;"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.Oracle/Migrations/20230310125506_InitialDatabase.Designer.cs b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/20230310125506_InitialDatabase.Designer.cs new file mode 100644 index 000000000..7948bc948 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/20230310125506_InitialDatabase.Designer.cs @@ -0,0 +1,377 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; +using WorkflowCore.Persistence.Oracle; + +#nullable disable + +namespace WorkflowCore.Persistence.Oracle.Migrations +{ + [DbContext(typeof(OracleContext))] + [Migration("20230310125506_InitialDatabase")] + partial class InitialDatabase + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("EventData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EventId") + .HasColumnType("RAW(16)"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("IsProcessed") + .HasColumnType("NUMBER(1)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("ErrorTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("Message") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("Active") + .HasColumnType("NUMBER(1)"); + + b.Property("Children") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ContextItem") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EndTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("EventData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("EventPublished") + .HasColumnType("NUMBER(1)"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("NVARCHAR2(50)"); + + b.Property("Outcome") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("PersistenceData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("RetryCount") + .HasColumnType("NUMBER(10)"); + + b.Property("Scope") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SleepUntil") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("StartTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("Status") + .HasColumnType("NUMBER(10)"); + + b.Property("StepId") + .HasColumnType("NUMBER(10)"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("WorkflowId") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("AttributeValue") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ExecutionPointerId") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("NVARCHAR2(500)"); + + b.Property("ExecuteTime") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique() + .HasFilter("\"CommandName\" IS NOT NULL AND \"Data\" IS NOT NULL"); + + b.ToTable("ScheduledCommand", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("StepId") + .HasColumnType("NUMBER(10)"); + + b.Property("SubscribeAsOf") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("SubscriptionData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("RAW(16)"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("CompleteTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("CreateTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("Data") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("NVARCHAR2(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("RAW(16)"); + + b.Property("NextExecution") + .HasColumnType("NUMBER(19)"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("Status") + .HasColumnType("NUMBER(10)"); + + b.Property("Version") + .HasColumnType("NUMBER(10)"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow", (string)null); + }); + + 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.Oracle/Migrations/20230310125506_InitialDatabase.cs b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/20230310125506_InitialDatabase.cs new file mode 100644 index 000000000..e758e161d --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/20230310125506_InitialDatabase.cs @@ -0,0 +1,260 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WorkflowCore.Persistence.Oracle.Migrations +{ + public partial class InitialDatabase : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Event", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + EventId = table.Column(type: "RAW(16)", nullable: false), + EventName = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + EventKey = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + EventData = table.Column(type: "NVARCHAR2(2000)", nullable: true), + EventTime = table.Column(type: "TIMESTAMP(7)", nullable: false), + IsProcessed = table.Column(type: "NUMBER(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Event", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "ExecutionError", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + WorkflowId = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + ExecutionPointerId = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + ErrorTime = table.Column(type: "TIMESTAMP(7)", nullable: false), + Message = table.Column(type: "NVARCHAR2(2000)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExecutionError", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "ScheduledCommand", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + CommandName = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + Data = table.Column(type: "NVARCHAR2(500)", maxLength: 500, nullable: true), + ExecuteTime = table.Column(type: "NUMBER(19)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ScheduledCommand", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "Subscription", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + SubscriptionId = table.Column(type: "RAW(16)", maxLength: 200, nullable: false), + WorkflowId = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + StepId = table.Column(type: "NUMBER(10)", nullable: false), + ExecutionPointerId = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + EventName = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + EventKey = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + SubscribeAsOf = table.Column(type: "TIMESTAMP(7)", nullable: false), + SubscriptionData = table.Column(type: "NVARCHAR2(2000)", nullable: true), + ExternalToken = table.Column(type: "NVARCHAR2(400)", maxLength: 400, nullable: true), + ExternalWorkerId = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + ExternalTokenExpiry = table.Column(type: "TIMESTAMP(7)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Subscription", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "Workflow", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + InstanceId = table.Column(type: "RAW(16)", maxLength: 200, nullable: false), + WorkflowDefinitionId = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + Version = table.Column(type: "NUMBER(10)", nullable: false), + Description = table.Column(type: "NVARCHAR2(500)", maxLength: 500, nullable: true), + Reference = table.Column(type: "NVARCHAR2(200)", maxLength: 200, nullable: true), + NextExecution = table.Column(type: "NUMBER(19)", nullable: true), + Data = table.Column(type: "CLOB", nullable: true), + CreateTime = table.Column(type: "TIMESTAMP(7)", nullable: false), + CompleteTime = table.Column(type: "TIMESTAMP(7)", nullable: true), + Status = table.Column(type: "NUMBER(10)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Workflow", x => x.PersistenceId); + }); + + migrationBuilder.CreateTable( + name: "ExecutionPointer", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + WorkflowId = table.Column(type: "NUMBER(19)", nullable: false), + Id = table.Column(type: "NVARCHAR2(50)", maxLength: 50, nullable: true), + StepId = table.Column(type: "NUMBER(10)", nullable: false), + Active = table.Column(type: "NUMBER(1)", nullable: false), + SleepUntil = table.Column(type: "TIMESTAMP(7)", nullable: true), + PersistenceData = table.Column(type: "NVARCHAR2(2000)", nullable: true), + StartTime = table.Column(type: "TIMESTAMP(7)", nullable: true), + EndTime = table.Column(type: "TIMESTAMP(7)", nullable: true), + EventName = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + EventKey = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + EventPublished = table.Column(type: "NUMBER(1)", nullable: false), + EventData = table.Column(type: "NVARCHAR2(2000)", nullable: true), + StepName = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + RetryCount = table.Column(type: "NUMBER(10)", nullable: false), + Children = table.Column(type: "NVARCHAR2(2000)", nullable: true), + ContextItem = table.Column(type: "NVARCHAR2(2000)", nullable: true), + PredecessorId = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + Outcome = table.Column(type: "NVARCHAR2(2000)", nullable: true), + Status = table.Column(type: "NUMBER(10)", nullable: false), + Scope = table.Column(type: "NVARCHAR2(2000)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExecutionPointer", x => x.PersistenceId); + table.ForeignKey( + name: "FK_ExecutionPointer_Wf_WfId", + column: x => x.WorkflowId, + principalTable: "Workflow", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExtensionAttribute", + columns: table => new + { + PersistenceId = table.Column(type: "NUMBER(19)", nullable: false) + .Annotation("Oracle:Identity", "START WITH 1 INCREMENT BY 1"), + ExecutionPointerId = table.Column(type: "NUMBER(19)", nullable: false), + AttributeKey = table.Column(type: "NVARCHAR2(100)", maxLength: 100, nullable: true), + AttributeValue = table.Column(type: "NVARCHAR2(2000)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExtensionAttribute", x => x.PersistenceId); + table.ForeignKey( + name: "FK_ExtAttr_ExPtr_ExPtrId", + column: x => x.ExecutionPointerId, + principalTable: "ExecutionPointer", + principalColumn: "PersistenceId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventId", + table: "Event", + column: "EventId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventName_EventKey", + table: "Event", + columns: new[] { "EventName", "EventKey" }); + + migrationBuilder.CreateIndex( + name: "IX_Event_EventTime", + table: "Event", + column: "EventTime"); + + migrationBuilder.CreateIndex( + name: "IX_Event_IsProcessed", + table: "Event", + column: "IsProcessed"); + + migrationBuilder.CreateIndex( + name: "IX_ExecutionPointer_WorkflowId", + table: "ExecutionPointer", + column: "WorkflowId"); + + migrationBuilder.CreateIndex( + name: "IX_ExtensionAttribute_ExecutionPointerId", + table: "ExtensionAttribute", + column: "ExecutionPointerId"); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_CommandName_Data", + table: "ScheduledCommand", + columns: new[] { "CommandName", "Data" }, + unique: true, + filter: "\"CommandName\" IS NOT NULL AND \"Data\" IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_ScheduledCommand_ExecuteTime", + table: "ScheduledCommand", + column: "ExecuteTime"); + + 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: "Event"); + + migrationBuilder.DropTable( + name: "ExecutionError"); + + migrationBuilder.DropTable( + name: "ExtensionAttribute"); + + migrationBuilder.DropTable( + name: "ScheduledCommand"); + + migrationBuilder.DropTable( + name: "Subscription"); + + migrationBuilder.DropTable( + name: "ExecutionPointer"); + + migrationBuilder.DropTable( + name: "Workflow"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.Oracle/Migrations/OracleContextModelSnapshot.cs b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/OracleContextModelSnapshot.cs new file mode 100644 index 000000000..7d88a0220 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/Migrations/OracleContextModelSnapshot.cs @@ -0,0 +1,381 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; +using WorkflowCore.Persistence.Oracle; + +#nullable disable + +namespace WorkflowCore.Persistence.Oracle.Migrations +{ + [DbContext(typeof(OracleContext))] + partial class OracleContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder +#if NET6_0 + .HasAnnotation("ProductVersion", "7.0.0") +#elif NET8_0 + .HasAnnotation("ProductVersion", "9.0.9") +#else + .HasAnnotation("ProductVersion", "9.0.9") +#endif + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("EventData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EventId") + .HasColumnType("RAW(16)"); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("IsProcessed") + .HasColumnType("NUMBER(1)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventId") + .IsUnique(); + + b.HasIndex("EventTime"); + + b.HasIndex("IsProcessed"); + + b.HasIndex("EventName", "EventKey"); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionError", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("ErrorTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("Message") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.HasKey("PersistenceId"); + + b.ToTable("ExecutionError", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExecutionPointer", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("Active") + .HasColumnType("NUMBER(1)"); + + b.Property("Children") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ContextItem") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EndTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("EventData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("EventKey") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("EventName") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("EventPublished") + .HasColumnType("NUMBER(1)"); + + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("NVARCHAR2(50)"); + + b.Property("Outcome") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("PersistenceData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("PredecessorId") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("RetryCount") + .HasColumnType("NUMBER(10)"); + + b.Property("Scope") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SleepUntil") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("StartTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("Status") + .HasColumnType("NUMBER(10)"); + + b.Property("StepId") + .HasColumnType("NUMBER(10)"); + + b.Property("StepName") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("WorkflowId") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("WorkflowId"); + + b.ToTable("ExecutionPointer", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("AttributeKey") + .HasMaxLength(100) + .HasColumnType("NVARCHAR2(100)"); + + b.Property("AttributeValue") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ExecutionPointerId") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecutionPointerId"); + + b.ToTable("ExtensionAttribute", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedScheduledCommand", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("CommandName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("Data") + .HasMaxLength(500) + .HasColumnType("NVARCHAR2(500)"); + + b.Property("ExecuteTime") + .HasColumnType("NUMBER(19)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("ExecuteTime"); + + b.HasIndex("CommandName", "Data") + .IsUnique() + .HasFilter("\"CommandName\" IS NOT NULL AND \"Data\" IS NOT NULL"); + + b.ToTable("ScheduledCommand", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedSubscription", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("EventKey") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("EventName") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExecutionPointerId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExternalToken") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("ExternalTokenExpiry") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("StepId") + .HasColumnType("NUMBER(10)"); + + b.Property("SubscribeAsOf") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("SubscriptionData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SubscriptionId") + .HasMaxLength(200) + .HasColumnType("RAW(16)"); + + b.Property("WorkflowId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("EventKey"); + + b.HasIndex("EventName"); + + b.HasIndex("SubscriptionId") + .IsUnique(); + + b.ToTable("Subscription", (string)null); + }); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedWorkflow", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("NUMBER(19)"); + + OraclePropertyBuilderExtensions.UseIdentityColumn(b.Property("PersistenceId"), 1L, 1); + + b.Property("CompleteTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("CreateTime") + .HasColumnType("TIMESTAMP(7)"); + + b.Property("Data") + .HasColumnType("CLOB"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("NVARCHAR2(500)"); + + b.Property("InstanceId") + .HasMaxLength(200) + .HasColumnType("RAW(16)"); + + b.Property("NextExecution") + .HasColumnType("NUMBER(19)"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.Property("Status") + .HasColumnType("NUMBER(10)"); + + b.Property("Version") + .HasColumnType("NUMBER(10)"); + + b.Property("WorkflowDefinitionId") + .HasMaxLength(200) + .HasColumnType("NVARCHAR2(200)"); + + b.HasKey("PersistenceId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.HasIndex("NextExecution"); + + b.ToTable("Workflow", (string)null); + }); + + 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.Oracle/OracleContext.cs b/src/providers/WorkflowCore.Persistence.Oracle/OracleContext.cs new file mode 100644 index 000000000..d6619c4b3 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/OracleContext.cs @@ -0,0 +1,72 @@ +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +using Oracle.EntityFrameworkCore.Infrastructure; + +using WorkflowCore.Persistence.EntityFramework.Models; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.Oracle +{ + public class OracleContext : WorkflowDbContext + { + private readonly string _connectionString; + private readonly Action _oracleOptionsAction; + + public OracleContext(string connectionString, Action oracleOptionsAction = null) + { + _connectionString = connectionString; + _oracleOptionsAction = oracleOptionsAction; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseOracle(_connectionString, _oracleOptionsAction); + } + + 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.Oracle/OracleContextFactory.cs b/src/providers/WorkflowCore.Persistence.Oracle/OracleContextFactory.cs new file mode 100644 index 000000000..e2d9c6e1a --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/OracleContextFactory.cs @@ -0,0 +1,26 @@ +using System; + +using Oracle.EntityFrameworkCore.Infrastructure; + +using WorkflowCore.Persistence.EntityFramework.Interfaces; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.Oracle +{ + public class OracleContextFactory : IWorkflowDbContextFactory + { + private readonly string _connectionString; + private readonly Action _oracleOptionsAction; + + public OracleContextFactory(string connectionString, Action oracleOptionsAction = null) + { + _connectionString = connectionString; + _oracleOptionsAction = oracleOptionsAction; + } + + public WorkflowDbContext Build() + { + return new OracleContext(_connectionString, _oracleOptionsAction); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.Oracle/README.md b/src/providers/WorkflowCore.Persistence.Oracle/README.md new file mode 100644 index 000000000..1dd74ee7c --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/README.md @@ -0,0 +1,31 @@ +# Oracle Persistence provider for Workflow Core + +Provides support to persist workflows running on [Workflow Core](../../README.md) to an Oracle database. + +## Installing + +Install the NuGet package "WorkflowCore.Persistence.Oracle" + +``` +PM> Install-Package WorkflowCore.Persistence.Oracle -Pre +``` + +## Usage + +Use the .UseOracle extension method when building your service provider. + +```C# +services.AddWorkflow(x => x.UseOracle(@"Server=127.0.0.1;Database=workflow;User=root;Password=password;", true, true)); +``` + +You can also add specific database version compatibility if needed. + +```C# +services.AddWorkflow(x => + { + x.UseOracle(connectionString, false, true, options => + { + options.UseOracleSQLCompatibility(OracleSQLCompatibility.DatabaseVersion19); + }); + }); +``` \ No newline at end of file diff --git a/src/providers/WorkflowCore.Persistence.Oracle/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.Oracle/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..704916fbe --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +using Oracle.EntityFrameworkCore.Infrastructure; + +using WorkflowCore.Interface; +using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework.Services; + +namespace WorkflowCore.Persistence.Oracle +{ + public static class ServiceCollectionExtensions + { + public static WorkflowOptions UseOracle(this WorkflowOptions options, string connectionString, bool canCreateDB, bool canMigrateDB, Action oracleOptionsAction = null) + { + options.UsePersistence(sp => new EntityFrameworkPersistenceProvider(new OracleContextFactory(connectionString, oracleOptionsAction), canCreateDB, canMigrateDB)); + options.Services.AddTransient(sp => new WorkflowPurger(new OracleContextFactory(connectionString, oracleOptionsAction))); + return options; + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.Oracle/WorkflowCore.Persistence.Oracle.csproj b/src/providers/WorkflowCore.Persistence.Oracle/WorkflowCore.Persistence.Oracle.csproj new file mode 100644 index 000000000..138ec3b95 --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.Oracle/WorkflowCore.Persistence.Oracle.csproj @@ -0,0 +1,50 @@ + + + + Workflow Core Oracle Persistence Provider + 1.0.0 + Christian Jundt + net6.0;net8.0 + WorkflowCore.Persistence.Oracle + WorkflowCore.Persistence.Oracle + workflow;.NET;Core;state machine;WorkflowCore;Oracle + 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 Oracle database. + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + 7.21.13 + + + + + + + + + diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs index c6f6d73f3..c74d0e6f1 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170126230815_InitialDatabase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs index 77fc5a589..6be6f105b 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20170312161610_Events.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 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.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.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.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/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/20250807084543_ChangeDateTimeTypeForPostgreSQL.Designer.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20250807084543_ChangeDateTimeTypeForPostgreSQL.Designer.cs new file mode 100644 index 000000000..63e6094bb --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20250807084543_ChangeDateTimeTypeForPostgreSQL.Designer.cs @@ -0,0 +1,377 @@ +// +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; + +#nullable disable + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20250807084543_ChangeDateTimeTypeForPostgreSQL")] + partial class ChangeDateTimeTypeForPostgreSQL + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.19") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => + { + b.Property("PersistenceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + 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 with 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + b.Property("ErrorTime") + .HasColumnType("timestamp with 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Children") + .HasColumnType("text"); + + b.Property("ContextItem") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + 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 with time zone"); + + b.Property("ExternalWorkerId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StepId") + .HasColumnType("integer"); + + b.Property("SubscribeAsOf") + .HasColumnType("timestamp with 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + b.Property("CompleteTime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreateTime") + .HasColumnType("timestamp with 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/20250807084543_ChangeDateTimeTypeForPostgreSQL.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20250807084543_ChangeDateTimeTypeForPostgreSQL.cs new file mode 100644 index 000000000..42ebf73dc --- /dev/null +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/20250807084543_ChangeDateTimeTypeForPostgreSQL.cs @@ -0,0 +1,191 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WorkflowCore.Persistence.PostgreSQL.Migrations +{ + /// + public partial class ChangeDateTimeTypeForPostgreSQL : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreateTime", + schema: "wfc", + table: "Workflow", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "CompleteTime", + schema: "wfc", + table: "Workflow", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SubscribeAsOf", + schema: "wfc", + table: "Subscription", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StartTime", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SleepUntil", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EndTime", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ErrorTime", + schema: "wfc", + table: "ExecutionError", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + + migrationBuilder.AlterColumn( + name: "EventTime", + schema: "wfc", + table: "Event", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp without time zone"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreateTime", + schema: "wfc", + table: "Workflow", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CompleteTime", + schema: "wfc", + table: "Workflow", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SubscribeAsOf", + schema: "wfc", + table: "Subscription", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ExternalTokenExpiry", + schema: "wfc", + table: "Subscription", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StartTime", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SleepUntil", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "EndTime", + schema: "wfc", + table: "ExecutionPointer", + type: "timestamp without time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ErrorTime", + schema: "wfc", + table: "ExecutionError", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "EventTime", + schema: "wfc", + table: "Event", + type: "timestamp without time zone", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + } +} diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs index 71ab82c32..5feae3a10 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/Migrations/PostgresPersistenceProviderModelSnapshot.cs @@ -6,6 +6,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using WorkflowCore.Persistence.PostgreSQL; +#nullable disable + namespace WorkflowCore.Persistence.PostgreSQL.Migrations { [DbContext(typeof(PostgresContext))] @@ -15,16 +17,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.0") +#if NETSTANDARD2_1 + .HasAnnotation("ProductVersion", "5.0.1") +#elif NET6_0 + .HasAnnotation("ProductVersion", "7.0.0") +#elif NET8_0 + .HasAnnotation("ProductVersion", "9.0.9") +#else + .HasAnnotation("ProductVersion", "9.0.9") +#endif .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => { b.Property("PersistenceId") .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("EventData") .HasColumnType("text"); @@ -33,15 +45,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("EventKey") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("EventName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("EventTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("IsProcessed") .HasColumnType("boolean"); @@ -57,41 +69,43 @@ 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("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("ErrorTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("ExecutionPointerId") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("Message") .HasColumnType("text"); b.Property("WorkflowId") - .HasColumnType("character varying(100)") - .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() - .HasColumnType("bigint") - .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("Active") .HasColumnType("boolean"); @@ -103,25 +117,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("EndTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("EventData") .HasColumnType("text"); b.Property("EventKey") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("EventName") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("EventPublished") .HasColumnType("boolean"); b.Property("Id") - .HasColumnType("character varying(50)") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("character varying(50)"); b.Property("Outcome") .HasColumnType("text"); @@ -130,8 +144,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("PredecessorId") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("RetryCount") .HasColumnType("integer"); @@ -140,10 +154,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("SleepUntil") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("StartTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Status") .HasColumnType("integer"); @@ -152,8 +166,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("StepName") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("WorkflowId") .HasColumnType("bigint"); @@ -162,19 +176,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("AttributeKey") - .HasColumnType("character varying(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("character varying(100)"); b.Property("AttributeValue") .HasColumnType("text"); @@ -186,55 +201,85 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); + + 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); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("EventKey") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("EventName") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ExecutionPointerId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ExternalToken") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("ExternalTokenExpiry") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("ExternalWorkerId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("StepId") .HasColumnType("integer"); b.Property("SubscribeAsOf") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("SubscriptionData") .HasColumnType("text"); b.Property("SubscriptionId") - .HasColumnType("uuid") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uuid"); b.Property("WorkflowId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("PersistenceId"); @@ -245,39 +290,40 @@ 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("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PersistenceId")); b.Property("CompleteTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("CreateTime") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Data") .HasColumnType("text"); b.Property("Description") - .HasColumnType("character varying(500)") - .HasMaxLength(500); + .HasMaxLength(500) + .HasColumnType("character varying(500)"); b.Property("InstanceId") - .HasColumnType("uuid") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uuid"); b.Property("NextExecution") .HasColumnType("bigint"); b.Property("Reference") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.Property("Status") .HasColumnType("integer"); @@ -286,8 +332,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("WorkflowDefinitionId") - .HasColumnType("character varying(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("character varying(200)"); b.HasKey("PersistenceId"); @@ -296,7 +342,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 => @@ -306,6 +352,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("WorkflowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Workflow"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -315,6 +363,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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/PostgresContext.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContext.cs index 818a75be2..e9a371d7f 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContext.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; @@ -62,6 +60,60 @@ protected override void ConfigureEventStorage(EntityTypeBuilder 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(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(x => + { + x.Property(p => p.CompleteTime) + .HasColumnType("timestamp with time zone"); + + x.Property(p => p.CreateTime) + .HasColumnType("timestamp with time zone"); + }); + + modelBuilder.Entity(x => + { + x.Property(p => p.SleepUntil) + .HasColumnType("timestamp with time zone"); + + x.Property(p => p.StartTime) + .HasColumnType("timestamp with time zone"); + + x.Property(p => p.EndTime) + .HasColumnType("timestamp with time zone"); + + }); + + modelBuilder.Entity(x => + { + x.Property(p => p.ErrorTime) + .HasColumnType("timestamp with time zone"); + + }); + + modelBuilder.Entity(x => + { + x.Property(p => p.SubscribeAsOf) + .HasColumnType("timestamp with time zone"); + + x.Property(p => p.ExternalTokenExpiry) + .HasColumnType("timestamp with time zone"); + }); + + modelBuilder.Entity( + x => x.Property(x => x.EventTime) + .HasColumnType("timestamp with time zone") + ); + } } } diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs index e0758dfab..98a633e1d 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/PostgresContextFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Persistence.EntityFramework.Interfaces; using WorkflowCore.Persistence.EntityFramework.Services; 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 8df41c092..9366c3936 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/ServiceCollectionExtensions.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.Persistence.EntityFramework.Services; diff --git a/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj b/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj index 7addc52a4..b79511625 100644 --- a/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj +++ b/src/providers/WorkflowCore.Persistence.PostgreSQL/WorkflowCore.Persistence.PostgreSQL.csproj @@ -3,7 +3,7 @@ Workflow Core PostgreSQL Persistence Provider Daniel Gerlag - netstandard2.1 + netstandard2.1;net6.0;net8.0 WorkflowCore.Persistence.PostgreSQL WorkflowCore.Persistence.PostgreSQL workflow;.NET;Core;state machine;WorkflowCore;PostgreSQL @@ -15,10 +15,6 @@ false false Provides support to persist workflows running on Workflow Core to a PostgreSQL database. - 3.1.0 - 3.1.0.0 - 3.1.0.0 - 3.1.0 @@ -26,13 +22,37 @@ - - - - + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + 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/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.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.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.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.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.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/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 8ecddd28e..299cdb315 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/Migrations/SqlServerPersistenceProviderModelSnapshot.cs @@ -15,8 +15,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0") .HasAnnotation("Relational:MaxIdentifierLength", 128) +#if NETSTANDARD2_1 + .HasAnnotation("ProductVersion", "5.0.1") +#elif NET6_0 + .HasAnnotation("ProductVersion", "7.0.0") +#elif NET8_0 || NET9_0 + .HasAnnotation("ProductVersion", "9.0.9") +#else + .HasAnnotation("ProductVersion", "9.0.9") +#endif .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedEvent", b => @@ -35,12 +43,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uniqueidentifier"); b.Property("EventKey") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("EventName") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("EventTime") .HasColumnType("datetime2"); @@ -59,7 +67,7 @@ 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 => @@ -75,19 +83,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime2"); b.Property("ExecutionPointerId") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("Message") .HasColumnType("nvarchar(max)"); b.Property("WorkflowId") - .HasColumnType("nvarchar(100)") - .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 => @@ -115,19 +123,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("EventKey") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("EventName") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("EventPublished") .HasColumnType("bit"); b.Property("Id") - .HasColumnType("nvarchar(50)") - .HasMaxLength(50); + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); b.Property("Outcome") .HasColumnType("nvarchar(max)"); @@ -136,8 +144,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("PredecessorId") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("RetryCount") .HasColumnType("int"); @@ -158,8 +166,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("StepName") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("WorkflowId") .HasColumnType("bigint"); @@ -168,7 +176,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("WorkflowId"); - b.ToTable("ExecutionPointer","wfc"); + b.ToTable("ExecutionPointer", "wfc"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -181,8 +189,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("AttributeKey") - .HasColumnType("nvarchar(100)") - .HasMaxLength(100); + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); b.Property("AttributeValue") .HasColumnType("nvarchar(max)"); @@ -194,7 +202,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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 => @@ -207,27 +246,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); b.Property("EventKey") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("EventName") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("ExecutionPointerId") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("ExternalToken") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("ExternalTokenExpiry") .HasColumnType("datetime2"); b.Property("ExternalWorkerId") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("StepId") .HasColumnType("int"); @@ -239,12 +278,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("SubscriptionId") - .HasColumnType("uniqueidentifier") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); b.Property("WorkflowId") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.HasKey("PersistenceId"); @@ -255,7 +294,7 @@ 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 => @@ -277,19 +316,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("Description") - .HasColumnType("nvarchar(500)") - .HasMaxLength(500); + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); b.Property("InstanceId") - .HasColumnType("uniqueidentifier") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("uniqueidentifier"); b.Property("NextExecution") .HasColumnType("bigint"); b.Property("Reference") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.Property("Status") .HasColumnType("int"); @@ -298,8 +337,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("WorkflowDefinitionId") - .HasColumnType("nvarchar(200)") - .HasMaxLength(200); + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); b.HasKey("PersistenceId"); @@ -308,7 +347,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 => @@ -318,6 +357,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("WorkflowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation("Workflow"); }); modelBuilder.Entity("WorkflowCore.Persistence.EntityFramework.Models.PersistedExtensionAttribute", b => @@ -327,6 +368,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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/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/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs index 009f1edb7..0a54b1ea2 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/ServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using System.Data.Common; using WorkflowCore.Interface; using WorkflowCore.Models; -using WorkflowCore.Persistence.EntityFramework.Interfaces; using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.SqlServer; diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs b/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs index 8cb6d57a8..066aae73f 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.SqlServer/SqlContextFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data.Common; using Microsoft.EntityFrameworkCore; using WorkflowCore.Persistence.EntityFramework.Interfaces; diff --git a/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.cs b/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.cs index 499c10295..eb03f3647 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/SqlServerContext.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; @@ -28,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 be52279d1..f617bfa05 100644 --- a/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj +++ b/src/providers/WorkflowCore.Persistence.SqlServer/WorkflowCore.Persistence.SqlServer.csproj @@ -4,7 +4,7 @@ Workflow Core SQL Server Persistence Provider 1.8.0 Daniel Gerlag - netstandard2.0 + netstandard2.1;net6.0;net8.0;net9.0 WorkflowCore.Persistence.SqlServer WorkflowCore.Persistence.SqlServer workflow;.NET;Core;state machine;WorkflowCore @@ -15,11 +15,7 @@ false false false - 3.1.0 Provides support to persist workflows running on Workflow Core to a SQL Server database. - 3.1.0.0 - 3.1.0.0 - 3.1.0 @@ -27,12 +23,45 @@ - - - + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + All + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + 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 f137c33b5..0d5dd5a8e 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/ServiceCollectionExtensions.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.Persistence.EntityFramework.Services; diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs index 7a9737c4e..0f2083933 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContext.cs @@ -1,9 +1,7 @@ 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; @@ -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 index 94c9e4df8..9d24d688c 100644 --- a/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContextFactory.cs +++ b/src/providers/WorkflowCore.Persistence.Sqlite/SqliteContextFactory.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Persistence.EntityFramework.Interfaces; using WorkflowCore.Persistence.EntityFramework.Services; diff --git a/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj b/src/providers/WorkflowCore.Persistence.Sqlite/WorkflowCore.Persistence.Sqlite.csproj index 2d2789021..d1fc08d7c 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;net8.0 WorkflowCore.Persistence.Sqlite WorkflowCore.Persistence.Sqlite workflow;.NET;Core;state machine;WorkflowCore;Sqlite @@ -16,10 +16,6 @@ false false Provides support to persist workflows running on Workflow Core to a Sqlite database. - 3.1.0 - 3.1.0.0 - 3.1.0.0 - 3.1.0 @@ -27,8 +23,16 @@ - - + + + + + + + + + + diff --git a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs index 41e48b013..d4d66614f 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisStreamConsumer.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Amazon.Kinesis.Model; diff --git a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs index 3a4cee3a0..f3cb37510 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Interface/IKinesisTracker.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace WorkflowCore.Providers.AWS.Interface diff --git a/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs b/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs index 6ec9b0f8b..daf5b6889 100644 --- a/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs +++ b/src/providers/WorkflowCore.Providers.AWS/ModelExtensions.cs @@ -1,18 +1,15 @@ using Amazon.DynamoDBv2.Model; -using Amazon.Util; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Newtonsoft.Json; -using Newtonsoft.Json.Bson; using WorkflowCore.Models; namespace WorkflowCore.Providers.AWS { internal static class ModelExtensions { - private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; + private static JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; public static Dictionary ToDynamoMap(this WorkflowInstance source) { @@ -21,10 +18,10 @@ public static Dictionary ToDynamoMap(this WorkflowInstan 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["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() }; + result["workflow_status"] = new AttributeValue { N = Convert.ToInt32(source.Status).ToString() }; if (!string.IsNullOrEmpty(source.Description)) result["description"] = new AttributeValue(source.Description); @@ -33,7 +30,7 @@ public static Dictionary ToDynamoMap(this WorkflowInstan result["reference"] = new AttributeValue(source.Reference); if (source.CompleteTime.HasValue) - result["complete_time"] = new AttributeValue() { N = source.CompleteTime.Value.Ticks.ToString() }; + result["complete_time"] = new AttributeValue { N = source.CompleteTime.Value.Ticks.ToString() }; var pointers = new List(); foreach (var pointer in source.ExecutionPointers) @@ -41,17 +38,17 @@ public static Dictionary ToDynamoMap(this WorkflowInstan pointers.Add(new AttributeValue(JsonConvert.SerializeObject(pointer, SerializerSettings))); } - result["pointers"] = new AttributeValue() { L = pointers }; + result["pointers"] = new AttributeValue { L = pointers }; if (source.Status == WorkflowStatus.Runnable) - result["runnable"] = new AttributeValue() { N = 1.ToString() }; + result["runnable"] = new AttributeValue { N = 1.ToString() }; return result; } public static WorkflowInstance ToWorkflowInstance(this Dictionary source) { - var result = new WorkflowInstance() + var result = new WorkflowInstance { Id = source["id"].S, WorkflowDefinitionId = source["workflow_definition_id"].S, @@ -90,7 +87,7 @@ public static Dictionary ToDynamoMap(this EventSubscript ["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() }, + ["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}") }; @@ -101,14 +98,14 @@ public static Dictionary ToDynamoMap(this EventSubscript result["external_worker_id"] = new AttributeValue(source.ExternalWorkerId); if (source.ExternalTokenExpiry.HasValue) - result["external_token_expiry"] = new AttributeValue() { N = source.ExternalTokenExpiry.Value.Ticks.ToString()}; + 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() + var result = new EventSubscription { Id = source["id"].S, EventName = source["event_name"].S, @@ -140,19 +137,19 @@ public static Dictionary ToDynamoMap(this Event source) ["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_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() }; + result["not_processed"] = new AttributeValue { N = 1.ToString() }; return result; } public static Event ToEvent(this Dictionary source) { - var result = new Event() + var result = new Event { Id = source["id"].S, EventName = source["event_name"].S, diff --git a/src/providers/WorkflowCore.Providers.AWS/README.md b/src/providers/WorkflowCore.Providers.AWS/README.md index b0717fc36..0a9286f37 100644 --- a/src/providers/WorkflowCore.Providers.AWS/README.md +++ b/src/providers/WorkflowCore.Providers.AWS/README.md @@ -27,13 +27,25 @@ 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 }); + 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) diff --git a/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs index 6c32f1571..c3c545f80 100644 --- a/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Providers.AWS/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using Amazon; using Amazon.DynamoDBv2; +using Amazon.Kinesis; using Amazon.Runtime; using Amazon.SQS; using Microsoft.Extensions.Logging; @@ -13,30 +14,57 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - public static WorkflowOptions UseAwsSimpleQueueService(this WorkflowOptions options, AWSCredentials credentials, AmazonSQSConfig config) + public static WorkflowOptions UseAwsSimpleQueueService(this WorkflowOptions options, AWSCredentials credentials, AmazonSQSConfig config, string queuesPrefix = "workflowcore") { - options.UseQueueProvider(sp => new SQSQueueProvider(credentials, config, sp.GetService())); + 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) { - options.UseDistributedLockManager(sp => new DynamoLockProvider(credentials, config, tableName, sp.GetService())); + 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) { - options.Services.AddTransient(sp => new DynamoDbProvisioner(credentials, config, tablePrefix, sp.GetService())); - options.UsePersistence(sp => new DynamoPersistenceProvider(credentials, config, sp.GetService(), tablePrefix, sp.GetService())); + 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) { - options.Services.AddTransient(sp => new KinesisTracker(credentials, region, "workflowcore_kinesis", sp.GetService())); - options.Services.AddTransient(sp => new KinesisStreamConsumer(credentials, region, sp.GetService(), sp.GetService(), sp.GetService())); - options.UseEventHub(sp => new KinesisProvider(credentials, region, appName, streamName, sp.GetService(), sp.GetService())); + 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 index a78b006c0..887d11a7a 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoDbProvisioner.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoDbProvisioner.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using WorkflowCore.Providers.AWS.Interface; @@ -16,10 +15,10 @@ public class DynamoDbProvisioner : IDynamoDbProvisioner private readonly IAmazonDynamoDB _client; private readonly string _tablePrefix; - public DynamoDbProvisioner(AWSCredentials credentials, AmazonDynamoDBConfig config, string tablePrefix, ILoggerFactory logFactory) + public DynamoDbProvisioner(AmazonDynamoDBClient dynamoDBClient, string tablePrefix, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(); - _client = new AmazonDynamoDBClient(credentials, config); + _client = dynamoDBClient; _tablePrefix = tablePrefix; } @@ -33,10 +32,10 @@ public Task ProvisionTables() private async Task CreateWorkflowTable() { - var runnableIndex = new GlobalSecondaryIndex() + var runnableIndex = new GlobalSecondaryIndex { IndexName = "ix_runnable", - KeySchema = new List() + KeySchema = new List { { new KeySchemaElement @@ -53,33 +52,33 @@ private async Task CreateWorkflowTable() } } }, - Projection = new Projection() + Projection = new Projection { ProjectionType = ProjectionType.KEYS_ONLY }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 } }; - var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.WORKFLOW_TABLE}", new List() + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.WORKFLOW_TABLE}", new List { new KeySchemaElement("id", KeyType.HASH) }) { - AttributeDefinitions = new List() + AttributeDefinitions = new List { new AttributeDefinition("id", ScalarAttributeType.S), new AttributeDefinition("runnable", ScalarAttributeType.N), new AttributeDefinition("next_execution", ScalarAttributeType.N), }, - GlobalSecondaryIndexes = new List() + GlobalSecondaryIndexes = new List { runnableIndex }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 @@ -91,10 +90,10 @@ private async Task CreateWorkflowTable() private async Task CreateSubscriptionTable() { - var slugIndex = new GlobalSecondaryIndex() + var slugIndex = new GlobalSecondaryIndex { IndexName = "ix_slug", - KeySchema = new List() + KeySchema = new List { { new KeySchemaElement @@ -111,33 +110,33 @@ private async Task CreateSubscriptionTable() } } }, - Projection = new Projection() + Projection = new Projection { ProjectionType = ProjectionType.ALL }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 } }; - var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.SUBCRIPTION_TABLE}", new List() + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.SUBCRIPTION_TABLE}", new List { new KeySchemaElement("id", KeyType.HASH) }) { - AttributeDefinitions = new List() + AttributeDefinitions = new List { new AttributeDefinition("id", ScalarAttributeType.S), new AttributeDefinition("event_slug", ScalarAttributeType.S), new AttributeDefinition("subscribe_as_of", ScalarAttributeType.N) }, - GlobalSecondaryIndexes = new List() + GlobalSecondaryIndexes = new List { slugIndex }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 @@ -149,10 +148,10 @@ private async Task CreateSubscriptionTable() private async Task CreateEventTable() { - var slugIndex = new GlobalSecondaryIndex() + var slugIndex = new GlobalSecondaryIndex { IndexName = "ix_slug", - KeySchema = new List() + KeySchema = new List { { new KeySchemaElement @@ -169,21 +168,21 @@ private async Task CreateEventTable() } } }, - Projection = new Projection() + Projection = new Projection { ProjectionType = ProjectionType.KEYS_ONLY }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 } }; - var processedIndex = new GlobalSecondaryIndex() + var processedIndex = new GlobalSecondaryIndex { IndexName = "ix_not_processed", - KeySchema = new List() + KeySchema = new List { { new KeySchemaElement @@ -200,35 +199,35 @@ private async Task CreateEventTable() } } }, - Projection = new Projection() + Projection = new Projection { ProjectionType = ProjectionType.KEYS_ONLY }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 } }; - var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.EVENT_TABLE}", new List() + var createRequest = new CreateTableRequest($"{_tablePrefix}-{DynamoPersistenceProvider.EVENT_TABLE}", new List { new KeySchemaElement("id", KeyType.HASH) }) { - AttributeDefinitions = new List() + 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() + GlobalSecondaryIndexes = new List { slugIndex, processedIndex }, - ProvisionedThroughput = new ProvisionedThroughput() + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs index 2a7c2f7bd..0863f1393 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoLockProvider.cs @@ -23,39 +23,41 @@ public class DynamoLockProvider : IDistributedLockProvider private Task _heartbeatTask; private CancellationTokenSource _cancellationTokenSource; private readonly AutoResetEvent _mutex = new AutoResetEvent(true); + private readonly IDateTimeProvider _dateTimeProvider; - public DynamoLockProvider(AWSCredentials credentials, AmazonDynamoDBConfig config, string tableName, ILoggerFactory logFactory) + public DynamoLockProvider(AmazonDynamoDBClient dynamoDBClient, string tableName, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) { _logger = logFactory.CreateLogger(); - _client = new AmazonDynamoDBClient(credentials, config); + _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() + var req = new PutItemRequest { TableName = _tableName, Item = new Dictionary { { "id", new AttributeValue(Id) }, { "lock_owner", new AttributeValue(_nodeId) }, - { "expires", new AttributeValue() + { "expires", new AttributeValue { - N = Convert.ToString(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds() + _ttl) + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _ttl) } } }, ConditionExpression = "attribute_not_exists(id) OR (expires < :expired)", ExpressionAttributeValues = new Dictionary { - { ":expired", new AttributeValue() + { ":expired", new AttributeValue { - N = Convert.ToString(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds() + _jitter) + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _jitter) } } } @@ -89,7 +91,7 @@ public async Task ReleaseLock(string Id) try { - var req = new DeleteItemRequest() + var req = new DeleteItemRequest { TableName = _tableName, Key = new Dictionary @@ -152,9 +154,9 @@ private async void SendHeartbeat() { { "id", new AttributeValue(item) }, { "lock_owner", new AttributeValue(_nodeId) }, - { "expires", new AttributeValue() + { "expires", new AttributeValue { - N = Convert.ToString(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds() + _ttl) + N = Convert.ToString(new DateTimeOffset(_dateTimeProvider.UtcNow).ToUnixTimeMilliseconds() + _ttl) } } }, @@ -202,12 +204,12 @@ private async Task EnsureTable() private async Task CreateTable() { - var createRequest = new CreateTableRequest(_tableName, new List() + var createRequest = new CreateTableRequest(_tableName, new List { new KeySchemaElement("id", KeyType.HASH) }) { - AttributeDefinitions = new List() + AttributeDefinitions = new List { new AttributeDefinition("id", ScalarAttributeType.S) }, diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs index afc531b7a..01beaaabe 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/DynamoPersistenceProvider.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using WorkflowCore.Providers.AWS.Interface; using WorkflowCore.Interface; using WorkflowCore.Models; +using System.Threading; namespace WorkflowCore.Providers.AWS.Services { @@ -24,47 +24,86 @@ public class DynamoPersistenceProvider : IPersistenceProvider public const string SUBCRIPTION_TABLE = "subscriptions"; public const string EVENT_TABLE = "events"; - public DynamoPersistenceProvider(AWSCredentials credentials, AmazonDynamoDBConfig config, IDynamoDbProvisioner provisioner, string tablePrefix, ILoggerFactory logFactory) + public bool SupportsScheduledCommands => false; + + public DynamoPersistenceProvider(AmazonDynamoDBClient dynamoDBClient, IDynamoDbProvisioner provisioner, string tablePrefix, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(); - _client = new AmazonDynamoDBClient(credentials, config); + _client = dynamoDBClient; _tablePrefix = tablePrefix; _provisioner = provisioner; } - public async Task CreateNewWorkflow(WorkflowInstance workflow) + public async Task CreateNewWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { workflow.Id = Guid.NewGuid().ToString(); - var req = new PutItemRequest() + var req = new PutItemRequest { TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", Item = workflow.ToDynamoMap(), ConditionExpression = "attribute_not_exists(id)" }; - var response = await _client.PutItemAsync(req); + var _ = await _client.PutItemAsync(req, cancellationToken); return workflow.Id; } - public async Task PersistWorkflow(WorkflowInstance workflow) + public async Task PersistWorkflow(WorkflowInstance workflow, CancellationToken cancellationToken = default) { - var request = new PutItemRequest() + var request = new PutItemRequest { TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", Item = workflow.ToDynamoMap() }; - var response = await _client.PutItemAsync(request); + 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) + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken cancellationToken = default) { var result = new List(); var now = asAt.ToUniversalTime().Ticks; - var request = new QueryRequest() + var request = new QueryRequest { TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", IndexName = "ix_runnable", @@ -73,13 +112,13 @@ public async Task> GetRunnableInstances(DateTime asAt) ExpressionAttributeValues = new Dictionary { { - ":r", new AttributeValue() + ":r", new AttributeValue { N = 1.ToString() } }, { - ":effective_date", new AttributeValue() + ":effective_date", new AttributeValue { N = Convert.ToString(now) } @@ -88,7 +127,7 @@ public async Task> GetRunnableInstances(DateTime asAt) ScanIndexForward = true }; - var response = await _client.QueryAsync(request); + var response = await _client.QueryAsync(request, cancellationToken); foreach (var item in response.Items) { @@ -103,9 +142,9 @@ public Task> GetWorkflowInstances(WorkflowStatus? throw new NotImplementedException(); } - public async Task GetWorkflowInstance(string Id) + public async Task GetWorkflowInstance(string Id, CancellationToken cancellationToken = default) { - var req = new GetItemRequest() + var req = new GetItemRequest { TableName = $"{_tablePrefix}-{WORKFLOW_TABLE}", Key = new Dictionary @@ -113,22 +152,22 @@ public async Task GetWorkflowInstance(string Id) { "id", new AttributeValue(Id) } } }; - var response = await _client.GetItemAsync(req); + var response = await _client.GetItemAsync(req, cancellationToken); return response.Item.ToWorkflowInstance(); } - public async Task> GetWorkflowInstances(IEnumerable ids) + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken cancellationToken = default) { if (ids == null) { return new List(); } - var keys = new KeysAndAttributes() { Keys = new List>() }; + var keys = new KeysAndAttributes { Keys = new List>() }; foreach (var id in ids) { - var key = new Dictionary() + var key = new Dictionary { { "id", new AttributeValue { S = id } @@ -139,7 +178,7 @@ public async Task> GetWorkflowInstances(IEnumerabl var request = new BatchGetItemRequest { - RequestItems = new Dictionary() + RequestItems = new Dictionary { { $"{_tablePrefix}-{WORKFLOW_TABLE}", keys @@ -151,7 +190,7 @@ public async Task> GetWorkflowInstances(IEnumerabl BatchGetItemResponse response; do { - response = await _client.BatchGetItemAsync(request); + response = await _client.BatchGetItemAsync(request, cancellationToken); foreach (var tableResponse in response.Responses) result.AddRange(tableResponse.Value); @@ -161,28 +200,28 @@ public async Task> GetWorkflowInstances(IEnumerabl return result.Select(i => i.ToWorkflowInstance()); } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken cancellationToken = default) { subscription.Id = Guid.NewGuid().ToString(); - var req = new PutItemRequest() + var req = new PutItemRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", Item = subscription.ToDynamoMap(), ConditionExpression = "attribute_not_exists(id)" }; - var response = await _client.PutItemAsync(req); + var response = await _client.PutItemAsync(req, cancellationToken); return subscription.Id; } - public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf) + 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() + var request = new QueryRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", IndexName = "ix_slug", @@ -194,7 +233,7 @@ public async Task> GetSubscriptions(string eventN ":slug", new AttributeValue($"{eventName}:{eventKey}") }, { - ":as_of", new AttributeValue() + ":as_of", new AttributeValue { N = Convert.ToString(asOfTicks) } @@ -203,7 +242,7 @@ public async Task> GetSubscriptions(string eventN ScanIndexForward = true }; - var response = await _client.QueryAsync(request); + var response = await _client.QueryAsync(request, cancellationToken); foreach (var item in response.Items) { @@ -213,9 +252,9 @@ public async Task> GetSubscriptions(string eventN return result; } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) { - var request = new DeleteItemRequest() + var request = new DeleteItemRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", Key = new Dictionary @@ -223,28 +262,28 @@ public async Task TerminateSubscription(string eventSubscriptionId) { "id", new AttributeValue(eventSubscriptionId) } } }; - await _client.DeleteItemAsync(request); + await _client.DeleteItemAsync(request, cancellationToken); } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken cancellationToken = default) { newEvent.Id = Guid.NewGuid().ToString(); - var req = new PutItemRequest() + var req = new PutItemRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", Item = newEvent.ToDynamoMap(), ConditionExpression = "attribute_not_exists(id)" }; - var response = await _client.PutItemAsync(req); + var _ = await _client.PutItemAsync(req, cancellationToken); return newEvent.Id; } - public async Task GetEvent(string id) + public async Task GetEvent(string id, CancellationToken cancellationToken = default) { - var req = new GetItemRequest() + var req = new GetItemRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", Key = new Dictionary @@ -252,17 +291,17 @@ public async Task GetEvent(string id) { "id", new AttributeValue(id) } } }; - var response = await _client.GetItemAsync(req); + var response = await _client.GetItemAsync(req, cancellationToken); return response.Item.ToEvent(); } - public async Task> GetRunnableEvents(DateTime asAt) + public async Task> GetRunnableEvents(DateTime asAt, CancellationToken cancellationToken = default) { var result = new List(); var now = asAt.ToUniversalTime().Ticks; - var request = new QueryRequest() + var request = new QueryRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", IndexName = "ix_not_processed", @@ -270,9 +309,9 @@ public async Task> GetRunnableEvents(DateTime asAt) KeyConditionExpression = "not_processed = :n and event_time <= :effectiveDate", ExpressionAttributeValues = new Dictionary { - { ":n" , new AttributeValue() { N = 1.ToString() } }, + { ":n" , new AttributeValue { N = 1.ToString() } }, { - ":effectiveDate", new AttributeValue() + ":effectiveDate", new AttributeValue { N = Convert.ToString(now) } @@ -281,7 +320,7 @@ public async Task> GetRunnableEvents(DateTime asAt) ScanIndexForward = true }; - var response = await _client.QueryAsync(request); + var response = await _client.QueryAsync(request, cancellationToken); foreach (var item in response.Items) { @@ -291,12 +330,12 @@ public async Task> GetRunnableEvents(DateTime asAt) return result; } - public async Task> GetEvents(string eventName, string eventKey, DateTime asOf) + 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() + var request = new QueryRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", IndexName = "ix_slug", @@ -308,7 +347,7 @@ public async Task> GetEvents(string eventName, string eventK ":slug", new AttributeValue($"{eventName}:{eventKey}") }, { - ":effective_date", new AttributeValue() + ":effective_date", new AttributeValue { N = Convert.ToString(asOfTicks) } @@ -317,7 +356,7 @@ public async Task> GetEvents(string eventName, string eventK ScanIndexForward = true }; - var response = await _client.QueryAsync(request); + var response = await _client.QueryAsync(request, cancellationToken); foreach (var item in response.Items) { @@ -327,9 +366,9 @@ public async Task> GetEvents(string eventName, string eventK return result; } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) { - var request = new UpdateItemRequest() + var request = new UpdateItemRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", Key = new Dictionary @@ -338,12 +377,12 @@ public async Task MarkEventProcessed(string id) }, UpdateExpression = "REMOVE not_processed" }; - await _client.UpdateItemAsync(request); + await _client.UpdateItemAsync(request, cancellationToken); } - public async Task MarkEventUnprocessed(string id) + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) { - var request = new UpdateItemRequest() + var request = new UpdateItemRequest { TableName = $"{_tablePrefix}-{EVENT_TABLE}", Key = new Dictionary @@ -351,15 +390,15 @@ public async Task MarkEventUnprocessed(string id) { "id", new AttributeValue(id) } }, UpdateExpression = "ADD not_processed = :n", - ExpressionAttributeValues = new Dictionary() + ExpressionAttributeValues = new Dictionary { - { ":n" , new AttributeValue() { N = 1.ToString() } } + { ":n" , new AttributeValue { N = 1.ToString() } } } }; - await _client.UpdateItemAsync(request); + await _client.UpdateItemAsync(request, cancellationToken); } - public Task PersistErrors(IEnumerable errors) + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) { //TODO return Task.CompletedTask; @@ -370,9 +409,9 @@ public void EnsureStoreExists() _provisioner.ProvisionTables().Wait(); } - public async Task GetSubscription(string eventSubscriptionId) + public async Task GetSubscription(string eventSubscriptionId, CancellationToken cancellationToken = default) { - var req = new GetItemRequest() + var req = new GetItemRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", Key = new Dictionary @@ -380,17 +419,17 @@ public async Task GetSubscription(string eventSubscriptionId) { "id", new AttributeValue(eventSubscriptionId) } } }; - var response = await _client.GetItemAsync(req); + var response = await _client.GetItemAsync(req, cancellationToken); return response.Item.ToEventSubscription(); } - public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf) + 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() + var request = new QueryRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", IndexName = "ix_slug", @@ -404,7 +443,7 @@ public async Task GetFirstOpenSubscription(string eventName, ":slug", new AttributeValue($"{eventName}:{eventKey}") }, { - ":as_of", new AttributeValue() + ":as_of", new AttributeValue { N = Convert.ToString(asOfTicks) } @@ -413,7 +452,7 @@ public async Task GetFirstOpenSubscription(string eventName, ScanIndexForward = true }; - var response = await _client.QueryAsync(request); + var response = await _client.QueryAsync(request, cancellationToken); foreach (var item in response.Items) result.Add(item.ToEventSubscription()); @@ -421,9 +460,9 @@ public async Task GetFirstOpenSubscription(string eventName, return result.FirstOrDefault(); } - public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry) + public async Task SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry, CancellationToken cancellationToken = default) { - var request = new UpdateItemRequest() + var request = new UpdateItemRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", Key = new Dictionary @@ -432,16 +471,16 @@ public async Task SetSubscriptionToken(string eventSubscriptionId, string }, 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() + 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() } } + { ":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); + await _client.UpdateItemAsync(request, cancellationToken); return true; } catch (ConditionalCheckFailedException) @@ -450,9 +489,9 @@ public async Task SetSubscriptionToken(string eventSubscriptionId, string } } - public async Task ClearSubscriptionToken(string eventSubscriptionId, string token) + public async Task ClearSubscriptionToken(string eventSubscriptionId, string token, CancellationToken cancellationToken = default) { - var request = new UpdateItemRequest() + var request = new UpdateItemRequest { TableName = $"{_tablePrefix}-{SUBCRIPTION_TABLE}", Key = new Dictionary @@ -461,13 +500,23 @@ public async Task ClearSubscriptionToken(string eventSubscriptionId, string toke }, UpdateExpression = "REMOVE external_token, external_worker_id, external_token_expiry", ConditionExpression = "external_token = :external_token", - ExpressionAttributeValues = new Dictionary() + ExpressionAttributeValues = new Dictionary { - { ":external_token" , new AttributeValue() { S = token } }, + { ":external_token" , new AttributeValue { S = token } }, } }; - await _client.UpdateItemAsync(request); + 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 index 7db21aab7..d8aa519bd 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisProvider.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading.Tasks; using Amazon; using Amazon.Kinesis; @@ -27,7 +26,7 @@ public class KinesisProvider : ILifeCycleEventHub private readonly int _defaultShardCount = 1; private bool _started = false; - public KinesisProvider(AWSCredentials credentials, RegionEndpoint region, string appName, string streamName, IKinesisStreamConsumer consumer, ILoggerFactory logFactory) + public KinesisProvider(AmazonKinesisClient kinesisClient, string appName, string streamName, IKinesisStreamConsumer consumer, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(GetType()); _appName = appName; @@ -35,7 +34,7 @@ public KinesisProvider(AWSCredentials credentials, RegionEndpoint region, string _consumer = consumer; _serializer = new JsonSerializer(); _serializer.TypeNameHandling = TypeNameHandling.All; - _client = new AmazonKinesisClient(credentials, region); + _client = kinesisClient; } public async Task PublishNotification(LifeCycleEvent evt) @@ -46,7 +45,7 @@ public async Task PublishNotification(LifeCycleEvent evt) _serializer.Serialize(writer, evt); writer.Flush(); - var response = await _client.PutRecordAsync(new PutRecordRequest() + var response = await _client.PutRecordAsync(new PutRecordRequest { StreamName = _streamName, PartitionKey = evt.WorkflowInstanceId, @@ -93,7 +92,7 @@ private async Task EnsureStream() { try { - await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest() + await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest { StreamName = _streamName }); @@ -106,7 +105,7 @@ await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest() private async Task CreateStream() { - await _client.CreateStreamAsync(new CreateStreamRequest() + await _client.CreateStreamAsync(new CreateStreamRequest { StreamName = _streamName, ShardCount = _defaultShardCount @@ -117,7 +116,7 @@ await _client.CreateStreamAsync(new CreateStreamRequest() { i++; await Task.Delay(3000); - var poll = await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest() + var poll = await _client.DescribeStreamSummaryAsync(new DescribeStreamSummaryRequest { StreamName = _streamName }); diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs index c22e40fce..5c89f7837 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisStreamConsumer.cs @@ -23,27 +23,29 @@ public class KinesisStreamConsumer : IKinesisStreamConsumer, IDisposable private readonly Task _processTask; private readonly int _batchSize = 100; private ICollection _subscribers = new HashSet(); + private readonly IDateTimeProvider _dateTimeProvider; - public KinesisStreamConsumer(AWSCredentials credentials, RegionEndpoint region, IKinesisTracker tracker, IDistributedLockProvider lockManager, ILoggerFactory logFactory) + public KinesisStreamConsumer(AmazonKinesisClient kinesisClient, IKinesisTracker tracker, IDistributedLockProvider lockManager, ILoggerFactory logFactory, IDateTimeProvider dateTimeProvider) { _logger = logFactory.CreateLogger(GetType()); _tracker = tracker; _lockManager = lockManager; - _client = new AmazonKinesisClient(credentials, region); + _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() + var shards = await _client.ListShardsAsync(new ListShardsRequest { StreamName = stream }); foreach (var shard in shards.Shards) { - _subscribers.Add(new ShardSubscription() + _subscribers.Add(new ShardSubscription { AppName = appName, Stream = stream, @@ -59,7 +61,7 @@ private async void Process() { try { - var todo = _subscribers.Where(x => x.Snooze < DateTime.Now).ToList(); + 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}", @@ -71,7 +73,7 @@ private async void Process() var records = await GetBatch(sub); if (records.Records.Count == 0) - sub.Snooze = DateTime.Now.AddSeconds(5); + sub.Snooze = _dateTimeProvider.Now.AddSeconds(5); var lastSequence = string.Empty; @@ -115,7 +117,7 @@ private async Task GetBatch(ShardSubscription sub) if (iterator == null) { - var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest() + var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest { ShardId = sub.Shard.ShardId, StreamName = sub.Stream, @@ -127,7 +129,7 @@ private async Task GetBatch(ShardSubscription sub) try { - var result = await _client.GetRecordsAsync(new GetRecordsRequest() + var result = await _client.GetRecordsAsync(new GetRecordsRequest { ShardIterator = iterator, Limit = _batchSize @@ -138,7 +140,7 @@ private async Task GetBatch(ShardSubscription sub) catch (ExpiredIteratorException) { var lastSequence = await _tracker.GetNextLastSequenceNumber(sub.AppName, sub.Stream, sub.Shard.ShardId); - var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest() + var iterResp = await _client.GetShardIteratorAsync(new GetShardIteratorRequest { ShardId = sub.Shard.ShardId, StreamName = sub.Stream, @@ -147,7 +149,7 @@ private async Task GetBatch(ShardSubscription sub) }); iterator = iterResp.ShardIterator; - var result = await _client.GetRecordsAsync(new GetRecordsRequest() + var result = await _client.GetRecordsAsync(new GetRecordsRequest { ShardIterator = iterator, Limit = _batchSize diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs index 5f4abc2e1..d7c028c46 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/KinesisTracker.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Amazon; using Amazon.DynamoDBv2; @@ -19,10 +17,10 @@ public class KinesisTracker : IKinesisTracker private readonly string _tableName; private bool _tableConfirmed = false; - public KinesisTracker(AWSCredentials credentials, RegionEndpoint region, string tableName, ILoggerFactory logFactory) + public KinesisTracker(AmazonDynamoDBClient client, string tableName, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(GetType()); - _client = new AmazonDynamoDBClient(credentials, region); + _client = client; _tableName = tableName; } @@ -31,7 +29,7 @@ public async Task GetNextShardIterator(string app, string stream, string if (!_tableConfirmed) await EnsureTable(); - var response = await _client.GetItemAsync(new GetItemRequest() + var response = await _client.GetItemAsync(new GetItemRequest { TableName = _tableName, Key = new Dictionary @@ -51,7 +49,7 @@ public async Task GetNextLastSequenceNumber(string app, string stream, s if (!_tableConfirmed) await EnsureTable(); - var response = await _client.GetItemAsync(new GetItemRequest() + var response = await _client.GetItemAsync(new GetItemRequest { TableName = _tableName, Key = new Dictionary @@ -71,7 +69,7 @@ public async Task IncrementShardIterator(string app, string stream, string shard if (!_tableConfirmed) await EnsureTable(); - await _client.UpdateItemAsync(new UpdateItemRequest() + await _client.UpdateItemAsync(new UpdateItemRequest { TableName = _tableName, Key = new Dictionary @@ -79,7 +77,7 @@ await _client.UpdateItemAsync(new UpdateItemRequest() {"id", new AttributeValue(FormatId(app, stream, shard))} }, UpdateExpression = "SET next_iterator = :n", - ExpressionAttributeValues = new Dictionary() + ExpressionAttributeValues = new Dictionary { { ":n" , new AttributeValue(iterator) } } @@ -91,7 +89,7 @@ public async Task IncrementShardIteratorAndSequence(string app, string stream, s if (!_tableConfirmed) await EnsureTable(); - await _client.PutItemAsync(new PutItemRequest() + await _client.PutItemAsync(new PutItemRequest { TableName = _tableName, Item = new Dictionary @@ -118,12 +116,12 @@ private async Task EnsureTable() private async Task CreateTable() { - var createRequest = new CreateTableRequest(_tableName, new List() + var createRequest = new CreateTableRequest(_tableName, new List { new KeySchemaElement("id", KeyType.HASH) }) { - AttributeDefinitions = new List() + AttributeDefinitions = new List { new AttributeDefinition("id", ScalarAttributeType.S) }, diff --git a/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs b/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs index 6daed010b..c15fb02af 100644 --- a/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs +++ b/src/providers/WorkflowCore.Providers.AWS/Services/SQSQueueProvider.cs @@ -17,13 +17,15 @@ public class SQSQueueProvider : IQueueProvider private readonly ILogger _logger; private readonly IAmazonSQS _client; private readonly Dictionary _queues = new Dictionary(); + private readonly string _queuesPrefix; public bool IsDequeueBlocking => true; - public SQSQueueProvider(AWSCredentials credentials, AmazonSQSConfig config, ILoggerFactory logFactory) + public SQSQueueProvider(AmazonSQSClient sqsClient, ILoggerFactory logFactory, string queuesPrefix) { _logger = logFactory.CreateLogger(); - _client = new AmazonSQSClient(credentials, config); + _client = sqsClient; + _queuesPrefix = queuesPrefix; } public async Task QueueWork(string id, QueueType queue) @@ -54,9 +56,9 @@ public async Task DequeueWork(QueueType queue, CancellationToken cancell public async Task Start() { - var workflowQueue = await _client.CreateQueueAsync(new CreateQueueRequest("workflowcore-workflows")); - var eventQueue = await _client.CreateQueueAsync(new CreateQueueRequest("workflowcore-events")); - var indexQueue = await _client.CreateQueueAsync(new CreateQueueRequest("workflowcore-index")); + 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; diff --git a/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj b/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj index d8470de03..4ea4a1ed8 100644 --- a/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj +++ b/src/providers/WorkflowCore.Providers.AWS/WorkflowCore.Providers.AWS.csproj @@ -11,17 +11,14 @@ https://github.com/danielgerlag/workflow-core https://github.com/danielgerlag/workflow-core.git git - 3.0.1 - 3.0.1.0 - 3.0.1 - - - + + + - + 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..587378e15 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Models/ControlledLock.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.WindowsAzure.Storage.Blob; +using Azure.Storage.Blobs.Specialized; namespace WorkflowCore.Providers.Azure.Models { @@ -9,9 +6,9 @@ class ControlledLock { public string Id { get; set; } public string LeaseId { get; set; } - public CloudBlockBlob Blob { get; set; } + public BlockBlobClient Blob { get; set; } - public ControlledLock(string id, string leaseId, CloudBlockBlob blob) + public ControlledLock(string id, string leaseId, BlockBlobClient blob) { Id = id; LeaseId = leaseId; 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 a6641c564..10c17bfbe 100644 --- a/src/providers/WorkflowCore.Providers.Azure/README.md +++ b/src/providers/WorkflowCore.Providers.Azure/README.md @@ -3,6 +3,7 @@ * 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. @@ -30,5 +31,6 @@ 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 5abb111fc..e08d0445f 100644 --- a/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Providers.Azure/ServiceCollectionExtensions.cs @@ -1,5 +1,10 @@ -using Microsoft.Extensions.Logging; +using System; +using Azure.Core; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Logging; +using WorkflowCore.Interface; using WorkflowCore.Models; +using WorkflowCore.Providers.Azure.Interface; using WorkflowCore.Providers.Azure.Services; namespace Microsoft.Extensions.DependencyInjection @@ -13,6 +18,13 @@ public static WorkflowOptions UseAzureSynchronization(this WorkflowOptions optio return options; } + public static WorkflowOptions UseAzureSynchronization(this WorkflowOptions options, Uri blobEndpoint, Uri queueEndpoint, TokenCredential tokenCredential) + { + options.UseQueueProvider(sp => new AzureStorageQueueProvider(queueEndpoint, tokenCredential, sp.GetService())); + options.UseDistributedLockManager(sp => new AzureLockManager(blobEndpoint, tokenCredential, sp.GetService())); + return options; + } + public static WorkflowOptions UseAzureServiceBusEventHub( this WorkflowOptions options, string connectionString, @@ -24,5 +36,75 @@ public static WorkflowOptions UseAzureServiceBusEventHub( return options; } + + public static WorkflowOptions UseAzureServiceBusEventHub( + this WorkflowOptions options, + string fullyQualifiedNamespace, + TokenCredential tokenCredential, + string topicName, + string subscriptionName) + { + options.UseEventHub(sp => new ServiceBusLifeCycleEventHub( + fullyQualifiedNamespace, tokenCredential, topicName, subscriptionName, sp.GetService())); + + return options; + } + + public static WorkflowOptions UseCosmosDbPersistence( + this WorkflowOptions options, + string connectionString, + string databaseId, + CosmosDbStorageOptions cosmosDbStorageOptions = null, + CosmosClientOptions clientOptions = null) + { + if (cosmosDbStorageOptions == null) + { + cosmosDbStorageOptions = new CosmosDbStorageOptions(); + } + + options.Services.AddSingleton(sp => new CosmosClientFactory(connectionString, clientOptions)); + 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; + } + + public static WorkflowOptions UseCosmosDbPersistence( + this WorkflowOptions options, + CosmosClient client, + string databaseId, + CosmosDbStorageOptions cosmosDbStorageOptions = null, + CosmosClientOptions clientOptions = null) + { + if (cosmosDbStorageOptions == null) + { + cosmosDbStorageOptions = new CosmosDbStorageOptions(); + } + + options.Services.AddSingleton(sp => new CosmosClientFactory(client)); + 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; + } + + public static WorkflowOptions UseCosmosDbPersistence( + this WorkflowOptions options, + string accountEndpoint, + TokenCredential tokenCredential, + string databaseId, + CosmosDbStorageOptions cosmosDbStorageOptions = null) + { + if (cosmosDbStorageOptions == null) + { + cosmosDbStorageOptions = new CosmosDbStorageOptions(); + } + + options.Services.AddSingleton(sp => new CosmosClientFactory(accountEndpoint, tokenCredential)); + 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 3d718b6e9..6780a77ad 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Services/AzureLockManager.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Azure.Core; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Specialized; using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; using WorkflowCore.Interface; using WorkflowCore.Providers.Azure.Models; @@ -14,11 +15,11 @@ namespace WorkflowCore.Providers.Azure.Services { public class AzureLockManager: IDistributedLockProvider { - private readonly CloudBlobClient _client; + private readonly BlobServiceClient _client; private readonly ILogger _logger; private readonly List _locks = new List(); private readonly AutoResetEvent _mutex = new AutoResetEvent(true); - private CloudBlobContainer _container; + private BlobContainerClient _container; private Timer _renewTimer; private TimeSpan LockTimeout => TimeSpan.FromMinutes(1); private TimeSpan RenewInterval => TimeSpan.FromSeconds(45); @@ -26,26 +27,31 @@ public class AzureLockManager: IDistributedLockProvider public AzureLockManager(string connectionString, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(); - var account = CloudStorageAccount.Parse(connectionString); - _client = account.CreateCloudBlobClient(); + _client = new BlobServiceClient(connectionString); + } + + public AzureLockManager(Uri blobEndpoint, TokenCredential tokenCredential, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(); + _client = new BlobServiceClient(blobEndpoint, tokenCredential); } public async Task AcquireLock(string Id, CancellationToken cancellationToken) { - var blob = _container.GetBlockBlobReference(Id); + var blob = _container.GetBlockBlobClient(Id); if (!await blob.ExistsAsync()) - await blob.UploadTextAsync(string.Empty); + await blob.UploadAsync(new MemoryStream()); if (_mutex.WaitOne()) { try { - var leaseId = await blob.AcquireLeaseAsync(LockTimeout); - _locks.Add(new ControlledLock(Id, leaseId, blob)); + var lease = await blob.GetBlobLeaseClient().AcquireAsync(LockTimeout); + _locks.Add(new ControlledLock(Id, lease.Value.LeaseId, blob)); return true; } - catch (StorageException ex) + catch (Exception ex) { _logger.LogDebug($"Failed to acquire lock {Id} - {ex.Message}"); return false; @@ -70,7 +76,7 @@ public async Task ReleaseLock(string Id) { try { - await entry.Blob.ReleaseLeaseAsync(AccessCondition.GenerateLeaseCondition(entry.LeaseId)); + await entry.Blob.GetBlobLeaseClient(entry.LeaseId).ReleaseAsync(); } catch (Exception ex) { @@ -88,18 +94,20 @@ public async Task ReleaseLock(string Id) public async Task Start() { - _container = _client.GetContainerReference("workflowcore-locks"); + _container = _client.GetBlobContainerClient("workflowcore-locks"); await _container.CreateIfNotExistsAsync(); _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) @@ -127,7 +135,7 @@ private async Task RenewLock(ControlledLock entry) { try { - await entry.Blob.RenewLeaseAsync(AccessCondition.GenerateLeaseCondition(entry.LeaseId)); + await entry.Blob.GetBlobLeaseClient(entry.LeaseId).RenewAsync(); } catch (Exception ex) { diff --git a/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs b/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs index 5b104ab30..3aea1d397 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Services/AzureStorageQueueProvider.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Azure.Core; +using Azure.Storage.Queues; using Microsoft.Extensions.Logging; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Queue; using WorkflowCore.Interface; namespace WorkflowCore.Providers.Azure.Services @@ -14,41 +13,44 @@ public class AzureStorageQueueProvider : IQueueProvider { private readonly ILogger _logger; - private readonly Dictionary _queues = new Dictionary(); + private readonly Dictionary _queues = new Dictionary(); public bool IsDequeueBlocking => false; public AzureStorageQueueProvider(string connectionString, ILoggerFactory logFactory) { _logger = logFactory.CreateLogger(); - var account = CloudStorageAccount.Parse(connectionString); - var client = account.CreateCloudQueueClient(); + var client = new QueueServiceClient(connectionString); - _queues[QueueType.Workflow] = client.GetQueueReference("workflowcore-workflows"); - _queues[QueueType.Event] = client.GetQueueReference("workflowcore-events"); - _queues[QueueType.Index] = client.GetQueueReference("workflowcore-index"); + _queues[QueueType.Workflow] = client.GetQueueClient("workflowcore-workflows"); + _queues[QueueType.Event] = client.GetQueueClient("workflowcore-events"); + _queues[QueueType.Index] = client.GetQueueClient("workflowcore-index"); + } + + public AzureStorageQueueProvider(Uri queueEndpoint, TokenCredential tokenCredential, ILoggerFactory logFactory) + { + _logger = logFactory.CreateLogger(); + var client = new QueueServiceClient(queueEndpoint, tokenCredential); + + _queues[QueueType.Workflow] = client.GetQueueClient("workflowcore-workflows"); + _queues[QueueType.Event] = client.GetQueueClient("workflowcore-events"); + _queues[QueueType.Index] = client.GetQueueClient("workflowcore-index"); } public async Task QueueWork(string id, QueueType queue) { - var msg = new CloudQueueMessage(id); - await _queues[queue].AddMessageAsync(msg); + await _queues[queue].SendMessageAsync(id); } public async Task DequeueWork(QueueType queue, CancellationToken cancellationToken) { - CloudQueue cloudQueue = _queues[queue]; - - if (cloudQueue == null) - return null; - - var msg = await cloudQueue.GetMessageAsync(); + var msg = await _queues[queue].ReceiveMessageAsync(); - if (msg == null) + if (msg == null || msg.Value == null) return null; - await cloudQueue.DeleteMessageAsync(msg); - return msg.AsString; + await _queues[queue].DeleteMessageAsync(msg.Value.MessageId, msg.Value.PopReceipt); + return msg.Value.Body.ToString(); } public async Task Start() 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..83f70f4d5 --- /dev/null +++ b/src/providers/WorkflowCore.Providers.Azure/Services/CosmosClientFactory.cs @@ -0,0 +1,59 @@ +using System; +using Azure.Core; +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, CosmosClientOptions clientOptions = null) + { + _client = new CosmosClient(connectionString, clientOptions); + } + + public CosmosClientFactory(CosmosClient client) + { + _client = client; + } + + public CosmosClientFactory(string accountEndpoint, TokenCredential tokenCredential) + { + _client = new CosmosClient(accountEndpoint, tokenCredential); + } + + 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 index 13ba2df55..f1ce10984 100644 --- a/src/providers/WorkflowCore.Providers.Azure/Services/ServiceBusLifeCycleEventHub.cs +++ b/src/providers/WorkflowCore.Providers.Azure/Services/ServiceBusLifeCycleEventHub.cs @@ -3,7 +3,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.ServiceBus; +using Azure.Core; +using Azure.Messaging.ServiceBus; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using WorkflowCore.Interface; @@ -13,17 +14,17 @@ 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, - }; + private readonly ServiceBusSender _sender; + private readonly ServiceBusReceiver _receiver; + private readonly ServiceBusProcessor _processor; + + private readonly ICollection> _subscribers = new HashSet>(); + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All, + ReferenceLoopHandling = ReferenceLoopHandling.Error, + }; public ServiceBusLifeCycleEventHub( string connectionString, @@ -31,21 +32,38 @@ public ServiceBusLifeCycleEventHub( string subscriptionName, ILoggerFactory logFactory) { - _subscriptionClient = new SubscriptionClient( - connectionString, topicName, subscriptionName); - _topicClient = new TopicClient(connectionString, topicName); + var client = new ServiceBusClient(connectionString); + _sender = client.CreateSender(topicName); + _receiver = client.CreateReceiver(topicName, subscriptionName); + _processor = client.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions + { + AutoCompleteMessages = false + }); _logger = logFactory.CreateLogger(GetType()); } - public async Task PublishNotification(LifeCycleEvent evt) + public ServiceBusLifeCycleEventHub( + string fullyQualifiedNamespace, + TokenCredential tokenCredential, + string topicName, + string subscriptionName, + ILoggerFactory logFactory) { - var payload = JsonConvert.SerializeObject(evt, _serializerSettings); - var message = new Message(Encoding.Default.GetBytes(payload)) + var client = new ServiceBusClient(fullyQualifiedNamespace, tokenCredential); + _sender = client.CreateSender(topicName); + _receiver = client.CreateReceiver(topicName, subscriptionName); + _processor = client.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions { - Label = evt.Reference - }; + AutoCompleteMessages = false + }); + _logger = logFactory.CreateLogger(GetType()); + } - await _topicClient.SendAsync(message); + public async Task PublishNotification(LifeCycleEvent evt) + { + var payload = JsonConvert.SerializeObject(evt, _serializerSettings); + var message = new ServiceBusMessage(payload); + await _sender.SendMessageAsync(message); } public void Subscribe(Action action) @@ -53,54 +71,41 @@ public void Subscribe(Action action) _subscribers.Add(action); } - public Task Start() + public async Task Start() { - var sessionHandlerOptions = new SessionHandlerOptions(ExceptionHandler) - { - MaxConcurrentSessions = 1, - AutoComplete = false - }; - - _subscriptionClient.RegisterSessionHandler( - MessageHandler, sessionHandlerOptions); - - return Task.CompletedTask; + _processor.ProcessErrorAsync += ExceptionHandler; + _processor.ProcessMessageAsync += MessageHandler; + await _processor.StartProcessingAsync(); } public async Task Stop() { - await _topicClient.CloseAsync(); - await _subscriptionClient.CloseAsync(); + await _sender.CloseAsync(); + await _receiver.CloseAsync(); + await _processor.CloseAsync(); } - private async Task MessageHandler( - IMessageSession messageSession, - Message message, - CancellationToken cancellationToken) + private async Task MessageHandler(ProcessMessageEventArgs args) { try { - var payload = Encoding.Default.GetString(message.Body); + var payload = args.Message.Body.ToString(); var evt = JsonConvert.DeserializeObject( payload, _serializerSettings); NotifySubscribers(evt); - await _subscriptionClient - .CompleteAsync(message.SystemProperties.LockToken) - .ConfigureAwait(false); + await _receiver.CompleteMessageAsync(args.Message); } catch { - await _subscriptionClient - .AbandonAsync(message.SystemProperties.LockToken); + await _receiver.AbandonMessageAsync(args.Message); } } - private Task ExceptionHandler(ExceptionReceivedEventArgs arg) + private Task ExceptionHandler(ProcessErrorEventArgs arg) { - _logger.LogWarning( - default, arg.Exception, "Error on receiving events"); + _logger.LogWarning(default, arg.Exception, "Error on receiving events"); return Task.CompletedTask; } 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 28a8e06f4..65517764c 100644 --- a/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj +++ b/src/providers/WorkflowCore.Providers.Azure/WorkflowCore.Providers.Azure.csproj @@ -7,20 +7,19 @@ - Provides distributed lock management on Workflow Core - Provides Queueing support on Workflow Core workflow workflowcore dlm - 2.0.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 - 2.0.0.0 - 2.0.0.0 - - + + + + diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs b/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs index 91c4838f7..1399bdd23 100644 --- a/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/Models/WorkflowSearchModel.cs @@ -39,7 +39,7 @@ public class WorkflowSearchModel public WorkflowSearchResult ToSearchResult() { - var result = new WorkflowSearchResult() + var result = new WorkflowSearchResult { Id = Id, CompleteTime = CompleteTime, @@ -88,7 +88,7 @@ public static WorkflowSearchModel FromWorkflowInstance(WorkflowInstance workflow { if (ep.Status == PointerStatus.Sleeping) { - result.SleepingSteps.Add(new StepInfo() + result.SleepingSteps.Add(new StepInfo { StepId = ep.StepId, Name = ep.StepName @@ -97,7 +97,7 @@ public static WorkflowSearchModel FromWorkflowInstance(WorkflowInstance workflow if (ep.Status == PointerStatus.WaitingForEvent) { - result.WaitingSteps.Add(new StepInfo() + result.WaitingSteps.Add(new StepInfo { StepId = ep.StepId, Name = ep.StepName @@ -106,7 +106,7 @@ public static WorkflowSearchModel FromWorkflowInstance(WorkflowInstance workflow if (ep.Status == PointerStatus.Failed) { - result.FailedSteps.Add(new StepInfo() + result.FailedSteps.Add(new StepInfo { StepId = ep.StepId, Name = ep.StepName diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs b/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs index 21e285207..8195bd53a 100644 --- a/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/Services/ElasticsearchIndexer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.Models; diff --git a/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj b/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj index e4263abaf..7bf5600b8 100644 --- a/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj +++ b/src/providers/WorkflowCore.Providers.Elasticsearch/WorkflowCore.Providers.Elasticsearch.csproj @@ -2,7 +2,6 @@ netstandard2.0 - 3.0.0 https://github.com/danielgerlag/workflow-core/blob/master/LICENSE.md https://github.com/danielgerlag/workflow-core https://github.com/danielgerlag/workflow-core.git @@ -14,7 +13,7 @@ - + diff --git a/src/providers/WorkflowCore.Providers.Redis/README.md b/src/providers/WorkflowCore.Providers.Redis/README.md index 9edc8f31e..a8a610298 100644 --- a/src/providers/WorkflowCore.Providers.Redis/README.md +++ b/src/providers/WorkflowCore.Providers.Redis/README.md @@ -35,6 +35,6 @@ 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") + cfg.UseRedisEventHub("localhost:6379", "channel-name"); }); ``` diff --git a/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs index a517690f2..27594cfac 100644 --- a/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.Providers.Redis/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Extensions.Logging; -using StackExchange.Redis; using WorkflowCore.Models; using WorkflowCore.Providers.Redis.Services; @@ -14,15 +13,15 @@ public static WorkflowOptions UseRedisQueues(this WorkflowOptions options, strin return options; } - public static WorkflowOptions UseRedisLocking(this WorkflowOptions options, string connectionString) + public static WorkflowOptions UseRedisLocking(this WorkflowOptions options, string connectionString, string prefix = null) { - options.UseDistributedLockManager(sp => new RedisLockProvider(connectionString, sp.GetService())); + options.UseDistributedLockManager(sp => new RedisLockProvider(connectionString, prefix, sp.GetService())); return options; } - public static WorkflowOptions UseRedisPersistence(this WorkflowOptions options, string connectionString, string prefix) + public static WorkflowOptions UseRedisPersistence(this WorkflowOptions options, string connectionString, string prefix, bool deleteComplete = false) { - options.UsePersistence(sp => new RedisPersistenceProvider(connectionString, prefix, sp.GetService())); + options.UsePersistence(sp => new RedisPersistenceProvider(connectionString, prefix, deleteComplete, sp.GetService())); return options; } diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs index 8d7d1e35d..bb595309d 100644 --- a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLifeCycleEventHub.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -16,7 +15,7 @@ public class RedisLifeCycleEventHub : ILifeCycleEventHub private readonly string _connectionString; private readonly string _channel; private ICollection> _subscribers = new HashSet>(); - private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; private IConnectionMultiplexer _multiplexer; private ISubscriber _subscriber; diff --git a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs index 1b18e7539..f71a1c323 100644 --- a/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.cs +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisLockProvider.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; @@ -15,15 +14,17 @@ namespace WorkflowCore.Providers.Redis.Services public class RedisLockProvider : IDistributedLockProvider { private readonly ILogger _logger; - private readonly string _connectionString; + 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, ILoggerFactory logFactory) + public RedisLockProvider(string connectionString, string prefix, ILoggerFactory logFactory) { _connectionString = connectionString; + _prefix = prefix; _logger = logFactory.CreateLogger(GetType()); } @@ -32,7 +33,7 @@ public async Task AcquireLock(string Id, CancellationToken cancellationTok if (_redlockFactory == null) throw new InvalidOperationException(); - var redLock = await _redlockFactory.CreateLockAsync(Id, _lockTimeout); + var redLock = await _redlockFactory.CreateLockAsync(GetResource(Id), _lockTimeout); if (redLock.IsAcquired) { @@ -51,11 +52,13 @@ 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 == Id) + if (redLock.Resource == resource) { redLock.Dispose(); ManagedLocks.Remove(redLock); @@ -70,7 +73,7 @@ public Task ReleaseLock(string Id) public async Task Start() { _multiplexer = await ConnectionMultiplexer.ConnectAsync(_connectionString); - _redlockFactory = RedLockFactory.Create(new List() { new RedLockMultiplexer(_multiplexer) }); + _redlockFactory = RedLockFactory.Create(new List { new RedLockMultiplexer(_multiplexer) }); } public async Task Stop() @@ -78,7 +81,15 @@ 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 index 621f01e8b..eb76fa29e 100644 --- a/src/providers/WorkflowCore.Providers.Redis/Services/RedisPersistenceProvider.cs +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisPersistenceProvider.cs @@ -1,7 +1,8 @@ -using System; +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; @@ -24,25 +25,39 @@ public class RedisPersistenceProvider : IPersistenceProvider private readonly IConnectionMultiplexer _multiplexer; private readonly IDatabase _redis; - private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + private readonly bool _removeComplete; - public RedisPersistenceProvider(string connectionString, string prefix, ILoggerFactory logFactory) + 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) + 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) + 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); @@ -50,13 +65,17 @@ public async Task PersistWorkflow(WorkflowInstance workflow) 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) + public async Task> GetRunnableInstances(DateTime asAt, CancellationToken _ = default) { var result = new List(); - var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{WORKFLOW_SET}.{RUNNABLE_INDEX}", -1, DateTime.UtcNow.Ticks); + var data = await _redis.SortedSetRangeByScoreAsync($"{_prefix}.{WORKFLOW_SET}.{RUNNABLE_INDEX}", -1, asAt.ToUniversalTime().Ticks); foreach (var item in data) result.Add(item); @@ -64,19 +83,23 @@ public async Task> GetRunnableInstances(DateTime asAt) return result; } - public async Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, + public Task> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take) { throw new NotImplementedException(); } - public async Task GetWorkflowInstance(string Id) + public async Task GetWorkflowInstance(string Id, CancellationToken _ = default) { var raw = await _redis.HashGetAsync($"{_prefix}.{WORKFLOW_SET}", Id); + if (!raw.HasValue) + { + return null; + } return JsonConvert.DeserializeObject(raw, _serializerSettings); } - public async Task> GetWorkflowInstances(IEnumerable ids) + public async Task> GetWorkflowInstances(IEnumerable ids, CancellationToken _ = default) { if (ids == null) { @@ -87,7 +110,7 @@ public async Task> GetWorkflowInstances(IEnumerabl return raw.Select(r => JsonConvert.DeserializeObject(r, _serializerSettings)); } - public async Task CreateEventSubscription(EventSubscription subscription) + public async Task CreateEventSubscription(EventSubscription subscription, CancellationToken _ = default) { subscription.Id = Guid.NewGuid().ToString(); var str = JsonConvert.SerializeObject(subscription, _serializerSettings); @@ -97,7 +120,7 @@ public async Task CreateEventSubscription(EventSubscription subscription return subscription.Id; } - public async Task> GetSubscriptions(string eventName, string eventKey, DateTime asOf) + 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); @@ -112,7 +135,7 @@ public async Task> GetSubscriptions(string eventN return result; } - public async Task TerminateSubscription(string eventSubscriptionId) + public async Task TerminateSubscription(string eventSubscriptionId, CancellationToken _ = default) { var existingRaw = await _redis.HashGetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId); var existing = JsonConvert.DeserializeObject(existingRaw, _serializerSettings); @@ -120,18 +143,18 @@ public async Task TerminateSubscription(string eventSubscriptionId) await _redis.SortedSetRemoveAsync($"{_prefix}.{SUBSCRIPTION_SET}.{EVENTSLUG_INDEX}.{existing.EventName}-{existing.EventKey}", eventSubscriptionId); } - public async Task GetSubscription(string 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) + public async Task GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf, CancellationToken cancellationToken = default) { - return (await GetSubscriptions(eventName, eventKey, asOf)).FirstOrDefault(sub => string.IsNullOrEmpty(sub.ExternalToken)); + 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) + 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) @@ -144,7 +167,7 @@ public async Task SetSubscriptionToken(string eventSubscriptionId, string return true; } - public async Task ClearSubscriptionToken(string eventSubscriptionId, string token) + 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) @@ -156,7 +179,7 @@ public async Task ClearSubscriptionToken(string eventSubscriptionId, string toke await _redis.HashSetAsync($"{_prefix}.{SUBSCRIPTION_SET}", eventSubscriptionId, str); } - public async Task CreateEvent(Event newEvent) + public async Task CreateEvent(Event newEvent, CancellationToken _ = default) { newEvent.Id = Guid.NewGuid().ToString(); var str = JsonConvert.SerializeObject(newEvent, _serializerSettings); @@ -171,13 +194,13 @@ public async Task CreateEvent(Event newEvent) return newEvent.Id; } - public async Task GetEvent(string 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) + 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); @@ -188,7 +211,7 @@ public async Task> GetRunnableEvents(DateTime asAt) return result; } - public async Task> GetEvents(string eventName, string eventKey, DateTime asOf) + 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); @@ -199,25 +222,25 @@ public async Task> GetEvents(string eventName, string eventK return result; } - public async Task MarkEventProcessed(string id) + public async Task MarkEventProcessed(string id, CancellationToken cancellationToken = default) { - var evt = await GetEvent(id); + 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) + public async Task MarkEventUnprocessed(string id, CancellationToken cancellationToken = default) { - var evt = await GetEvent(id); + 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) + public Task PersistErrors(IEnumerable errors, CancellationToken _ = default) { return Task.CompletedTask; } @@ -225,5 +248,15 @@ public Task PersistErrors(IEnumerable errors) 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 index dbed9fa98..347c699b6 100644 --- a/src/providers/WorkflowCore.Providers.Redis/Services/RedisQueueProvider.cs +++ b/src/providers/WorkflowCore.Providers.Redis/Services/RedisQueueProvider.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; @@ -18,7 +17,7 @@ public class RedisQueueProvider : IQueueProvider private IConnectionMultiplexer _multiplexer; private IDatabase _redis; - private readonly Dictionary _queues = new Dictionary() + private readonly Dictionary _queues = new Dictionary { [QueueType.Workflow] = "workflows", [QueueType.Event] = "events", @@ -37,7 +36,13 @@ public async Task QueueWork(string id, QueueType queue) if (_redis == null) throw new InvalidOperationException(); - await _redis.ListRightPushAsync(GetQueueName(queue), id, When.Always); + 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) diff --git a/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj b/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj index 4513eb506..b1cc0aa97 100644 --- a/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj +++ b/src/providers/WorkflowCore.Providers.Redis/WorkflowCore.Providers.Redis.csproj @@ -2,19 +2,16 @@ netstandard2.0 - 3.0.1 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) - - 3.0.1 + 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..33be17272 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/ServiceCollectionExtensions.cs @@ -2,18 +2,56 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; 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 Task RabbitMqConnectionFactory(IServiceProvider sp, string clientProvidedName, CancellationToken cancellationToken = default); + 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(async (sp, name, cancellationToken) => await connectionFactory.CreateConnectionAsync(name, cancellationToken)); + } + + 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(async (sp, name, cancellationToken) => await connectionFactory.CreateConnectionAsync(hostnames, name, cancellationToken)); + } + + 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 c28091092..1554d8bed 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) @@ -31,11 +37,16 @@ public async Task QueueWork(string id, QueueType queue) if (_connection == null) throw new InvalidOperationException("RabbitMQ provider not running"); - using (var channel = _connection.CreateModel()) + var channel = await _connection.CreateChannelAsync(new CreateChannelOptions(publisherConfirmationsEnabled: false, publisherConfirmationTrackingEnabled: false), CancellationToken.None); + try + { + await channel.QueueDeclareAsync(queue: _queueNameProvider.GetQueueName(queue), durable: true, exclusive: false, autoDelete: false, arguments: null, passive: false, noWait: false, CancellationToken.None); + var body = new ReadOnlyMemory(Encoding.UTF8.GetBytes(id)); + await channel.BasicPublishAsync(exchange: "", routingKey: _queueNameProvider.GetQueueName(queue), mandatory: false, basicProperties: new BasicProperties(), body: body, CancellationToken.None); + } + finally { - channel.QueueDeclare(queue: 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); + await channel.CloseAsync(200, "OK", abort: false, CancellationToken.None); } } @@ -44,25 +55,33 @@ public async Task DequeueWork(QueueType queue, CancellationToken cancell if (_connection == null) throw new InvalidOperationException("RabbitMQ provider not running"); - using (var channel = _connection.CreateModel()) + var channel = await _connection.CreateChannelAsync(new CreateChannelOptions(publisherConfirmationsEnabled: false, publisherConfirmationTrackingEnabled: false), CancellationToken.None); + try { - channel.QueueDeclare(queue: GetQueueName(queue), - durable: true, - exclusive: false, - autoDelete: false, - arguments: null); + await channel.QueueDeclareAsync(queue: _queueNameProvider.GetQueueName(queue), + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + passive: false, + noWait: false, + CancellationToken.None); - channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); + await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 1, global: false, CancellationToken.None); - var msg = channel.BasicGet(GetQueueName(queue), false); + var msg = await channel.BasicGetAsync(_queueNameProvider.GetQueueName(queue), autoAck: false, CancellationToken.None); if (msg != null) { - var data = Encoding.UTF8.GetString(msg.Body); - channel.BasicAck(msg.DeliveryTag, false); + var data = Encoding.UTF8.GetString(msg.Body.ToArray()); + await channel.BasicAckAsync(msg.DeliveryTag, multiple: false, CancellationToken.None); return data; } return null; } + finally + { + await channel.CloseAsync(200, "OK", abort: false, CancellationToken.None); + } } public void Dispose() @@ -70,38 +89,24 @@ public void Dispose() if (_connection != null) { if (_connection.IsOpen) - _connection.Close(); + _connection.CloseAsync(200, "OK", TimeSpan.FromSeconds(10), abort: false, CancellationToken.None).GetAwaiter().GetResult(); } } public async Task Start() { - _connection = _connectionFactory.CreateConnection("Workflow-Core"); + _connection = await _rabbitMqConnectionFactory(_serviceProvider, "Workflow-Core"); } public async Task Stop() { if (_connection != null) { - _connection.Close(); + await _connection.CloseAsync(200, "OK", TimeSpan.FromSeconds(10), abort: false, CancellationToken.None); _connection = null; } } - private 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"; - } - 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 6735f21eb..4729c7eeb 100644 --- a/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj +++ b/src/providers/WorkflowCore.QueueProviders.RabbitMQ/WorkflowCore.QueueProviders.RabbitMQ.csproj @@ -2,7 +2,6 @@ Workflow Core RabbitMQ queue provider - 1.1.0 Daniel Gerlag netstandard2.0 WorkflowCore.QueueProviders.RabbitMQ @@ -15,10 +14,7 @@ false false false - 2.0.0 Queue provider for Workflow-core using RabbitMQ - 2.0.0.0 - 2.0.0.0 @@ -26,8 +22,8 @@ - - + + diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs index 50f1173e1..19b173d5c 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Interfaces/ISqlServerQueueProviderMigrator.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; namespace WorkflowCore.QueueProviders.SqlServer.Interfaces diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs index 1915f487c..c323b9d03 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Models/QueueConfig.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace WorkflowCore.QueueProviders.SqlServer.Models { diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs index 7e2364408..56f4f7fc2 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/ServiceCollectionExtensions.cs @@ -1,8 +1,6 @@ #region using using System; -using Microsoft.Extensions.DependencyInjection; - using WorkflowCore.Models; using WorkflowCore.QueueProviders.SqlServer; using WorkflowCore.QueueProviders.SqlServer.Interfaces; @@ -26,7 +24,7 @@ public static WorkflowOptions UseSqlServerBroker(this WorkflowOptions options, s options.Services.AddTransient(); options.Services.AddTransient(sp => new SqlServerQueueProviderMigrator(connectionString, sp.GetService(), sp.GetService())); - var sqlOptions = new SqlServerQueueProviderOptions() + var sqlOptions = new SqlServerQueueProviderOptions { ConnectionString = connectionString, CanCreateDb = canCreateDb, diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs index 691e6561b..4cbcd7dac 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/QueueConfigProvider.cs @@ -1,7 +1,6 @@ #region using using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using WorkflowCore.Interface; @@ -17,7 +16,7 @@ namespace WorkflowCore.QueueProviders.SqlServer.Services /// public class QueueConfigProvider : IQueueConfigProvider { - private readonly Dictionary _queues = new Dictionary() + private readonly Dictionary _queues = new Dictionary { [QueueType.Workflow] = new QueueConfig("workflow"), [QueueType.Event] = new QueueConfig("event"), diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs index 3ff92713d..5c77342fe 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlCommandExecutor.cs @@ -1,8 +1,6 @@ #region using using System; -using System.Collections.Generic; -using System.Data; using System.Data.Common; using System.Data.SqlClient; using System.Linq; diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs index a28f894e6..18a27b920 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/Services/SqlServerQueueProviderMigrator.cs @@ -3,7 +3,6 @@ using System; using System.Data.SqlClient; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using WorkflowCore.Interface; using WorkflowCore.QueueProviders.SqlServer.Interfaces; diff --git a/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj b/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj index 36d7cda77..26e52c308 100644 --- a/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj +++ b/src/providers/WorkflowCore.QueueProviders.SqlServer/WorkflowCore.QueueProviders.SqlServer.csproj @@ -5,7 +5,7 @@ Roberto Paterlini Queue provider for Workflow-core using SQL Server Service Broker - 1.0.3-alpha + alpha @@ -22,7 +22,7 @@ - + diff --git a/src/samples/Directory.Build.props b/src/samples/Directory.Build.props new file mode 100644 index 000000000..8e4a35270 --- /dev/null +++ b/src/samples/Directory.Build.props @@ -0,0 +1,7 @@ + + + net6.0 + latest + false + + \ No newline at end of file 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/WebApiSample/Startup.cs b/src/samples/WebApiSample/WebApiSample/Startup.cs index 13a19ddd8..0eee8a49e 100644 --- a/src/samples/WebApiSample/WebApiSample/Startup.cs +++ b/src/samples/WebApiSample/WebApiSample/Startup.cs @@ -44,11 +44,14 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseDeveloperExceptionPage(); } - + app.UseRouting(); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1")); - app.UseMvc(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Workflows}/{action=Get}"); + }); var host = app.ApplicationServices.GetService(); host.RegisterWorkflow(); diff --git a/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj b/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj index e5545be0c..e1454eb7d 100644 --- a/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj +++ b/src/samples/WebApiSample/WebApiSample/WebApiSample.csproj @@ -1,7 +1,6 @@  - netcoreapp2.1 Linux ..\docker-compose.dcproj @@ -15,7 +14,7 @@ - + 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 c25838dbe..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 { 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 e4a09e491..d20a6bc51 100644 --- a/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj +++ b/src/samples/WorkflowCore.Sample01/WorkflowCore.Sample01.csproj @@ -1,7 +1,6 @@  - netcoreapp2.2 WorkflowCore.Sample01 Exe WorkflowCore.Sample01 diff --git a/src/samples/WorkflowCore.Sample02/Program.cs b/src/samples/WorkflowCore.Sample02/Program.cs index 3c1a65bcf..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 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 0765d50c3..be4e85c2d 100644 --- a/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj +++ b/src/samples/WorkflowCore.Sample02/WorkflowCore.Sample02.csproj @@ -1,7 +1,6 @@  - netcoreapp2.2 WorkflowCore.Sample02 Exe WorkflowCore.Sample02 diff --git a/src/samples/WorkflowCore.Sample03/MyDataClass.cs b/src/samples/WorkflowCore.Sample03/MyDataClass.cs index a3b51429c..3f0cead60 100644 --- a/src/samples/WorkflowCore.Sample03/MyDataClass.cs +++ b/src/samples/WorkflowCore.Sample03/MyDataClass.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using WorkflowCore.Interface; namespace WorkflowCore.Sample03 { 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/Program.cs b/src/samples/WorkflowCore.Sample03/Program.cs index 96c06d45d..813e5dfd7 100644 --- a/src/samples/WorkflowCore.Sample03/Program.cs +++ b/src/samples/WorkflowCore.Sample03/Program.cs @@ -1,12 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Debug; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using WorkflowCore.Interface; -using WorkflowCore.Services; namespace WorkflowCore.Sample03 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 fc4a5d4fd..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,7 +15,6 @@ - diff --git a/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs b/src/samples/WorkflowCore.Sample04/EventSampleWorkflow.cs index d07a5ac03..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; diff --git a/src/samples/WorkflowCore.Sample04/MyDataClass.cs b/src/samples/WorkflowCore.Sample04/MyDataClass.cs index 0863891a2..719459c33 100644 --- a/src/samples/WorkflowCore.Sample04/MyDataClass.cs +++ b/src/samples/WorkflowCore.Sample04/MyDataClass.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace WorkflowCore.Sample04 { diff --git a/src/samples/WorkflowCore.Sample04/Program.cs b/src/samples/WorkflowCore.Sample04/Program.cs index 09e80b737..0e1895d44 100644 --- a/src/samples/WorkflowCore.Sample04/Program.cs +++ b/src/samples/WorkflowCore.Sample04/Program.cs @@ -1,18 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using MongoDB.Driver; using StackExchange.Redis; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using WorkflowCore.Interface; -using WorkflowCore.Persistence.MongoDB.Services; -using WorkflowCore.Services; -using Amazon.DynamoDBv2; -using Amazon.SQS; namespace WorkflowCore.Sample04 { 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 82107b0e6..5940a8e00 100644 --- a/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj +++ b/src/samples/WorkflowCore.Sample04/WorkflowCore.Sample04.csproj @@ -1,10 +1,9 @@  - netcoreapp3.0 WorkflowCore.Sample04 Exe - WorkflowCore.Sample04 + WorkflowCore.Sample04 false false false @@ -24,12 +23,7 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + 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 274e10232..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 { 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 592fa228b..bbfd7f7c7 100644 --- a/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj +++ b/src/samples/WorkflowCore.Sample05/WorkflowCore.Sample05.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Sample05 Exe WorkflowCore.Sample05 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 ba08aa071..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 { 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 b61478b9e..47e03b0ee 100644 --- a/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj +++ b/src/samples/WorkflowCore.Sample06/WorkflowCore.Sample06.csproj @@ -1,7 +1,6 @@  - netcoreapp2.0 WorkflowCore.Sample06 Exe WorkflowCore.Sample06 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 42abb05cb..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; diff --git a/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj b/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj index 5ca51a866..1cc78f66f 100644 --- a/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj +++ b/src/samples/WorkflowCore.Sample07/WorkflowCore.Sample07.csproj @@ -1,7 +1,6 @@  - netcoreapp3.0 true WorkflowCore.Sample07 Exe @@ -26,7 +25,6 @@ - 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 4da29c6b9..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 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/README.md b/src/samples/WorkflowCore.Sample08/README.md new file mode 100644 index 000000000..7c6e83c51 --- /dev/null +++ b/src/samples/WorkflowCore.Sample08/README.md @@ -0,0 +1,71 @@ +# Human (User) Workflow Sample + +This sample demonstrates how to create workflows that require human interaction using the WorkflowCore.Users extension. + +## What this sample shows + +* **User Tasks**: How to create tasks that are assigned to specific users or groups +* **User Options**: How to provide multiple choice options for users to select from +* **Conditional Branching**: How to execute different workflow paths based on user choices +* **Task Escalation**: How to automatically reassign tasks to different users when timeouts occur +* **User Action Management**: How to retrieve open user actions and publish user responses programmatically + +## The Workflow + +```c# +public class HumanWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(context => ExecutionResult.Next()) + .UserTask("Do you approve", data => @"domain\bob") + .WithOption("yes", "I approve").Do(then => then + .StartWith(context => Console.WriteLine("You approved")) + ) + .WithOption("no", "I do not approve").Do(then => then + .StartWith(context => Console.WriteLine("You did not approve")) + ) + .WithEscalation(x => TimeSpan.FromSeconds(20), x => @"domain\frank", action => action + .StartWith(context => Console.WriteLine("Escalated task")) + .Then(context => Console.WriteLine("Sending notification...")) + ) + .Then(context => Console.WriteLine("end")); + } +} +``` + +## How it works + +1. **Task Assignment**: The workflow creates a user task with the prompt "Do you approve" and assigns it to `domain\bob` + +2. **User Options**: Two options are provided: + - "yes" with label "I approve" - executes approval workflow + - "no" with label "I do not approve" - executes rejection workflow + +3. **Escalation**: If the task is not completed within 20 seconds, it automatically escalates to `domain\frank` and executes the escalation workflow + +4. **User Interaction**: The program demonstrates how to: + - Get open user actions using `host.GetOpenUserActions(workflowId)` + - Display options to the user + - Publish user responses using `host.PublishUserAction(key, user, value)` + +## Key Features + +* **UserTask**: Creates tasks that wait for human input +* **WithOption**: Defines multiple choice options with conditional workflow paths +* **WithEscalation**: Automatically reassigns tasks after a timeout period +* **Interactive Console**: Shows how to build a simple interface for user interaction + +## Dependencies + +This sample requires the `WorkflowCore.Users` extension package, which provides the human workflow capabilities. + +## Use Cases + +This pattern is useful for: +- Approval workflows +- Decision-making processes +- Task assignment and escalation +- Interactive business processes +- Multi-step user interactions \ No newline at end of file diff --git a/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj b/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj index 3ff429e28..f6ae3bcba 100644 --- a/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj +++ b/src/samples/WorkflowCore.Sample08/WorkflowCore.Sample08.csproj @@ -1,7 +1,6 @@  - netcoreapp3.0 WorkflowCore.Sample08 Exe WorkflowCore.Sample08 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 52516fca8..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; 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 6f75a0d7d..e7c7206ec 100644 --- a/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj +++ b/src/samples/WorkflowCore.Sample09/WorkflowCore.Sample09.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp3.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..2c60afe02 --- /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("ForeachSync").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; + } + + } +} 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 75173f70c..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(); 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 6f75a0d7d..e7c7206ec 100644 --- a/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj +++ b/src/samples/WorkflowCore.Sample10/WorkflowCore.Sample10.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp3.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 f16853aa4..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(); 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 b6454f6a2..106048445 100644 --- a/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj +++ b/src/samples/WorkflowCore.Sample11/WorkflowCore.Sample11.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp2.2 diff --git a/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs b/src/samples/WorkflowCore.Sample12/OutcomeWorkflow.cs index 61f1f98cd..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 { diff --git a/src/samples/WorkflowCore.Sample12/Program.cs b/src/samples/WorkflowCore.Sample12/Program.cs index 81f50021d..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..."); - host.StartWorkflow("outcome-sample", new MyData() { Value = 2 }); + host.StartWorkflow("outcome-sample", new MyData { Value = 2 }); Console.ReadLine(); 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 9879baab2..e0fb0e8aa 100644 --- a/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj +++ b/src/samples/WorkflowCore.Sample12/WorkflowCore.Sample12.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp3.0 diff --git a/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs b/src/samples/WorkflowCore.Sample13/ParallelWorkflow.cs index 8b4ea0d0a..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 { diff --git a/src/samples/WorkflowCore.Sample13/Program.cs b/src/samples/WorkflowCore.Sample13/Program.cs index 9f5a22e7a..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 { 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 dbe49166e..522eaac98 100644 --- a/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj +++ b/src/samples/WorkflowCore.Sample13/WorkflowCore.Sample13.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp2.2 diff --git a/src/samples/WorkflowCore.Sample14/Program.cs b/src/samples/WorkflowCore.Sample14/Program.cs index b2ecabb2d..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 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 fef8173cc..539973bee 100644 --- a/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj +++ b/src/samples/WorkflowCore.Sample14/WorkflowCore.Sample14.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp2.2 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 04d4c2a58..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; 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 c63fbd792..f8d1df555 100644 --- a/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj +++ b/src/samples/WorkflowCore.Sample15/WorkflowCore.Sample15.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp2.2 diff --git a/src/samples/WorkflowCore.Sample16/Program.cs b/src/samples/WorkflowCore.Sample16/Program.cs index 6ca03139d..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; 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 c63fbd792..f8d1df555 100644 --- a/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj +++ b/src/samples/WorkflowCore.Sample16/WorkflowCore.Sample16.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp2.2 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/WorkflowCore.Sample17.csproj b/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj index 26933096d..f54a7ad6d 100644 --- a/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj +++ b/src/samples/WorkflowCore.Sample17/WorkflowCore.Sample17.csproj @@ -2,13 +2,8 @@ Exe - netcoreapp3.0 - - - - diff --git a/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs b/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs index 126e2be15..22b32a782 100644 --- a/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs +++ b/src/samples/WorkflowCore.Sample18/ActivityWorkflow.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; using WorkflowCore.Interface; using WorkflowCore.Sample18.Steps; diff --git a/src/samples/WorkflowCore.Sample18/Program.cs b/src/samples/WorkflowCore.Sample18/Program.cs index 8bf555690..aa7d3f2f7 100644 --- a/src/samples/WorkflowCore.Sample18/Program.cs +++ b/src/samples/WorkflowCore.Sample18/Program.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Console; -using Microsoft.Extensions.Logging.Debug; using System; using WorkflowCore.Interface; @@ -20,7 +18,7 @@ static void Main(string[] args) Console.WriteLine("Starting workflow..."); - var workflowId = host.StartWorkflow("activity-sample", new MyData() { Request = "Spend $1,000,000" }).Result; + 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; diff --git a/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs b/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs index 0d9803de3..69f1dd42c 100644 --- a/src/samples/WorkflowCore.Sample18/Steps/CustomMessage.cs +++ b/src/samples/WorkflowCore.Sample18/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.Sample18/Steps/GoodbyeWorld.cs b/src/samples/WorkflowCore.Sample18/Steps/GoodbyeWorld.cs index dffce0d6b..b42f3e913 100644 --- a/src/samples/WorkflowCore.Sample18/Steps/GoodbyeWorld.cs +++ b/src/samples/WorkflowCore.Sample18/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.Sample18/Steps/HelloWorld.cs b/src/samples/WorkflowCore.Sample18/Steps/HelloWorld.cs index 8440b8cbe..8378bb7cd 100644 --- a/src/samples/WorkflowCore.Sample18/Steps/HelloWorld.cs +++ b/src/samples/WorkflowCore.Sample18/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.Sample18/WorkflowCore.Sample18.csproj b/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj index 660b47511..c1c6e0846 100644 --- a/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj +++ b/src/samples/WorkflowCore.Sample18/WorkflowCore.Sample18.csproj @@ -2,7 +2,6 @@ Exe - netcoreapp3.0 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 282fdbb15..308565745 100644 --- a/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj +++ b/src/samples/WorkflowCore.TestSample01/WorkflowCore.TestSample01.csproj @@ -1,9 +1,5 @@  - - netcoreapp2.2 - - @@ -15,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..cdb7e1676 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,17 @@ + + + net6.0;net8.0 + 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 8d4989b7c..a83648df4 100644 --- a/test/Docker.Testify/Docker.Testify.csproj +++ b/test/Docker.Testify/Docker.Testify.csproj @@ -1,9 +1,5 @@  - - netstandard2.0 - - diff --git a/test/Docker.Testify/DockerSetup.cs b/test/Docker.Testify/DockerSetup.cs index 0bf7055df..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()}", @@ -111,32 +110,37 @@ public async Task PullImage(string name, string tag) return; Debug.WriteLine($"Pulling docker image {name}:{tag}"); - await docker.Images.CreateImageAsync(new ImagesCreateParameters() { FromImage = name, Tag = tag }, null, new Progress()); + await docker.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = name, Tag = tag }, null, new Progress()); } 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 index 0e57c4012..6401bda70 100644 --- a/test/ScratchPad/ElasticTest.cs +++ b/test/ScratchPad/ElasticTest.cs @@ -1,16 +1,7 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; -using WorkflowCore.Models; -using System.Text; -using Amazon; -using Amazon.DynamoDBv2; -using Amazon.Runtime; using Nest; using WorkflowCore.Models.Search; @@ -30,10 +21,10 @@ public static void test(string[] args) host.RegisterWorkflow(); host.Start(); - var data1 = new WorkflowCore.Sample03.MyDataClass() { Value1 = 2, Value2 = 3 }; + 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" }; + var data2 = new WorkflowCore.Sample04.MyDataClass { Value1 = "test" }; host.StartWorkflow("EventSampleWorkflow", data2, "alt1 boom").Wait(); diff --git a/test/ScratchPad/Program.cs b/test/ScratchPad/Program.cs index 40c569a29..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,8 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using WorkflowCore.Interface; using WorkflowCore.Models; -using System.Text; -using WorkflowCore.Services.DefinitionStorage; namespace ScratchPad { @@ -26,15 +23,30 @@ public static void Main(string[] args) //loader.LoadDefinition(Properties.Resources.HelloWorld, Deserializers.Json); host.Start(); - - host.StartWorkflow("Test01", 1, new WfData() + + 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.PublishEvent("MyEvent", "Key", "one", DateTime.Now); + + for (var i = 0; i < 12000; i++) { - Value1 = "two", - Value2 = "data2" - }); + 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); + - - Console.ReadLine(); host.Stop(); } @@ -46,8 +58,11 @@ private static IServiceProvider ConfigureServices() services.AddLogging(); //services.AddWorkflow(); //services.AddWorkflow(x => x.UseMongoDB(@"mongodb://localhost:27017", "workflow")); + 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"); @@ -111,10 +126,12 @@ public void Build(IWorkflowBuilder builder) 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(20)).Wait(); + Task.Delay(TimeSpan.FromSeconds(5)).Wait(); Console.WriteLine("------2"); return ExecutionResult.Next(); }) 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 548b8fa61..4e58ba3b1 100644 --- a/test/ScratchPad/ScratchPad.csproj +++ b/test/ScratchPad/ScratchPad.csproj @@ -1,13 +1,14 @@  - netcoreapp3.0 ScratchPad Exe ScratchPad false false false + false + net6.0;net8.0 @@ -23,11 +24,6 @@ - - - - - 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 index 03562bfbd..a5819d2fa 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/ActivityScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -45,19 +43,19 @@ public void Build(IWorkflowBuilder builder) public ActivityScenario() { - Setup(); + Setup(); //NOTE cjundt [10/03/2023] setup shouldn't be here in constructor. It prevents from using ICollectionFixture data. } [Fact] public void Scenario() { - var workflowId = StartWorkflow(new MyDataClass() { ActivityInput = new ActivityInput() { Value1 = "a", Value2 = 1 } }); + 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() + Host.SubmitActivitySuccess(activity.Token, new ActivityOutput { Value1 = actInput.Value1 + "1", Value2 = actInput.Value2 + 1 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 index 9f7f93245..1c1fe7bdd 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/AttachScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/AttachScenario.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; using FluentAssertions; -using System.Threading; using WorkflowCore.Testing; namespace WorkflowCore.IntegrationTests.Scenarios diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/BaseScenario.cs index 4e70b446c..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 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 index 3ae1faa66..4d9f733fd 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/DecisionScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DecisionScenario.cs @@ -78,7 +78,7 @@ public DecisionScenario() [Fact] public void Scenario1() { - var workflowId = StartWorkflow(new MyDataClass() { Op = "+", Value1 = 2, Value2 = 3 }); + var workflowId = StartWorkflow(new MyDataClass { Op = "+", Value1 = 2, Value2 = 3 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); @@ -89,7 +89,7 @@ public void Scenario1() [Fact] public void Scenario2() { - var workflowId = StartWorkflow(new MyDataClass() { Op = "-", Value1 = 2, Value2 = 3 }); + var workflowId = StartWorkflow(new MyDataClass { Op = "-", Value1 = 2, Value2 = 3 }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); GetStatus(workflowId).Should().Be(WorkflowStatus.Complete); 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 index 81a782264..fd100fe2d 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Autofac.Extensions.DependencyInjection; using Autofac; -using System.Diagnostics; namespace WorkflowCore.IntegrationTests.Scenarios { diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs index 416db55f2..aa6591f6a 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DynamicDataIOScenario.cs @@ -58,7 +58,7 @@ public DynamicDataIOScenario() [Fact] public void Scenario() { - 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/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 index 5ac5766ed..79fd2633d 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/EventOrderScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/EventOrderScenario.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/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 index 36c12c3bd..b2f83ef65 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/FailingSagaScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/FailingSagaScenario.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/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 index 68fedfae9..0f080504a 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/MultistepCompensationScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; @@ -34,11 +32,11 @@ public void Build(IWorkflowBuilder builder) Compensation1Fired = CompensationCounter; }) .Then(context => ExecutionResult.Next()) - .CompensateWith(context => + .CompensateWithSequence(context => context.StartWith(c => { CompensationCounter++; Compensation2Fired = CompensationCounter; - }) + })) .Then(context => ExecutionResult.Next()) .CompensateWith(context => { 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 index dd4d2d720..f8b3c1a08 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/NestedRetrySagaScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/NestedRetrySagaScenario.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/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/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 index fb7d2eed1..383eb39a4 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/StoredJsonScenario.cs +++ b/test/WorkflowCore.IntegrationTests/Scenarios/StoredJsonScenario.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using WorkflowCore.Interface; using WorkflowCore.Models; using Xunit; using FluentAssertions; @@ -11,7 +8,7 @@ namespace WorkflowCore.IntegrationTests.Scenarios { public class StoredJsonScenario : JsonWorkflowTest - { + { public StoredJsonScenario() { Setup(); @@ -20,7 +17,7 @@ public StoredJsonScenario() [Fact(DisplayName = "Execute branch 1")] public void should_execute_branch1() { - var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard() { Flag1 = true, Flag2 = true, Flag3 = true }); + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard { Flag1 = true, Flag2 = true, Flag3 = true }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); var data = GetData(workflowId); @@ -34,12 +31,13 @@ public void should_execute_branch1() 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 }); + var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionJson(), new CounterBoard { Flag1 = true, Flag2 = true, Flag3 = false }); WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(30)); var data = GetData(workflowId); @@ -53,6 +51,7 @@ public void should_execute_branch2() data.Counter6.Should().Be(1); data.Counter7.Should().Be(0); data.Counter8.Should().Be(1); + data.Counter10.Should().Be(1); } [Fact] @@ -67,7 +66,8 @@ public void should_execute_json_workflow_with_dynamic_data() ["Counter3"] = 0, ["Counter4"] = 0, ["Counter5"] = 0, - ["Counter6"] = 0 + ["Counter6"] = 0, + ["Counter10"] = 0 }; var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionDynamicJson(), initialData); @@ -82,6 +82,7 @@ public void should_execute_json_workflow_with_dynamic_data() 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/StoredYamlScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/StoredYamlScenario.cs index 820f86bd8..95243ff03 100644 --- a/test/WorkflowCore.IntegrationTests/Scenarios/StoredYamlScenario.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; @@ -11,7 +8,7 @@ namespace WorkflowCore.IntegrationTests.Scenarios { public class StoredYamlScenario : YamlWorkflowTest - { + { public StoredYamlScenario() { Setup(); @@ -20,7 +17,7 @@ public StoredYamlScenario() [Fact(DisplayName = "Execute workflow from stored YAML definition")] public void should_execute_yaml_workflow() { - var workflowId = StartWorkflow(TestAssets.Utils.GetTestDefinitionYaml(), 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 should_execute_yaml_workflow() 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 index 8c834f82f..cc9847574 100644 --- a/test/WorkflowCore.IntegrationTests/SearchIndexTests.cs +++ b/test/WorkflowCore.IntegrationTests/SearchIndexTests.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; using FluentAssertions; -using FluentAssertions.Collections; -using FluentAssertions.Equivalency; -using FluentAssertions.Common; using Xunit; using WorkflowCore.Interface; using WorkflowCore.Models; @@ -23,13 +20,14 @@ protected SearchIndexTests() 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() + result.Add(new WorkflowInstance { Id = "1", CreateTime = new DateTime(2010, 1, 1), @@ -37,25 +35,25 @@ protected IEnumerable BuildTestData() Reference = "ref1" }); - result.Add(new WorkflowInstance() + result.Add(new WorkflowInstance { Id = "2", CreateTime = new DateTime(2020, 1, 1), Status = WorkflowStatus.Runnable, Reference = "ref2", - Data = new DataObject() + Data = new DataObject { Value3 = 7 } }); - result.Add(new WorkflowInstance() + result.Add(new WorkflowInstance { Id = "3", CreateTime = new DateTime(2010, 1, 1), Status = WorkflowStatus.Complete, Reference = "ref3", - Data = new DataObject() + Data = new DataObject { Value3 = 5, Value1 = "quick fox", @@ -63,13 +61,13 @@ protected IEnumerable BuildTestData() } }); - result.Add(new WorkflowInstance() + result.Add(new WorkflowInstance { Id = "4", CreateTime = new DateTime(2010, 1, 1), Status = WorkflowStatus.Complete, Reference = "ref4", - Data = new AltDataObject() + Data = new AltDataObject { Value1 = 9, Value2 = new DateTime(2000, 1, 1) @@ -164,7 +162,7 @@ class DataObject : ISearchable public IEnumerable GetSearchTokens() { - return new List() + return new List { Value1, Value2 diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index 971f10c57..c92182936 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -1,32 +1,24 @@  - netcoreapp2.2 WorkflowCore.IntegrationTests WorkflowCore.IntegrationTests true false false false + net6.0 - + - - - - - - - - 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/LockProvider/DistributedLockProviderTests.cs b/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs index 7f1eebf1d..7ea5218bb 100644 --- a/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs +++ b/test/WorkflowCore.TestAssets/LockProvider/DistributedLockProviderTests.cs @@ -1,13 +1,12 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; +using System.Threading.Tasks; using WorkflowCore.Interface; using FluentAssertions; using NUnit.Framework; namespace WorkflowCore.TestAssets.LockProvider -{ +{ public abstract class DistributedLockProviderTests { protected IDistributedLockProvider Subject; @@ -19,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"; @@ -34,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()); @@ -45,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 286c37d49..d4b47397a 100644 --- a/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs +++ b/test/WorkflowCore.TestAssets/Properties/Resources.Designer.cs @@ -19,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 { 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 ea0f85f54..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,7 +7,7 @@ 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) { @@ -32,6 +30,10 @@ 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() { diff --git a/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj b/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj index 0105a6e9d..cbba0dccb 100644 --- a/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj +++ b/test/WorkflowCore.TestAssets/WorkflowCore.TestAssets.csproj @@ -1,7 +1,6 @@  - netstandard2.0 WorkflowCore.TestAssets WorkflowCore.TestAssets false @@ -13,9 +12,13 @@ + + + Always + Always @@ -35,9 +38,7 @@ - - - + diff --git a/test/WorkflowCore.TestAssets/stored-definition.json b/test/WorkflowCore.TestAssets/stored-definition.json index 56f36ac5e..7c77806b3 100644 --- a/test/WorkflowCore.TestAssets/stored-definition.json +++ b/test/WorkflowCore.TestAssets/stored-definition.json @@ -58,6 +58,26 @@ "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" } + } ] ] }, @@ -90,4 +110,4 @@ } ] -} \ No newline at end of file +} diff --git a/test/WorkflowCore.TestAssets/stored-definition.yaml b/test/WorkflowCore.TestAssets/stored-definition.yaml index f01efb07b..e469b1e88 100644 --- a/test/WorkflowCore.TestAssets/stored-definition.yaml +++ b/test/WorkflowCore.TestAssets/stored-definition.yaml @@ -50,6 +50,21 @@ Steps: 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: diff --git a/test/WorkflowCore.TestAssets/stored-dynamic-definition.json b/test/WorkflowCore.TestAssets/stored-dynamic-definition.json index dc572f8af..8c6a10dbe 100644 --- a/test/WorkflowCore.TestAssets/stored-dynamic-definition.json +++ b/test/WorkflowCore.TestAssets/stored-dynamic-definition.json @@ -57,6 +57,26 @@ "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" } + } ] ] }, 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 index a166ec804..83501a6d5 100644 --- a/test/WorkflowCore.Tests.DynamoDB/DynamoDbDockerSetup.cs +++ b/test/WorkflowCore.Tests.DynamoDB/DynamoDbDockerSetup.cs @@ -1,10 +1,5 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using System; -using System.Collections.Generic; +using System; using System.Net; -using System.Runtime.InteropServices; -using System.Text; using Docker.Testify; using Xunit; using Amazon.DynamoDBv2; @@ -16,10 +11,11 @@ public class DynamoDbDockerSetup : DockerSetup { public static string ConnectionString { get; set; } - public static AWSCredentials Credentials => new EnvironmentVariablesAWSCredentials(); + 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() { @@ -34,7 +30,7 @@ public override bool TestReady() { ServiceURL = $"http://localhost:{ExternalPort}" }; - AmazonDynamoDBClient client = new AmazonDynamoDBClient(clientConfig); + AmazonDynamoDBClient client = new AmazonDynamoDBClient(Credentials, clientConfig); var resp = client.ListTablesAsync().Result; return resp.HttpStatusCode == HttpStatusCode.OK; diff --git a/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs b/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs index aa0663936..7d215f4ee 100644 --- a/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.DynamoDB/DynamoPersistenceProviderFixture.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; @@ -28,8 +26,9 @@ protected override IPersistenceProvider Subject if (_subject == null) { var cfg = new AmazonDynamoDBConfig { ServiceURL = DynamoDbDockerSetup.ConnectionString }; - var provisioner = new DynamoDbProvisioner(DynamoDbDockerSetup.Credentials, cfg, "unittests", new LoggerFactory()); - var client = new DynamoPersistenceProvider(DynamoDbDockerSetup.Credentials, cfg, provisioner, "unittests", new LoggerFactory()); + 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; } diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs index 66a621c4f..8b6754c02 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoBasicScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs index 23ea7b32c..f475c58cb 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoCompensationScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs index e0214203d..648b18868 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDataScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs index d5d0ff5ac..419d7ac37 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoDynamicDataScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs index 887e39541..e421d31a2 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoEventScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs index a8026da4b..1c9e30679 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoForeachScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs index 2b7ff3fac..efc3738d8 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoIfScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs index a09dad420..5aea6ee1a 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoRetrySagaScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs index 9cb309614..8c1f6a317 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoSagaScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs index 50960652a..7912b3f53 100644 --- a/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs +++ b/test/WorkflowCore.Tests.DynamoDB/Scenarios/DynamoWhileScenario.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using Amazon.DynamoDBv2; -using Amazon.Runtime; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.DynamoDB; using Xunit; namespace WorkflowCore.Tests.DynamoDB.Scenarios diff --git a/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj b/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj index c2563b8b2..6de478177 100644 --- a/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj +++ b/test/WorkflowCore.Tests.DynamoDB/WorkflowCore.Tests.DynamoDB.csproj @@ -1,16 +1,11 @@ - netcoreapp2.2 - - false + net6.0 - - - - + diff --git a/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs index e79779466..4ed16a152 100644 --- a/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs +++ b/test/WorkflowCore.Tests.Elasticsearch/ElasticsearchDockerSetup.cs @@ -1,49 +1,34 @@ using System; -using System.Collections.Generic; -using System.Net; -using System.Text; -using Docker.Testify; -using Nest; +using System.Threading.Tasks; +using Squadron; using Xunit; namespace WorkflowCore.Tests.Elasticsearch { - public class ElasticsearchDockerSetup : DockerSetup + public class ElasticsearchDockerSetup : IAsyncLifetime { + private readonly ElasticsearchResource _elasticsearchResource; public static string ConnectionString { get; set; } - - public override string ImageName => @"elasticsearch"; - public override string ImageTag => "7.5.1"; - public override int InternalPort => 9200; - public override TimeSpan TimeOut => TimeSpan.FromSeconds(30); - - public override IList EnvironmentVariables => new List { - $"discovery.type=single-node" - }; - public override void PublishConnectionInfo() + public ElasticsearchDockerSetup() { - ConnectionString = $"http://localhost:{ExternalPort}"; + _elasticsearchResource = new ElasticsearchResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var client = new ElasticClient(new ConnectionSettings(new Uri($"http://localhost:{ExternalPort}"))); - var ping = client.Ping(); - return ping.IsValid; - } - catch - { - return false; - } + await _elasticsearchResource.InitializeAsync(); + ConnectionString = $"http://localhost:{_elasticsearchResource.Instance.HostPort}"; + } + public Task DisposeAsync() + { + return _elasticsearchResource.DisposeAsync(); } } [CollectionDefinition("Elasticsearch collection")] - public class DynamoDbCollection : ICollectionFixture + public class ElasticsearchCollection : ICollectionFixture { } -} +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj b/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj index 168be514f..d5bae6c71 100644 --- a/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj +++ b/test/WorkflowCore.Tests.Elasticsearch/WorkflowCore.Tests.Elasticsearch.csproj @@ -1,16 +1,11 @@  - netcoreapp2.2 - - false + net6.0 - - - - + diff --git a/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs b/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs index 515f3ed8a..e5c18b341 100644 --- a/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs +++ b/test/WorkflowCore.Tests.MongoDB/MongoDockerSetup.cs @@ -1,46 +1,40 @@ -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 MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Squadron; +using WorkflowCore.UnitTests; using Xunit; namespace WorkflowCore.Tests.MongoDB -{ - public class MongoDockerSetup : DockerSetup +{ + public class MongoDockerSetup : IAsyncLifetime { + private readonly MongoReplicaSetResource _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 MongoReplicaSetResource(); } - 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; + BsonSerializer.TryRegisterSerializer(new ObjectSerializer(type => + ObjectSerializer.DefaultAllowedTypes(type) || type.FullName.StartsWith("WorkflowCore."))); + } + 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..fd6546aca 100644 --- a/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.MongoDB/MongoPersistenceProviderFixture.cs @@ -1,7 +1,4 @@ using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Persistence.MongoDB.Services; using WorkflowCore.UnitTests; @@ -24,8 +21,10 @@ protected override IPersistenceProvider Subject get { var client = new MongoClient(MongoDockerSetup.ConnectionString); - var db = client.GetDatabase("workflow-tests"); - return new MongoPersistenceProvider(db); + var db = client.GetDatabase(nameof(MongoPersistenceProviderFixture)); + var provider = new MongoPersistenceProvider(db); + provider.EnsureStoreExists(); + return provider; } } } 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 index 29682a442..ca815f41a 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoActivityScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoActivityScenario.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson.Serialization; using WorkflowCore.IntegrationTests.Scenarios; @@ -16,7 +14,7 @@ protected override void ConfigureServices(IServiceCollection services) BsonClassMap.RegisterClassMap(x => x.AutoMap()); BsonClassMap.RegisterClassMap(x => x.AutoMap()); - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + 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 index 828dda1a2..0db093806 100644 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDecisionScenario.cs +++ b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDecisionScenario.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson.Serialization; using WorkflowCore.IntegrationTests.Scenarios; using Xunit; @@ -13,7 +10,7 @@ public class MongoDecisionScenario : DecisionScenario { protected override void ConfigureServices(IServiceCollection services) { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); + 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/MongoDynamicDataScenario.cs b/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDynamicDataScenario.cs deleted file mode 100644 index 456f0a0f1..000000000 --- a/test/WorkflowCore.Tests.MongoDB/Scenarios/MongoDynamicDataScenario.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using WorkflowCore.IntegrationTests.Scenarios; -using Xunit; - -namespace WorkflowCore.Tests.MongoDB.Scenarios -{ - [Collection("Mongo collection")] - public class MongoDynamicDataScenario : DynamicDataIOScenario - { - protected override void ConfigureServices(IServiceCollection services) - { - services.AddWorkflow(x => x.UseMongoDB(MongoDockerSetup.ConnectionString, "integration-tests")); - } - } -} 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 bec4d8561..0bffc02e8 100644 --- a/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj +++ b/test/WorkflowCore.Tests.MongoDB/WorkflowCore.Tests.MongoDB.csproj @@ -1,13 +1,13 @@  - netcoreapp2.2 WorkflowCore.Tests.MongoDB WorkflowCore.Tests.MongoDB true false false false + net8.0 @@ -15,19 +15,14 @@ - + - - - - - - - + + diff --git a/test/WorkflowCore.Tests.MySQL/DockerSetup.cs b/test/WorkflowCore.Tests.MySQL/DockerSetup.cs index f91909f9d..06c041818 100644 --- a/test/WorkflowCore.Tests.MySQL/DockerSetup.cs +++ b/test/WorkflowCore.Tests.MySQL/DockerSetup.cs @@ -1,52 +1,38 @@ -using System.Collections.Generic; -using Docker.Testify; -using Xunit; -using MySql.Data.MySqlClient; +using Xunit; using System; +using System.Threading.Tasks; +using Squadron; namespace WorkflowCore.Tests.MySQL { - public class MysqlDockerSetup : DockerSetup + public class MysqlDockerSetup : IAsyncLifetime { + private readonly MySqlResource _mySqlResource; public static string ConnectionString { get; set; } public static string ScenarioConnectionString { get; set; } - public static string RootPassword => "rootpwd123"; - public override TimeSpan TimeOut => TimeSpan.FromSeconds(60); - - public override string ImageName => "mysql"; - public override IList EnvironmentVariables => new List { - $"MYSQL_ROOT_PASSWORD={RootPassword}" - }; - - public override int InternalPort => 3306; - - public override void PublishConnectionInfo() + public MysqlDockerSetup() { - ConnectionString = $"Server=127.0.0.1;Port={ExternalPort};Database=workflow;User=root;Password={RootPassword};"; - ScenarioConnectionString = $"Server=127.0.0.1;Port={ExternalPort};Database=scenarios;User=root;Password={RootPassword};"; + _mySqlResource = new MySqlResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var connection = new MySqlConnection($"host=127.0.0.1;port={ExternalPort};user=root;password={RootPassword};database=mysql;"); - connection.Open(); - connection.Close(); - return true; - } - catch - { - return false; - } + 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/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 index 2b8dd3c9d..5ec59395d 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDynamicDataScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlDynamicDataScenario.cs @@ -1,7 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs index 09838d64a..65e99ad84 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlEventScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs index 23111adbd..01a15de17 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForeachScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs index 0d2d43421..8a57e465b 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlForkScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs index d02f5dbe7..7cf6721fc 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlIfScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs index 12dcbeb58..f16190f73 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlRetrySagaScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs index 2d88ea835..e23719b22 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlSagaScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs index 22017da65..909b98b4f 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlUserScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs index dae355db2..e7721e382 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhenScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs index e6b07202e..8e6c5f6ee 100644 --- a/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs +++ b/test/WorkflowCore.Tests.MySQL/Scenarios/MysqlWhileScenario.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using WorkflowCore.IntegrationTests.Scenarios; -using WorkflowCore.Tests.MySQL; using Xunit; namespace WorkflowCore.Tests.MySQL.Scenarios diff --git a/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj b/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj index a255e01d6..bdb4029e7 100644 --- a/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj +++ b/test/WorkflowCore.Tests.MySQL/WorkflowCore.Tests.MySQL.csproj @@ -1,15 +1,11 @@ - netcoreapp2.2 - - false + net6.0;net8.0 - - - + @@ -18,7 +14,7 @@ - + diff --git a/test/WorkflowCore.Tests.Oracle/OraclePersistenceProviderFixture.cs b/test/WorkflowCore.Tests.Oracle/OraclePersistenceProviderFixture.cs new file mode 100644 index 000000000..a87c11bad --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/OraclePersistenceProviderFixture.cs @@ -0,0 +1,23 @@ +using WorkflowCore.Interface; +using WorkflowCore.Persistence.EntityFramework.Services; +using WorkflowCore.Persistence.Oracle; +using WorkflowCore.UnitTests; +using Xunit; +using Xunit.Abstractions; + +namespace WorkflowCore.Tests.Oracle +{ + [Collection("Oracle collection")] + public class OraclePersistenceProviderFixture : BasePersistenceFixture + { + private readonly EntityFrameworkPersistenceProvider _subject; + protected override IPersistenceProvider Subject => _subject; + + public OraclePersistenceProviderFixture(OracleDockerSetup dockerSetup, ITestOutputHelper output) + { + output.WriteLine($"Connecting on {OracleDockerSetup.ConnectionString}"); + _subject = new EntityFrameworkPersistenceProvider(new OracleContextFactory(OracleDockerSetup.ConnectionString), true, true); + _subject.EnsureStoreExists(); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/OracleSetup.cs b/test/WorkflowCore.Tests.Oracle/OracleSetup.cs new file mode 100644 index 000000000..a5ee262ec --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/OracleSetup.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Testcontainers.Oracle; +using Xunit; + +namespace WorkflowCore.Tests.Oracle +{ + public class OracleDockerSetup : IAsyncLifetime + { + private readonly OracleContainer _oracleContainer; + + public static string ConnectionString { get; private set; } + + public OracleDockerSetup() + { + _oracleContainer = new OracleBuilder() + .WithImage("gvenzl/oracle-free:latest") + .WithUsername("TEST_WF") + .WithPassword("test") + .Build(); + } + + public async Task InitializeAsync() + { + await _oracleContainer.StartAsync(); + // Build connection string manually since TestContainers might not provide Oracle-specific format + ConnectionString = $"Data Source=localhost:{_oracleContainer.GetMappedPublicPort(1521)}/FREEPDB1;User Id=TEST_WF;Password=test;"; + } + + public async Task DisposeAsync() => await _oracleContainer.DisposeAsync(); + } + + [CollectionDefinition("Oracle collection")] + public class OracleCollection : ICollectionFixture { } +} \ No newline at end of file diff --git a/test/WorkflowCore.Tests.Oracle/Properties/AssemblyInfo.cs b/test/WorkflowCore.Tests.Oracle/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..24072c66f --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +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.Tests.Oracle")] +[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("8c2bd4d2-43ec-4930-9364-cda938c01803")] diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleActivityScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleActivityScenario.cs new file mode 100644 index 000000000..7bf8ae2d6 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleActivityScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleActivityScenario : ActivityScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleBasicScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleBasicScenario.cs new file mode 100644 index 000000000..a7f8bb940 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleBasicScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleBasicScenario : BasicScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDataScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDataScenario.cs new file mode 100644 index 000000000..136b54a3e --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDataScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleDataScenario : DataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDelayScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDelayScenario.cs new file mode 100644 index 000000000..22a582f0d --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDelayScenario.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleDelayScenario : DelayScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(cfg => + { + cfg.UseOracle(OracleDockerSetup.ConnectionString, true, true); + cfg.UsePollInterval(TimeSpan.FromSeconds(2)); + }); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDynamicDataScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDynamicDataScenario.cs new file mode 100644 index 000000000..5ab8b940b --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleDynamicDataScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleDynamicDataScenario : DynamicDataIOScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleEventScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleEventScenario.cs new file mode 100644 index 000000000..a38290065 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleEventScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleEventScenario : EventScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForeachScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForeachScenario.cs new file mode 100644 index 000000000..023920ba1 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForeachScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleForeachScenario : ForeachScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForkScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForkScenario.cs new file mode 100644 index 000000000..bbe85a909 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleForkScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleForkScenario : ForkScenario + { + protected override void Configure(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleIfScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleIfScenario.cs new file mode 100644 index 000000000..cab8004bd --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleIfScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleIfScenario : IfScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleRetrySagaScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleRetrySagaScenario.cs new file mode 100644 index 000000000..21bf5dbef --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleRetrySagaScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleRetrySagaScenario : RetrySagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleSagaScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleSagaScenario.cs new file mode 100644 index 000000000..ea093b222 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleSagaScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleSagaScenario : SagaScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleUserScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleUserScenario.cs new file mode 100644 index 000000000..60ab568b2 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleUserScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleUserScenario : UserScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhenScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhenScenario.cs new file mode 100644 index 000000000..d12490f59 --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhenScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleWhenScenario : WhenScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhileScenario.cs b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhileScenario.cs new file mode 100644 index 000000000..45f5eebdd --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/Scenarios/OracleWhileScenario.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using WorkflowCore.IntegrationTests.Scenarios; +using WorkflowCore.Persistence.Oracle; + +using Xunit; + +namespace WorkflowCore.Tests.Oracle.Scenarios +{ + [Collection("Oracle collection")] + public class OracleWhileScenario : WhileScenario + { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddWorkflow(x => x.UseOracle(OracleDockerSetup.ConnectionString, true, true)); + } + } +} diff --git a/test/WorkflowCore.Tests.Oracle/WorkflowCore.Tests.Oracle.csproj b/test/WorkflowCore.Tests.Oracle/WorkflowCore.Tests.Oracle.csproj new file mode 100644 index 000000000..b3b68406c --- /dev/null +++ b/test/WorkflowCore.Tests.Oracle/WorkflowCore.Tests.Oracle.csproj @@ -0,0 +1,25 @@ + + + + net6.0;net8.0 + WorkflowCore.Tests.Oracle + WorkflowCore.Tests.Oracle + true + false + false + false + + + + + + + + + + + + + + + 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 95ebe7976..9322b033a 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/PostgresPersistenceProviderFixture.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.PostgreSQL; 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 index 7b63b42dd..2f31eca31 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresActivityScenario.cs +++ b/test/WorkflowCore.Tests.PostgreSQL/Scenarios/PostgresActivityScenario.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/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/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 1cc7d56bd..53a91aa9a 100644 --- a/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj +++ b/test/WorkflowCore.Tests.PostgreSQL/WorkflowCore.Tests.PostgreSQL.csproj @@ -1,13 +1,13 @@  - netcoreapp3.0 WorkflowCore.Tests.PostgreSQL WorkflowCore.Tests.PostgreSQL true false false false + net6.0;net8.0 @@ -16,15 +16,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..40ba780ac --- /dev/null +++ b/test/WorkflowCore.Tests.QueueProviders.RabbitMQ/WorkflowCore.Tests.QueueProviders.RabbitMQ.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + + + + + + + + + + + diff --git a/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs b/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs index cfd3d11f2..4faba0af2 100644 --- a/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs +++ b/test/WorkflowCore.Tests.Redis/RedisDockerSetup.cs @@ -1,44 +1,34 @@ -using Docker.DotNet; -using Docker.DotNet.Models; -using System; -using System.Collections.Generic; -using System.Net; -using Docker.Testify; -using StackExchange.Redis; +using System; +using System.Threading.Tasks; +using Squadron; using Xunit; namespace WorkflowCore.Tests.Redis -{ - public class RedisDockerSetup : DockerSetup +{ + public class RedisDockerSetup : IAsyncLifetime { + private readonly RedisResource _redisResource; public static string ConnectionString { get; set; } - public override string ImageName => @"redis"; - public override int InternalPort => 6379; - - public override void PublishConnectionInfo() + public RedisDockerSetup() { - ConnectionString = $"localhost:{ExternalPort}"; + _redisResource = new RedisResource(); } - public override bool TestReady() + public async Task InitializeAsync() { - try - { - var multiplexer = ConnectionMultiplexer.Connect($"localhost:{ExternalPort}"); - return multiplexer.IsConnected; - } - catch - { - return false; - } + 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 index 105e5900a..22a898e2b 100644 --- a/test/WorkflowCore.Tests.Redis/RedisPersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.Redis/RedisPersistenceProviderFixture.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.Logging; using WorkflowCore.Interface; using WorkflowCore.Providers.Redis.Services; @@ -26,7 +24,7 @@ protected override IPersistenceProvider Subject { if (_subject == null) { - var client = new RedisPersistenceProvider(RedisDockerSetup.ConnectionString, "test", new LoggerFactory()); + var client = new RedisPersistenceProvider(RedisDockerSetup.ConnectionString, "test", false, new LoggerFactory()); client.EnsureStoreExists(); _subject = client; } diff --git a/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj b/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj index c5ee9a6f8..f6a8aaed1 100644 --- a/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj +++ b/test/WorkflowCore.Tests.Redis/WorkflowCore.Tests.Redis.csproj @@ -1,16 +1,12 @@ - netcoreapp2.2 - - false + net6.0 - - - + diff --git a/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs b/test/WorkflowCore.Tests.SqlServer/DockerSetup.cs index 6b589666d..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(120); - - 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 index cf09f78e3..2094cdf66 100644 --- a/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerActivityScenario.cs +++ b/test/WorkflowCore.Tests.SqlServer/Scenarios/SqlServerActivityScenario.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/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/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/WorkflowCore.Tests.SqlServer.csproj b/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj index 011714983..11b6c00e2 100644 --- a/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj +++ b/test/WorkflowCore.Tests.SqlServer/WorkflowCore.Tests.SqlServer.csproj @@ -1,13 +1,11 @@  - netcoreapp2.2 + net6.0;net8.0;net9.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 5a27fd530..f8318ad9b 100644 --- a/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs +++ b/test/WorkflowCore.Tests.Sqlite/SqlitePersistenceProviderFixture.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Persistence.EntityFramework.Services; using WorkflowCore.Persistence.Sqlite; diff --git a/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj b/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj index 9ce1b77cd..e3e981713 100644 --- a/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj +++ b/test/WorkflowCore.Tests.Sqlite/WorkflowCore.Tests.Sqlite.csproj @@ -1,15 +1,9 @@  - - netcoreapp2.2 + + net6.0;net8.0 - - - - - - diff --git a/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs b/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs index 87b733a2d..9114da7b9 100644 --- a/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs +++ b/test/WorkflowCore.UnitTests/BasePersistenceFixture.cs @@ -17,7 +17,7 @@ public abstract class BasePersistenceFixture [Fact] 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_should_generate_id() Version = 1, WorkflowDefinitionId = "My Workflow" }; - workflow.ExecutionPointers.Add(new ExecutionPointer() + workflow.ExecutionPointers.Add(new ExecutionPointer { Id = Guid.NewGuid().ToString(), Active = true, @@ -42,9 +42,9 @@ public void CreateNewWorkflow_should_generate_id() [Fact] 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, @@ -52,13 +52,13 @@ public void GetWorkflowInstance_should_retrieve_workflow() WorkflowDefinitionId = "My Workflow", Reference = "My Reference" }; - workflow.ExecutionPointers.Add(new ExecutionPointer() + workflow.ExecutionPointers.Add(new ExecutionPointer { Id = "1", Active = true, StepId = 0, SleepUntil = new DateTime(2000, 1, 1).ToUniversalTime(), - Scope = new List() { "4", "3", "2", "1" } + Scope = new List { "4", "3", "2", "1" } }); var workflowId = Subject.CreateNewWorkflow(workflow).Result; @@ -72,9 +72,9 @@ public void GetWorkflowInstance_should_retrieve_workflow() [Fact] public void GetWorkflowInstances_should_retrieve_workflows() { - var workflow01 = new WorkflowInstance() + var workflow01 = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, @@ -82,19 +82,19 @@ public void GetWorkflowInstances_should_retrieve_workflows() WorkflowDefinitionId = "My Workflow", Reference = "My Reference" }; - workflow01.ExecutionPointers.Add(new ExecutionPointer() + 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" } + Scope = new List { "4", "3", "2", "1" } }); var workflowId01 = Subject.CreateNewWorkflow(workflow01).Result; - var workflow02 = new WorkflowInstance() + var workflow02 = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, @@ -102,19 +102,19 @@ public void GetWorkflowInstances_should_retrieve_workflows() WorkflowDefinitionId = "My Workflow", Reference = "My Reference" }; - workflow02.ExecutionPointers.Add(new ExecutionPointer() + 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" } + Scope = new List { "4", "3", "2", "1" } }); var workflowId02 = Subject.CreateNewWorkflow(workflow02).Result; - var workflow03 = new WorkflowInstance() + var workflow03 = new WorkflowInstance { - Data = new TestData() { Value1 = 7 }, + Data = new TestData { Value1 = 7 }, Description = "My Description", Status = WorkflowStatus.Runnable, NextExecution = 0, @@ -122,13 +122,13 @@ public void GetWorkflowInstances_should_retrieve_workflows() WorkflowDefinitionId = "My Workflow", Reference = "My Reference" }; - workflow03.ExecutionPointers.Add(new ExecutionPointer() + 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" } + Scope = new List { "4", "3", "2", "1" } }); var workflowId03 = Subject.CreateNewWorkflow(workflow03).Result; @@ -155,9 +155,9 @@ public void GetWorkflowInstances_should_retrieve_workflows() [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, @@ -166,25 +166,91 @@ public void PersistWorkflow() 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, - Scope = new List() { "1", "2", "3", "4" } + 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() @@ -197,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, @@ -207,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, @@ -216,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. }); diff --git a/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs b/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs index c25b57857..81e667139 100644 --- a/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs +++ b/test/WorkflowCore.UnitTests/Models/MemberMapParameterTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; using WorkflowCore.Interface; @@ -18,7 +17,7 @@ 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() + var data = new MyData { Value1 = 5 }; @@ -36,7 +35,7 @@ public void should_assign_output() Expression> valueExpr = (x => x.Value1); var subject = new MemberMapParameter(valueExpr, memberExpr); var data = new MyData(); - var step = new MyStep() + var step = new MyStep { Value1 = 5 }; @@ -53,7 +52,7 @@ public void should_convert_input() Expression> valueExpr = (x => x.Value1); var subject = new MemberMapParameter(valueExpr, memberExpr); - var data = new MyData() + var data = new MyData { Value1 = 5 }; @@ -72,7 +71,7 @@ public void should_convert_output() Expression> valueExpr = (x => x.Value1); var subject = new MemberMapParameter(valueExpr, memberExpr); - var data = new MyData() + var data = new MyData { Value1 = 5 }; 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 0a9293523..c47ba054b 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 @@ -23,7 +19,7 @@ public class DefinitionLoaderTests public DefinitionLoaderTests() { _registry = A.Fake(); - _subject = new DefinitionLoader(_registry); + _subject = new DefinitionLoader(_registry, new TypeResolver()); } [Fact(DisplayName = "Should register workflow")] @@ -47,6 +43,16 @@ public void ParseDefinition() 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() diff --git a/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs b/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs index 557abdb5a..a5697882b 100644 --- a/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/ExecutionResultProcessorFixture.cs @@ -3,15 +3,12 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Text; using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Services; using FluentAssertions; using Xunit; -using WorkflowCore.Primitives; using System.Linq.Expressions; -using System.Threading.Tasks; namespace WorkflowCore.UnitTests.Services { @@ -48,15 +45,15 @@ 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 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(() => step.Outcomes).Returns(new List { outcome }); A.CallTo(() => PointerFactory.BuildNextPointer(definition, pointer1, outcome)).Returns(pointer2); //act @@ -77,7 +74,7 @@ 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 pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; var step = A.Fake(); var workflowResult = new WorkflowExecutorResult(); var instance = GivenWorkflow(pointer); @@ -95,7 +92,7 @@ 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 pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; var step = A.Fake(); var workflowResult = new WorkflowExecutorResult(); var instance = GivenWorkflow(pointer); @@ -117,19 +114,19 @@ 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" }; + 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 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(() => 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); @@ -153,7 +150,7 @@ 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 pointer = new ExecutionPointer { Id = "1", Active = true, StepId = 0, Status = PointerStatus.Running }; var step = A.Fake(); var workflowResult = new WorkflowExecutorResult(); var instance = GivenWorkflow(pointer); @@ -174,14 +171,14 @@ public void should_branch_children() 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 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); + var result = ExecutionResult.Branch(new List { branch }, null); - A.CallTo(() => step.Children).Returns(new List() { child }); + A.CallTo(() => step.Children).Returns(new List { child }); A.CallTo(() => PointerFactory.BuildChildPointer(definition, pointer, child, branch)).Returns(childPointer); //act @@ -197,7 +194,7 @@ private static WorkflowInstance GivenWorkflow(ExecutionPointer pointer) return new WorkflowInstance { Status = WorkflowStatus.Runnable, - ExecutionPointers = new ExecutionPointerCollection(new List() + 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 f340f3010..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; 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 6c99586f6..e548b8e88 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs @@ -3,15 +3,12 @@ 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; -using WorkflowCore.Primitives; -using System.Linq.Expressions; -using System.Threading.Tasks; namespace WorkflowCore.UnitTests.Services { @@ -27,6 +24,8 @@ public class WorkflowExecutorFixture protected IServiceProvider ServiceProvider; protected IScopeProvider ScopeProvider; protected IDateTimeProvider DateTimeProvider; + protected IStepExecutor StepExecutor; + protected IWorkflowMiddlewareRunner MiddlewareRunner; protected WorkflowOptions Options; public WorkflowExecutorFixture() @@ -40,19 +39,48 @@ public WorkflowExecutorFixture() 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(); - A.CallTo(() => ScopeProvider.CreateScope()).Returns(scope); + 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); + //loggerFactory.AddConsole(LogLevel.Debug); Subject = new WorkflowExecutor(Registry, ServiceProvider, ScopeProvider, DateTimeProvider, ResultProcesser, EventHub, CancellationProcessor, Options, loggerFactory); } @@ -60,7 +88,7 @@ public WorkflowExecutorFixture() [Fact(DisplayName = "Should execute active step")] public void should_execute_active_step() { - //arrange + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -73,11 +101,11 @@ public void should_execute_active_step() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) - }; + }; //act Subject.Execute(instance); @@ -87,10 +115,68 @@ public void should_execute_active_step() A.CallTo(() => ResultProcesser.ProcessExecutionResult(instance, A.Ignored, A.Ignored, step1, A.Ignored, A.Ignored)).MustHaveHappened(); } + [Fact(DisplayName = "Should call execute middleware when not completed")] + public void should_call_execute_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); + + 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.RunExecuteMiddleware(instance, A.Ignored)).MustHaveHappened(); + } + + [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); + + 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() { - //arrange + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -103,9 +189,9 @@ public void should_trigger_step_hooks() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; @@ -121,7 +207,7 @@ public void should_trigger_step_hooks() [Fact(DisplayName = "Should not execute inactive step")] public void should_not_execute_inactive_step() { - //arrange + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -134,12 +220,12 @@ public void should_not_execute_inactive_step() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = false, StepId = 0 } + new ExecutionPointer { Id = "1", Active = false, StepId = 0 } }) }; - + //act Subject.Execute(instance); @@ -153,9 +239,9 @@ public void should_map_inputs() //arrange var param = A.Fake(); - var step1Body = A.Fake(); + var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Next()); - WorkflowStep step1 = BuildFakeStep(step1Body, new List() + WorkflowStep step1 = BuildFakeStep(step1Body, new List { param } @@ -170,10 +256,10 @@ public void should_map_inputs() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - Data = new DataClass() { Value1 = 5 }, - ExecutionPointers = new ExecutionPointerCollection(new List() + Data = new DataClass { Value1 = 5 }, + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; @@ -188,13 +274,13 @@ public void should_map_inputs() [Fact(DisplayName = "Should map outputs")] public void should_map_outputs() { - //arrange + //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() + WorkflowStep step1 = BuildFakeStep(step1Body, new List(), new List { param } @@ -202,7 +288,7 @@ public void should_map_outputs() Given1StepWorkflow(step1, "Workflow", 1); - var data = new DataClass() { Value1 = 5 }; + var data = new DataClass { Value1 = 5 }; var instance = new WorkflowInstance { @@ -212,12 +298,12 @@ public void should_map_outputs() NextExecution = 0, Id = "001", Data = data, - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; - + //act Subject.Execute(instance); @@ -226,12 +312,12 @@ public void should_map_outputs() .MustHaveHappened(); } - + [Fact(DisplayName = "Should handle step exception")] public void should_handle_step_exception() { - //arrange + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Throws(); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -244,9 +330,9 @@ public void should_handle_step_exception() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; @@ -256,13 +342,13 @@ public void should_handle_step_exception() //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(); + 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 + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Persist(null)); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -275,9 +361,9 @@ public void should_process_after_execution_iteration() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; @@ -291,7 +377,7 @@ public void should_process_after_execution_iteration() [Fact(DisplayName = "Should process cancellations")] public void should_process_cancellations() { - //arrange + //arrange var step1Body = A.Fake(); A.CallTo(() => step1Body.RunAsync(A.Ignored)).Returns(ExecutionResult.Persist(null)); WorkflowStep step1 = BuildFakeStep(step1Body); @@ -304,9 +390,9 @@ public void should_process_cancellations() Status = WorkflowStatus.Runnable, NextExecution = 0, Id = "001", - ExecutionPointers = new ExecutionPointerCollection(new List() + ExecutionPointers = new ExecutionPointerCollection(new List { - new ExecutionPointer() { Id = "1", Active = true, StepId = 0 } + new ExecutionPointer { Id = "1", Active = true, StepId = 0 } }) }; @@ -320,12 +406,12 @@ public void should_process_cancellations() private void Given1StepWorkflow(WorkflowStep step1, string id, int version) { - A.CallTo(() => Registry.GetDefinition(id, version)).Returns(new WorkflowDefinition() + A.CallTo(() => Registry.GetDefinition(id, version)).Returns(new WorkflowDefinition { Id = id, Version = version, DataType = typeof(object), - Steps = new WorkflowStepCollection() + Steps = new WorkflowStepCollection { step1 } 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 index c3dbb96fd..3fb0bb75d 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowRegistryFixture.cs @@ -1,17 +1,9 @@ using FakeItEasy; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Text; -using WorkflowCore.Interface; using WorkflowCore.Models; using WorkflowCore.Services; using FluentAssertions; using Xunit; -using WorkflowCore.Primitives; -using System.Linq.Expressions; -using System.Threading.Tasks; namespace WorkflowCore.UnitTests.Services { 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 294adbd23..ff8f2e19e 100644 --- a/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj +++ b/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj @@ -1,13 +1,13 @@  - netcoreapp2.2 WorkflowCore.UnitTests WorkflowCore.UnitTests true false false false + net6.0 @@ -16,17 +16,6 @@ - - - - - - - - - - -