diff --git a/README.md b/README.md index 073104a37..a4e9c84d3 100644 --- a/README.md +++ b/README.md @@ -1,280 +1,275 @@ -![Web-based workflow designer](/doc/elsa-cover.png) - -## Elsa Workflows - -Elsa Core is a workflows library that enables workflow execution in any .NET Core application. -Workflows can be defined not only using code but also as JSON, YAML or XML. - -In addition, workflows can be visually designed using [Elsa Designer](https://github.com/elsa-workflows/elsa-designer-html), a reusable & extensible HTML5 web component built with [StencilJS[(https://stenciljs.com/). - -![Web-based workflow designer](/doc/workflow-sample-3.png) - -## Programmatic Workflows - -Workflows can be created programmatically and then executed using `IWorkflowInvoker`. - -### Hello World -The following code snippet demonstrates creating a workflow with two custom activities from code and then invoking it: - -```c# - -// Define a strongly-typed workflow. -public class HelloWorldWorkflow : IWorkflow -{ - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith() - .Then(); - } -} - -// Setup a service collection. -var services = new ServiceCollection() - .AddWorkflows() - .AddActivity() - .AddActivity() - .BuildServiceProvider(); - -// Invoke the workflow. -var invoker = services.GetService(); -await invoker.InvokeAsync(); - -// Output: -// /> Hello World! -// /> Goodbye cruel World... -``` - -### Persistence - -Workflows can be persisted using virtually any storage mechanism. -The following providers will be supported: - -- In Memory -- File System -- SQL Server -- MongoDB -- CosmosDB - -### Formats - -Currently, workflows can be stored in YAML or JSON format. -The following demonstrates a simple workflow expressed in YAML and JSON, respectively: - -**YAML** -```yaml -activities: -- name: WriteLine - id: hi-activity - textExpression: - syntax: PlainText - expression: Hi! What's your name? -- name: ReadLine - id: read-name-activity - argumentName: name -- name: WriteLine - id: greeting-activity - textExpression: - syntax: JavaScript - expression: '`Nice to meet you, ${name}!`' -connections: -- source: - activityId: hi-activity - name: Done - target: - activityId: read-name-activity -- source: - activityId: read-name-activity - name: Done - target: - activityId: greeting-activity -``` - -**JSON** -```json -{ - "activities": [ - { - "name": "WriteLine", - "id": "hi-activity", - "textExpression": { - "syntax": "PlainText", - "expression": "Hi! What's your name?" - } - }, - { - "id": "read-name-activity", - "name": "ReadLine", - "argumentName": "name" - }, - { - "name": "WriteLine", - "id": "greeting-activity", - "textExpression": { - "syntax": "JavaScript", - "expression": "`Nice to meet you, ${name}!`" - } - } - ], - "connections": [ - { - "source": { - "activityId": "hi-activity", - "name": "Done" - }, - "target": { - "activityId": "read-name-activity" - } - }, - { - "source": { - "activityId": "read-name-activity", - "name": "Done" - }, - "target": { - "activityId": "greeting-activity" - } - } - ] -} -``` - -The following demonstrates how to parse a workflow in YAML format: - -```c# -// Setup a service collection and use the FileSystemProvider for both workflow definitions and workflow instances. -var services = new ServiceCollection() - .AddWorkflowsInvoker() - .AddConsoleActivities() - .AddSingleton(Console.In) - .BuildServiceProvider(); - -// Load the data and specify data format. -var data = Resources.SampleWorkflowDefinition; -var format = YamlTokenFormatter.FormatName; // "YAML" - -// Deserialize the workflow from data. -var serializer = services.GetService(); -var workflowDefinition = await serializer.DeserializeAsync(data, format, CancellationToken.None); - -// Invoke the workflow. -var invoker = services.GetService(); -await invoker.InvokeAsync(workflowDefinition, workflowDefinition.Activities.First()); -``` - -## Workflow Host -In addition to programmatically invoke specific workflows using `IWorkflowInvoker`, you can instead "trigger" workflows using `IWorkflowHost`. -For example, if you have a bunch of workflows defined that start with e.g. a `HttpRequestTrigger` activity, you can execute all of these workflows using the following statement: - -`await workflowHost.TriggerWorkflowsAsync("HttpRequestTrigger", Variables.Empty, cancellationToken)` - -What this will do is invoke every workflow that either starts with a _HttpRequestTrigger_ activity or is blocked on said activity. - -## Long Running Workflows - -## Why Elsa Workflows? - -One of the main goals of Elsa is to **enable workflows in any .NET application** with **minimum effort** and **maximum extensibility**. -This means that it should be easy to integrate workflow capabilities into your own application. - -### What about Azure Logic Apps? - -As powerful and as complete Azure Logic Apps is, it's available only as a managed service in Azure. Elsa on the other hand allows you to host it not only on Azure, but on any cloud provider that supports .NET Core. And of course you can host it on-premise. - -Although you can implement long-running workflows with Logic Apps, you would typically do so with splitting your workflow with multiple Logic Apps where one workflow invokes the other. This can make the logic flow a bit hard to follow. -with Elsa, you simply add triggers anywhere in the workflow, making it easier to have a complete view of your application logic. And if you want, you can still invoke other workflows form one workflow. - -### What about Windows Workflow Foundation? - -I've always liked Windows Workflow Foundation, but unfortunately [development appears to have halted](https://forums.dotnetfoundation.org/t/what-is-the-roadmap-of-workflow-foundation/3066). -Although there's an effort being made to [port WF to .NET Standard](https://github.com/dmetzgar/corewf), there are a few reasons I prefer Elsa: - -- Elsa intrinsically supports triggering events that starts new workflows and resumes halted workflow instances in an easy to use manner. E.g. `workflowHost.TriggerWorkflowAsync("HttpRequestTrigger");"` will start and resume all workflows that either start with or are halted on the `HttpRequestTrigger`. -- Elsa has a web-based workflow designer. I once worked on a project for a customer that was building a huge SaaS platform. One of the requirements was to provide a workflow engine and a web-based editor. Although there are commercial workflow libraries and editors out there, the business model required open-source software. We used WF and the re-hosted Workflow Designer. It worked, but it wasn't great. - -### What about Orchard Workflows? - -Both [Orchard](http://docs.orchardproject.net/en/latest/Documentation/Workflows/) and [Orchard Core](https://orchardcore.readthedocs.io/en/dev/OrchardCore.Modules/OrchardCore.Workflows/) ship with a powerful workflows module, and both are awesome. -In fact, Elsa Workflows is taken & adapted from Orchard Core's Workflows module. Elsa uses a similar model, but there are some differences: - -- Elsa Workflows is completely decoupled from web, whereas Orchard Core Workflows is coupled to not only the web, but also the Orchard Core Framework itself. -- Elsa Workflows can execute in any .NET Core application without taking a dependency on any Orchard Core packages (not to be confused with Elsa Workflows Designer, which takes advantage of some Orchard Core packages). -- Elsa Workflows separates activity models from activity execution logic. - -I am a big fan of Orchard Core, and its Workflows module is in my opinion one of its biggest gems. In fact, the Elsa Workflows web-based designer depends on Orchard Core Framework packages because Orchard Core is that useful! -An important roadmap item is to provide an Orchard Core module called `OrchardCore.ElsaWorkfows`, which uses Elsa's engine and web-based designer within the context of an Orchard Core application and provides Orchard Core-specific activities such as content-related triggers and actions. - -As mentioned earlier: this is one of the main reasons that Elsa exists: to enable workflows in any .NET application. Orchard Core included. -There are a few reasons I think contributing to `OrchardCore.ElsaWorkflows` makes sense: - -- Elsa potentially has a broader audience, because workflows are applicable in more environments than Orchard Core. -- Orchard Core is awesome, and `OrchardCore.Workflows` is a key feature of it. If Elsa is used more widely, it is likely to also have more community support, which means more features. - -## Features - -The following lists some of Elsa's key features: - -- **Small, simple and fast**. The library should be lean & mean, meaning that it should be **easy to use**, **fast to execute** and **easy to extend** with custom activities. -- It must be a set of **libraries**. This allows me to create my application anyway I like, and implement workflow capabilities as I see fit. Thanks to ASP.NET Core's application model however, creating a workflow designer & workflow host is as simple as referencing the right packages and making a few calls. -- Invoke arbitrary workflows as if they were **functions of my application**. -- Trigger events that cause the appropriate workflows to **automatically start/resume** based on that event. -- Support **long-running workflows**. When a workflow executes and encounters an activity that requires e.g. user input, the workflow will halt, be persisted and go out of memory until it's time to resume. this could be a few seconds later, a few minutes, hours, days or even years. -- **Correlate** workflows with application-specific data. This is a key requirement for long-running workflows. -- Store workflows in a **file-based** format so I can make it part of source-control. -- Store workflows in a **database** when I don't want to make them part of source control. -- A **web-based designer**. Whether I store my workflows on a file system or in a database, and whether I host the designer online or only on my local machine, I need to be able to edit my workflows. -- Configure workflow activities with **expressions**. Oftentimes, information being processed by a workflow is dynamic in nature, and activities need a way to interact with this information. Workflow expressions allow for this. -- **Extensible** with application-specific **activities**, **custom stores** and **scripting engines**. -- Invoke other workflows. This allows for invoking reusable application logic from various workflows. Like invoking general-purpose functions from C# without having to duplicate code. -- **View & analyze** executed workflow instances. I want to see **which path** a workflow took, its **runtime state**, where it **faulted** and **compensate** faulted workflows. -- **Embed** the web-based workflow designer in **my own dashboard** application. This gives me the option of creating a single Workflow Host that runs all of my application logic, but also the option of hosting a workflows runtime in individual micro services (allowing for orchestration as well as choreography). -- **Separation of concerns**: The workflow core library, runtime and designer should all be separated. I.e. when the workflow host should not have a dependency on the web-based designer. This allows one for example to implement a desktop-based designer, or not use a designer at all and just go with YAML files. The host in the end only needs the workflow definitions and access to persistence stores. -- **On premise** or **managed** in the cloud - both scenarios are supported, because Elsa is just a set of NuGet packages that you reference from your application. - -## How to use Elsa - -Elsa is distributed as a set of NuGet packages, which makes it easy to add to your application. -When working with Elsa, you'll typically want to have at least two applications: - -1. An ASP.NET Core application to host the workflows designer. -2. A .NET application that executed workflows - -### Setting up a Workflow Designer ASP.NET Core Application - -TODO: describe all the steps to add packages and register services. - -### Setting up a Workflow Host .NET Application - -TODO: describe all the steps to add packages and register services. - -## Running Elsa Workflows Dashboard - -In order to run Elsa on your local machine, follow these steps: - -1. Clone the repository. -2. Run NPM install on all folders containing packages.json (or run `node npm-install.js` - a script in the root that recursively installs the Node packages) -3. Open a shell and navigate to `src/samples/SampleDashboard.Web` and run `dotnet run`. -4. Navigate to https://localhost:44397/ - -## Running Elsa Workflows Host - -(TODO) - -## Roadmap - -(TODO) - -- Describe all the features (core engine, runtime, webbased designer, YAML, scripting, separation of designer from invoker from engine). -- Describe various use cases. -- Describe how to use. -- Describe architecture. -- Describe how to implement (custom host, custom dashboard). -- Implement more activities -- Implement integration with Orchard Core (separate repo) -- Detailed documentation -- Open API Activity Harvester -- MassTransit Activity Harvester -- RabbitMQ Activities -- Azure Service Bus Activities -- Automatic UI for Activity Editor - +![Web-based workflow designer](/doc/elsa-cover.png) + +## Elsa Workflows + +Elsa Core is a workflows library that enables workflow execution in any .NET Core application. +Workflows can be defined not only using code but also as JSON, YAML or XML. + +In addition, workflows can be visually designed using [Elsa Designer](https://github.com/elsa-workflows/elsa-designer-html), a reusable & extensible HTML5 web component built with [StencilJS[(https://stenciljs.com/). + +![Web-based workflow designer](/doc/workflow-sample-3.png) + +## Programmatic Workflows + +Workflows can be created programmatically and then executed using `IWorkflowInvoker`. + +### Hello World +The following code snippet demonstrates creating a workflow with two custom activities from code and then invoking it: + +```c# + +// Define a strongly-typed workflow. +public class HelloWorldWorkflow : IWorkflow +{ + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Then(); + } +} + +// Setup a service collection. +var services = new ServiceCollection() + .AddWorkflows() + .AddActivity() + .AddActivity() + .BuildServiceProvider(); + +// Invoke the workflow. +var invoker = services.GetService(); +await invoker.InvokeAsync(); + +// Output: +// /> Hello World! +// /> Goodbye cruel World... +``` + +### Persistence + +Workflows can be persisted using virtually any storage mechanism. +The following providers will be supported: + +- In Memory +- File System +- SQL Server +- MongoDB +- CosmosDB + +### Formats + +Currently, workflows can be stored in YAML or JSON format. +The following demonstrates a simple workflow expressed in YAML and JSON, respectively: + +**YAML** +```yaml +activities: +- name: WriteLine + id: activity-1 + textExpression: + syntax: PlainText + expression: Hi! What's your name? +- name: ReadLine + id: activity-2 + argumentName: name +- name: WriteLine + id: activity-3 + textExpression: + syntax: JavaScript + expression: '`Nice to meet you, ${name}!`' +connections: +- source: + activityId: activity-1 + name: Done + target: + activityId: activity-2 +- source: + activityId: activity-2 + name: Done + target: + activityId: activity-3 +``` + +**JSON** +```json +{ + "activities": [ + { + "name": "WriteLine", + "id": "activity-1", + "textExpression": { + "syntax": "PlainText", + "expression": "Hi! What's your name?" + } + }, + { + "id": "activity-2", + "name": "ReadLine", + "argumentName": "name" + }, + { + "name": "WriteLine", + "id": "activity-3", + "textExpression": { + "syntax": "JavaScript", + "expression": "`Nice to meet you, ${name}!`" + } + } + ], + "connections": [ + { + "source": { + "activityId": "activity-1", + "name": "Done" + }, + "target": { + "activityId": "activity-2" + } + }, + { + "source": { + "activityId": "activity-2", + "name": "Done" + }, + "target": { + "activityId": "activity-3" + } + } + ] +} +``` + +The following demonstrates loading a workflow from a YAML string: + +```c# +// Setup a service collection and use the FileSystemProvider for both workflow definitions and workflow instances. +var services = new ServiceCollection() + .AddWorkflowsInvoker() + .AddConsoleActivities() + .AddSingleton(Console.In) + .BuildServiceProvider(); + +// Load the data and specify data format. +var data = Resources.SampleWorkflowDefinition; +var format = YamlTokenFormatter.FormatName; // "YAML" + +// Deserialize the workflow from data. +var serializer = services.GetService(); +var workflowDefinition = await serializer.DeserializeAsync(data, format, CancellationToken.None); + +// Invoke the workflow. +var invoker = services.GetService(); +await invoker.InvokeAsync(workflowDefinition); +``` + +## Long Running Workflows + +Elsa has native support for long-running workflows. As soon as a workflow is halted because of some blocking activity, the workflow is persisted. +When the appropriate event occurs, the workflow is loaded from the store and resumed. + +## Why Elsa Workflows? + +One of the main goals of Elsa is to **enable workflows in any .NET application** with **minimum effort** and **maximum extensibility**. +This means that it should be easy to integrate workflow capabilities into your own application. + +### What about Azure Logic Apps? + +As powerful and as complete Azure Logic Apps is, it's available only as a managed service in Azure. Elsa on the other hand allows you to host it not only on Azure, but on any cloud provider that supports .NET Core. And of course you can host it on-premise. + +Although you can implement long-running workflows with Logic Apps, you would typically do so with splitting your workflow with multiple Logic Apps where one workflow invokes the other. This can make the logic flow a bit hard to follow. +with Elsa, you simply add triggers anywhere in the workflow, making it easier to have a complete view of your application logic. And if you want, you can still invoke other workflows form one workflow. + +### What about Windows Workflow Foundation? + +I've always liked Windows Workflow Foundation, but unfortunately [development appears to have halted](https://forums.dotnetfoundation.org/t/what-is-the-roadmap-of-workflow-foundation/3066). +Although there's an effort being made to [port WF to .NET Standard](https://github.com/dmetzgar/corewf), there are a few reasons I prefer Elsa: + +- Elsa intrinsically supports triggering events that starts new workflows and resumes halted workflow instances in an easy to use manner. E.g. `workflowHost.TriggerWorkflowAsync("HttpRequestTrigger");"` will start and resume all workflows that either start with or are halted on the `HttpRequestTrigger`. +- Elsa has a web-based workflow designer. I once worked on a project for a customer that was building a huge SaaS platform. One of the requirements was to provide a workflow engine and a web-based editor. Although there are commercial workflow libraries and editors out there, the business model required open-source software. We used WF and the re-hosted Workflow Designer. It worked, but it wasn't great. + +### What about Orchard Workflows? + +Both [Orchard](http://docs.orchardproject.net/en/latest/Documentation/Workflows/) and [Orchard Core](https://orchardcore.readthedocs.io/en/dev/OrchardCore.Modules/OrchardCore.Workflows/) ship with a powerful workflows module, and both are awesome. +In fact, Elsa Workflows is taken & adapted from Orchard Core's Workflows module. Elsa uses a similar model, but there are some differences: + +- Elsa Workflows is completely decoupled from web, whereas Orchard Core Workflows is coupled to not only the web, but also the Orchard Core Framework itself. +- Elsa Workflows can execute in any .NET Core application without taking a dependency on any Orchard Core packages (not to be confused with Elsa Workflows Designer, which takes advantage of some Orchard Core packages). +- Elsa Workflows separates activity models from activity execution logic. + +I am a big fan of Orchard Core, and its Workflows module is in my opinion one of its biggest gems. In fact, the Elsa Workflows web-based designer depends on Orchard Core Framework packages because Orchard Core is that useful! +An important roadmap item is to provide an Orchard Core module called `OrchardCore.ElsaWorkfows`, which uses Elsa's engine and web-based designer within the context of an Orchard Core application and provides Orchard Core-specific activities such as content-related triggers and actions. + +As mentioned earlier: this is one of the main reasons that Elsa exists: to enable workflows in any .NET application. Orchard Core included. +There are a few reasons I think contributing to `OrchardCore.ElsaWorkflows` makes sense: + +- Elsa potentially has a broader audience, because workflows are applicable in more environments than Orchard Core. +- Orchard Core is awesome, and `OrchardCore.Workflows` is a key feature of it. If Elsa is used more widely, it is likely to also have more community support, which means more features. + +## Features + +The following lists some of Elsa's key features: + +- **Small, simple and fast**. The library should be lean & mean, meaning that it should be **easy to use**, **fast to execute** and **easy to extend** with custom activities. +- It must be a set of **libraries**. This allows me to create my application anyway I like, and implement workflow capabilities as I see fit. Thanks to ASP.NET Core's application model however, creating a workflow designer & workflow host is as simple as referencing the right packages and making a few calls. +- Invoke arbitrary workflows as if they were **functions of my application**. +- Trigger events that cause the appropriate workflows to **automatically start/resume** based on that event. +- Support **long-running workflows**. When a workflow executes and encounters an activity that requires e.g. user input, the workflow will halt, be persisted and go out of memory until it's time to resume. this could be a few seconds later, a few minutes, hours, days or even years. +- **Correlate** workflows with application-specific data. This is a key requirement for long-running workflows. +- Store workflows in a **file-based** format so I can make it part of source-control. +- Store workflows in a **database** when I don't want to make them part of source control. +- A **web-based designer**. Whether I store my workflows on a file system or in a database, and whether I host the designer online or only on my local machine, I need to be able to edit my workflows. +- Configure workflow activities with **expressions**. Oftentimes, information being processed by a workflow is dynamic in nature, and activities need a way to interact with this information. Workflow expressions allow for this. +- **Extensible** with application-specific **activities**, **custom stores** and **scripting engines**. +- Invoke other workflows. This allows for invoking reusable application logic from various workflows. Like invoking general-purpose functions from C# without having to duplicate code. +- **View & analyze** executed workflow instances. I want to see **which path** a workflow took, its **runtime state**, where it **faulted** and **compensate** faulted workflows. +- **Embed** the web-based workflow designer in **my own dashboard** application. This gives me the option of creating a single Workflow Host that runs all of my application logic, but also the option of hosting a workflows runtime in individual micro services (allowing for orchestration as well as choreography). +- **Separation of concerns**: The workflow core library, runtime and designer should all be separated. I.e. when the workflow host should not have a dependency on the web-based designer. This allows one for example to implement a desktop-based designer, or not use a designer at all and just go with YAML files. The host in the end only needs the workflow definitions and access to persistence stores. +- **On premise** or **managed** in the cloud - both scenarios are supported, because Elsa is just a set of NuGet packages that you reference from your application. + +## How to use Elsa + +Elsa is distributed as a set of NuGet packages, which makes it easy to add to your application. +When working with Elsa, you'll typically want to have at least two applications: + +1. An ASP.NET Core application to host the workflows designer. +2. A .NET application that executed workflows + +### Setting up a Workflow Designer ASP.NET Core Application + +TODO: describe all the steps to add packages and register services. + +### Setting up a Workflow Host .NET Application + +TODO: describe all the steps to add packages and register services. + +## Running Elsa Workflows Dashboard + +In order to run Elsa on your local machine, follow these steps: + +1. Clone the repository. +2. Run NPM install on all folders containing packages.json (or run `node npm-install.js` - a script in the root that recursively installs the Node packages) +3. Open a shell and navigate to `src/samples/SampleDashboard.Web` and run `dotnet run`. +4. Navigate to https://localhost:44397/ + +## Running Elsa Workflows Host + +(TODO) + +## Roadmap + +(TODO) + +- Describe all the features (core engine, runtime, webbased designer, YAML, scripting, separation of designer from invoker from engine). +- Describe various use cases. +- Describe how to use. +- Describe architecture. +- Describe how to implement (custom host, custom dashboard). +- Implement more activities +- Implement integration with Orchard Core (separate repo) +- Detailed documentation +- Open API Activity Harvester +- MassTransit Activity Harvester +- RabbitMQ Activities +- Azure Service Bus Activities +- Automatic UI for Activity Editor + diff --git a/samples/Sample04/CalculatorWorkflow.cs b/samples/Sample04/CalculatorWorkflow.cs index 683e1d231..ca4bb9a81 100644 --- a/samples/Sample04/CalculatorWorkflow.cs +++ b/samples/Sample04/CalculatorWorkflow.cs @@ -1,74 +1,75 @@ -using Elsa; -using Elsa.Activities.Console.Activities; -using Elsa.Core.Activities.Primitives; -using Elsa.Core.Expressions; -using Elsa.Services; -using Elsa.Services.Models; -using Sample04.Activities; - -namespace Sample04 -{ - public class CalculatorWorkflow : IWorkflow - { - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith(x => x.TextExpression = new PlainTextExpression("Welcome to Calculator Workflow!")) - .Then(x => x.TextExpression = new PlainTextExpression("Enter number 1:"), id: "start") - .Then(x => x.ArgumentName = "number1") - .Then(x => x.TextExpression = new PlainTextExpression("Enter number 2:")) - .Then(x => x.ArgumentName = "number2") - .Then(x => x.TextExpression = new PlainTextExpression("Now enter the operation you wish to apply. Options are: add, subtract, multiply or divide:")) - .Then(x => x.ArgumentName = "operation") - .Then(@switch => - { - @switch.Expression = new JavaScriptExpression("operation"); - @switch.Cases = new[] { "add", "subtract", "multiply", "divide" }; - }, - @switch => - { - @switch - .When("add") - .Then(SetupOperation) - .Then("showResult"); - - @switch - .When("subtract") - .Then(SetupOperation) - .Then("showResult"); - - @switch - .When("multiply") - .Then(SetupOperation) - .Then("showResult"); - - @switch - .When("divide") - .Then(SetupOperation) - .Then("showResult"); - } - ) - .Add(x => x.TextExpression = new JavaScriptExpression("`Result: ${result}`"), "showResult") - .Then(x => x.TextExpression = new PlainTextExpression("Try again? (y/n)")) - .Then(x => x.ArgumentName = "retry") - .Then( - x => x.ConditionExpression = new JavaScriptExpression("retry.toLowerCase() === 'y'"), - ifElse => - { - ifElse - .When(OutcomeNames.True) - .Then("start"); - - ifElse - .When(OutcomeNames.False) - .Then(x => x.TextExpression = new PlainTextExpression("Bye!")); - });; - } - - private void SetupOperation(ArithmeticOperation operation) - { - operation.Values = new JavaScriptExpression("[number1, number2]"); - operation.ResultVariableName = "result"; - } - } +using Elsa; +using Elsa.Activities.Console.Activities; +using Elsa.Core.Activities.ControlFlow; +using Elsa.Core.Activities.Primitives; +using Elsa.Core.Expressions; +using Elsa.Services; +using Elsa.Services.Models; +using Sample04.Activities; + +namespace Sample04 +{ + public class CalculatorWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(x => x.TextExpression = new PlainTextExpression("Welcome to Calculator Workflow!")) + .Then(x => x.TextExpression = new PlainTextExpression("Enter number 1:"), id: "start") + .Then(x => x.ArgumentName = "number1") + .Then(x => x.TextExpression = new PlainTextExpression("Enter number 2:")) + .Then(x => x.ArgumentName = "number2") + .Then(x => x.TextExpression = new PlainTextExpression("Now enter the operation you wish to apply. Options are: add, subtract, multiply or divide:")) + .Then(x => x.ArgumentName = "operation") + .Then(@switch => + { + @switch.Expression = new JavaScriptExpression("operation"); + @switch.Cases = new[] { "add", "subtract", "multiply", "divide" }; + }, + @switch => + { + @switch + .When("add") + .Then(SetupOperation) + .Then("showResult"); + + @switch + .When("subtract") + .Then(SetupOperation) + .Then("showResult"); + + @switch + .When("multiply") + .Then(SetupOperation) + .Then("showResult"); + + @switch + .When("divide") + .Then(SetupOperation) + .Then("showResult"); + } + ) + .Add(x => x.TextExpression = new JavaScriptExpression("`Result: ${result}`"), "showResult") + .Then(x => x.TextExpression = new PlainTextExpression("Try again? (y/n)")) + .Then(x => x.ArgumentName = "retry") + .Then( + x => x.ConditionExpression = new JavaScriptExpression("retry.toLowerCase() === 'y'"), + ifElse => + { + ifElse + .When(OutcomeNames.True) + .Then("start"); + + ifElse + .When(OutcomeNames.False) + .Then(x => x.TextExpression = new PlainTextExpression("Bye!")); + });; + } + + private void SetupOperation(ArithmeticOperation operation) + { + operation.Values = new JavaScriptExpression("[number1, number2]"); + operation.ResultVariableName = "result"; + } + } } \ No newline at end of file diff --git a/samples/Sample06/HelloWorldWorkflow.cs b/samples/Sample06/HelloWorldWorkflow.cs index 3945fb7a8..9fd93356f 100644 --- a/samples/Sample06/HelloWorldWorkflow.cs +++ b/samples/Sample06/HelloWorldWorkflow.cs @@ -1,27 +1,27 @@ -using System; -using System.Net; -using Elsa.Activities.Http.Activities; -using Elsa.Core.Expressions; -using Elsa.Services; -using Elsa.Services.Models; - -namespace Sample06 -{ - public class HelloWorldWorkflow : IWorkflow - { - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith(activity => activity.Path = new Uri("/hello-world", UriKind.RelativeOrAbsolute)) - .Then( - activity => - { - activity.Body = new PlainTextExpression("

Hello World!

Elsa says hi :)

"); - activity.ContentType = new PlainTextExpression("text/html"); - activity.StatusCode = HttpStatusCode.OK; - activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows"); - } - ); - } - } +using System; +using System.Net; +using Elsa.Activities.Http.Activities; +using Elsa.Core.Expressions; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Sample06 +{ + public class HelloWorldWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith(activity => activity.Path = new Uri("/hello-world", UriKind.RelativeOrAbsolute)) + .Then( + activity => + { + activity.Content = new PlainTextExpression("

Hello World!

Elsa says hi :)

"); + activity.ContentType = new PlainTextExpression("text/html"); + activity.StatusCode = HttpStatusCode.OK; + activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows"); + } + ); + } + } } \ No newline at end of file diff --git a/samples/Sample07/DocumentApprovalWorkflow.cs b/samples/Sample07/DocumentApprovalWorkflow.cs index c8ccf0cb8..aff5bb166 100644 --- a/samples/Sample07/DocumentApprovalWorkflow.cs +++ b/samples/Sample07/DocumentApprovalWorkflow.cs @@ -1,98 +1,99 @@ -using System; -using System.Net; -using System.Net.Http; -using Elsa; -using Elsa.Activities.Email.Activities; -using Elsa.Activities.Http.Activities; -using Elsa.Core.Activities.Primitives; -using Elsa.Core.Expressions; -using Elsa.Services; -using Elsa.Services.Models; - -namespace Sample07 -{ - public class DocumentApprovalWorkflow : IWorkflow - { - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith( - activity => - { - activity.Method = HttpMethod.Post.Method; - activity.Path = new Uri("/documents", UriKind.RelativeOrAbsolute); - activity.ReadContent = true; - } - ) - .Then( - activity => - { - activity.VariableName = "document"; - activity.ValueExpression = new JavaScriptExpression("lastResult().ParsedContent"); - } - ) - .Then( - activity => - { - activity.From = new PlainTextExpression("approval@acme.com"); - activity.To = new JavaScriptExpression("document.author.email"); - activity.Subject = new JavaScriptExpression("`Document received from ${document.author.name}`"); - activity.Body = new JavaScriptExpression( - "`Document from ${document.author.name} received for review. " + - "Approve or Reject`" - ); - } - ) - .Then( - activity => - { - activity.Body = new PlainTextExpression("

Request for Approval Sent

Your document has been received and will be reviewed shortly.

"); - activity.ContentType = new PlainTextExpression("text/html"); - activity.StatusCode = HttpStatusCode.OK; - activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows"); - } - ) - .Then( - activity => { activity.Forks = new[] { "Approve", "Reject" }; }, - fork => - { - fork - .When("Approve") - .Then(activity => activity.SignalName = "approve") - .Then("join-signals"); - - fork - .When("Reject") - .Then(activity => activity.SignalName = "reject") - .Then("join-signals"); - } - ) - .Add(activity => activity.Mode = Join.JoinMode.WaitAny, "join-signals") - .Then(activity => activity.ConditionExpression = new JavaScriptExpression("input('signal') === 'approve'"), - ifElse => - { - ifElse - .When(OutcomeNames.True) - .Then( - activity => - { - activity.From = new PlainTextExpression("approval@acme.com"); - activity.To = new JavaScriptExpression("document.author.email"); - activity.Subject = new JavaScriptExpression("`Document ${document.id} approved!`"); - activity.Body = new JavaScriptExpression("`Great job ${document.author.name}, that document is perfect! Keep it up.`"); - }); - - ifElse - .When(OutcomeNames.False) - .Then( - activity => - { - activity.From = new PlainTextExpression("approval@acme.com"); - activity.To = new JavaScriptExpression("document.author.email"); - activity.Subject = new JavaScriptExpression("`Document ${document.id} rejected`"); - activity.Body = new JavaScriptExpression("`Sorry ${document.author.name}, that document isn't good enough. Please try again.`"); - }); - }); - } - } +using System; +using System.Net; +using System.Net.Http; +using Elsa; +using Elsa.Activities.Email.Activities; +using Elsa.Activities.Http.Activities; +using Elsa.Core.Activities.ControlFlow; +using Elsa.Core.Activities.Primitives; +using Elsa.Core.Expressions; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Sample07 +{ + public class DocumentApprovalWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith( + activity => + { + activity.Method = HttpMethod.Post.Method; + activity.Path = new Uri("/documents", UriKind.RelativeOrAbsolute); + activity.ReadContent = true; + } + ) + .Then( + activity => + { + activity.VariableName = "document"; + activity.ValueExpression = new JavaScriptExpression("lastResult().ParsedContent"); + } + ) + .Then( + activity => + { + activity.From = new PlainTextExpression("approval@acme.com"); + activity.To = new JavaScriptExpression("document.author.email"); + activity.Subject = new JavaScriptExpression("`Document received from ${document.author.name}`"); + activity.Body = new JavaScriptExpression( + "`Document from ${document.author.name} received for review. " + + "Approve or Reject`" + ); + } + ) + .Then( + activity => + { + activity.Content = new PlainTextExpression("

Request for Approval Sent

Your document has been received and will be reviewed shortly.

"); + activity.ContentType = new PlainTextExpression("text/html"); + activity.StatusCode = HttpStatusCode.OK; + activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows"); + } + ) + .Then( + activity => { activity.Branches = new[] { "Approve", "Reject" }; }, + fork => + { + fork + .When("Approve") + .Then(activity => activity.SignalName = "approve") + .Then("join-signals"); + + fork + .When("Reject") + .Then(activity => activity.SignalName = "reject") + .Then("join-signals"); + } + ) + .Add(activity => activity.Mode = Join.JoinMode.WaitAny, "join-signals") + .Then(activity => activity.ConditionExpression = new JavaScriptExpression("input('signal') === 'approve'"), + ifElse => + { + ifElse + .When(OutcomeNames.True) + .Then( + activity => + { + activity.From = new PlainTextExpression("approval@acme.com"); + activity.To = new JavaScriptExpression("document.author.email"); + activity.Subject = new JavaScriptExpression("`Document ${document.id} approved!`"); + activity.Body = new JavaScriptExpression("`Great job ${document.author.name}, that document is perfect! Keep it up.`"); + }); + + ifElse + .When(OutcomeNames.False) + .Then( + activity => + { + activity.From = new PlainTextExpression("approval@acme.com"); + activity.To = new JavaScriptExpression("document.author.email"); + activity.Subject = new JavaScriptExpression("`Document ${document.id} rejected`"); + activity.Body = new JavaScriptExpression("`Sorry ${document.author.name}, that document isn't good enough. Please try again.`"); + }); + }); + } + } } \ No newline at end of file diff --git a/samples/Sample08/Workflows/CreateOrderWorkflow.cs b/samples/Sample08/Workflows/CreateOrderWorkflow.cs index 9468de76e..7bd3c640e 100644 --- a/samples/Sample08/Workflows/CreateOrderWorkflow.cs +++ b/samples/Sample08/Workflows/CreateOrderWorkflow.cs @@ -1,74 +1,75 @@ -using System; -using System.Net; -using System.Net.Http; -using Elsa.Activities.Email.Activities; -using Elsa.Activities.Http.Activities; -using Elsa.Activities.MassTransit.Activities; -using Elsa.Core.Activities.Primitives; -using Elsa.Core.Expressions; -using Elsa.Services; -using Elsa.Services.Models; -using Sample08.Messages; - -namespace Sample08.Workflows -{ - public class CreateOrderWorkflow : IWorkflow - { - public void Build(IWorkflowBuilder builder) - { - builder - .StartWith( - activity => - { - activity.Method = HttpMethod.Post.Method; - activity.Path = new Uri("/orders", UriKind.RelativeOrAbsolute); - activity.ReadContent = true; - } - ) - .Then( - activity => - { - activity.VariableName = "order"; - activity.ValueExpression = new JavaScriptExpression("lastResult().ParsedContent"); - } - ) - .Then(activity => - { - activity.Message = new JavaScriptExpression("return {order: order};"); - activity.MessageType = typeof(CreateOrder); - } - ) - .Then( - activity => activity.Forks = new[] { "Write-Response", "Await-Shipment" }, - fork => - { - fork - .When("Write-Response") - .Then( - activity => - { - activity.Body = new PlainTextExpression("

Order Received

Your order has been received. Waiting for shipment.

"); - activity.ContentType = new PlainTextExpression("text/html"); - activity.StatusCode = HttpStatusCode.Accepted; - } - ); - - fork - .When("Await-Shipment") - .Then(activity => activity.MessageType = typeof(OrderShipped)) - .Then( - activity => - { - activity.From = new PlainTextExpression("shipment@acme.com"); - activity.To = new JavaScriptExpression("order.customer.email"); - activity.Subject = new JavaScriptExpression("`Your order with ID #${order.id} has been shipped!`"); - activity.Body = new JavaScriptExpression( - "`Dear ${order.customer.name}, your order has shipped!`" - ); - } - ); - } - ); - } - } +using System; +using System.Net; +using System.Net.Http; +using Elsa.Activities.Email.Activities; +using Elsa.Activities.Http.Activities; +using Elsa.Activities.MassTransit.Activities; +using Elsa.Core.Activities.ControlFlow; +using Elsa.Core.Activities.Primitives; +using Elsa.Core.Expressions; +using Elsa.Services; +using Elsa.Services.Models; +using Sample08.Messages; + +namespace Sample08.Workflows +{ + public class CreateOrderWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith( + activity => + { + activity.Method = HttpMethod.Post.Method; + activity.Path = new Uri("/orders", UriKind.RelativeOrAbsolute); + activity.ReadContent = true; + } + ) + .Then( + activity => + { + activity.VariableName = "order"; + activity.ValueExpression = new JavaScriptExpression("lastResult().ParsedContent"); + } + ) + .Then(activity => + { + activity.Message = new JavaScriptExpression("return {order: order};"); + activity.MessageType = typeof(CreateOrder); + } + ) + .Then( + activity => activity.Branches = new[] { "Write-Response", "Await-Shipment" }, + fork => + { + fork + .When("Write-Response") + .Then( + activity => + { + activity.Content = new PlainTextExpression("

Order Received

Your order has been received. Waiting for shipment.

"); + activity.ContentType = new PlainTextExpression("text/html"); + activity.StatusCode = HttpStatusCode.Accepted; + } + ); + + fork + .When("Await-Shipment") + .Then(activity => activity.MessageType = typeof(OrderShipped)) + .Then( + activity => + { + activity.From = new PlainTextExpression("shipment@acme.com"); + activity.To = new JavaScriptExpression("order.customer.email"); + activity.Subject = new JavaScriptExpression("`Your order with ID #${order.id} has been shipped!`"); + activity.Body = new JavaScriptExpression( + "`Dear ${order.customer.name}, your order has shipped!`" + ); + } + ); + } + ); + } + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpRequestTrigger.cs b/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs similarity index 95% rename from src/activities/Elsa.Activities.Http/Activities/HttpRequestTrigger.cs rename to src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs index d706478d9..eeb13ff6b 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpRequestTrigger.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpRequestEvent.cs @@ -1,116 +1,116 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Activities.Http.Extensions; -using Elsa.Activities.Http.Models; -using Elsa.Activities.Http.Services; -using Elsa.Core.Services; -using Elsa.Extensions; -using Elsa.Results; -using Elsa.Services.Models; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json.Linq; - -namespace Elsa.Activities.Http.Activities -{ - public class HttpRequestTrigger : Activity - { - public static Uri GetPath(JObject state) - { - return state.GetState(nameof(Path)); - } - - public static string GetMethod(JObject state) - { - return state.GetState(nameof(Method)); - } - - private readonly IHttpContextAccessor httpContextAccessor; - private readonly IEnumerable contentFormatters; - - public HttpRequestTrigger( - IHttpContextAccessor httpContextAccessor, - IEnumerable contentFormatters) - { - this.httpContextAccessor = httpContextAccessor; - this.contentFormatters = contentFormatters; - } - - /// - /// The path that triggers this activity. - /// - [Display(Description = "The relative path that triggers this activity.")] - [Required] - [UIHint("RelativePath")] - public Uri Path - { - get => GetState(); - set => SetState(value); - } - - /// - /// The HTTP method that triggers this activity. - /// - [Display(Description = "The HTTP method that triggers this activity.")] - [Required] - [UIHint("Dropdown")] - public string Method { - get => GetState(); - set => SetState(value); - } - - /// - /// A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. - /// The stored format depends on the content-type header. - /// - [Display(Description = "A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. The stored format depends on the content-type header.")] - public bool ReadContent - { - get => GetState(); - set => SetState(value); - } - - protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) - { - return Halt(true); - } - - protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) - { - var request = httpContextAccessor.HttpContext.Request; - var model = new HttpRequestModel - { - Path = new Uri(request.Path.ToString(), UriKind.Relative), - QueryString = request.Query.ToDictionary(x => x.Key, x => x.Value), - Headers = request.Headers.ToDictionary(x => x.Key, x => x.Value), - Method = request.Method - }; - - if (ReadContent) - { - if (request.HasFormContentType) - { - model.Form = (await request.ReadFormAsync(cancellationToken)).ToDictionary(x => x.Key, x => x.Value); - } - - var parser = SelectContentParser(request.ContentType); - var content = await request.ReadBodyAsync(); - model.Content = content; - model.ParsedContent = await parser.ParseAsync(content, request.ContentType); - } - - workflowContext.CurrentScope.LastResult = model; - - return Done(); - } - - private IContentFormatter SelectContentParser(string contentType) - { - var formatters = contentFormatters.OrderByDescending(x => x.Priority).ToList(); - return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last(); - } - } +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Extensions; +using Elsa.Activities.Http.Models; +using Elsa.Activities.Http.Services; +using Elsa.Core.Services; +using Elsa.Extensions; +using Elsa.Results; +using Elsa.Services.Models; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; + +namespace Elsa.Activities.Http.Activities +{ + public class HttpRequestEvent : Activity + { + public static Uri GetPath(JObject state) + { + return state.GetState(nameof(Path)); + } + + public static string GetMethod(JObject state) + { + return state.GetState(nameof(Method)); + } + + private readonly IHttpContextAccessor httpContextAccessor; + private readonly IEnumerable contentFormatters; + + public HttpRequestEvent( + IHttpContextAccessor httpContextAccessor, + IEnumerable contentFormatters) + { + this.httpContextAccessor = httpContextAccessor; + this.contentFormatters = contentFormatters; + } + + /// + /// The path that triggers this activity. + /// + [Display(Description = "The relative path that triggers this activity.")] + [Required] + [UIHint("RelativePath")] + public Uri Path + { + get => GetState(); + set => SetState(value); + } + + /// + /// The HTTP method that triggers this activity. + /// + [Display(Description = "The HTTP method that triggers this activity.")] + [Required] + [UIHint("Dropdown")] + public string Method { + get => GetState(); + set => SetState(value); + } + + /// + /// A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. + /// The stored format depends on the content-type header. + /// + [Display(Description = "A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model. The stored format depends on the content-type header.")] + public bool ReadContent + { + get => GetState(); + set => SetState(value); + } + + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) + { + return Halt(true); + } + + protected override async Task OnResumeAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var request = httpContextAccessor.HttpContext.Request; + var model = new HttpRequestModel + { + Path = new Uri(request.Path.ToString(), UriKind.Relative), + QueryString = request.Query.ToDictionary(x => x.Key, x => x.Value), + Headers = request.Headers.ToDictionary(x => x.Key, x => x.Value), + Method = request.Method + }; + + if (ReadContent) + { + if (request.HasFormContentType) + { + model.Form = (await request.ReadFormAsync(cancellationToken)).ToDictionary(x => x.Key, x => x.Value); + } + + var parser = SelectContentParser(request.ContentType); + var content = await request.ReadBodyAsync(); + model.Content = content; + model.ParsedContent = await parser.ParseAsync(content, request.ContentType); + } + + workflowContext.CurrentScope.LastResult = model; + + return Done(); + } + + private IContentFormatter SelectContentParser(string contentType) + { + var formatters = contentFormatters.OrderByDescending(x => x.Priority).ToList(); + return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(contentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last(); + } + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpResponseTask.cs similarity index 86% rename from src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs rename to src/activities/Elsa.Activities.Http/Activities/HttpResponseTask.cs index 9e8b180c0..ed4501ed9 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpResponseTask.cs @@ -1,97 +1,97 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Core.Expressions; -using Elsa.Core.Extensions; -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services; -using Elsa.Services.Models; -using Microsoft.AspNetCore.Http; - -namespace Elsa.Activities.Http.Activities -{ - public class HttpResponseAction : Activity - { - private readonly IWorkflowExpressionEvaluator expressionEvaluator; - private readonly IHttpContextAccessor httpContextAccessor; - - public HttpResponseAction(IWorkflowExpressionEvaluator expressionEvaluator, IHttpContextAccessor httpContextAccessor) - { - this.expressionEvaluator = expressionEvaluator; - this.httpContextAccessor = httpContextAccessor; - } - - /// - /// The HTTP status code to return. - /// - public HttpStatusCode StatusCode - { - get => GetState(() => HttpStatusCode.OK); - set => SetState(value); - } - - /// - /// The body to send along with the response - /// - public WorkflowExpression Body - { - get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); - set => SetState(value); - } - - /// - /// The Content-Type header to send along with the response. - /// - public WorkflowExpression ContentType - { - get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); - set => SetState(value); - } - - /// - /// The headers to send along with the response, one header: value pair per line. - /// - public WorkflowExpression ResponseHeaders - { - get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); - set => SetState(value); - } - - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) - { - var response = httpContextAccessor.HttpContext.Response; - - response.StatusCode = (int) StatusCode; - response.ContentType = await expressionEvaluator.EvaluateAsync(ContentType, workflowContext, cancellationToken); - - var headersText = await expressionEvaluator.EvaluateAsync(ResponseHeaders, workflowContext, cancellationToken); - - if (headersText != null) - { - var headersQuery = - from line in Regex.Split(headersText, "\\n", RegexOptions.Multiline) - let pair = line.Split(':', '=') - select new KeyValuePair(pair[0], pair[1]); - - foreach (var header in headersQuery) - { - var headerValueExpression = new WorkflowExpression(ResponseHeaders.Syntax, header.Value); - response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); - } - } - - var bodyText = await expressionEvaluator.EvaluateAsync(Body, workflowContext, cancellationToken); - - if (!string.IsNullOrWhiteSpace(bodyText)) - { - await response.WriteAsync(bodyText, cancellationToken); - } - - return Done(); - } - } +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Core.Expressions; +using Elsa.Core.Extensions; +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services; +using Elsa.Services.Models; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Http.Activities +{ + public class HttpResponseTask : Activity + { + private readonly IWorkflowExpressionEvaluator expressionEvaluator; + private readonly IHttpContextAccessor httpContextAccessor; + + public HttpResponseTask(IWorkflowExpressionEvaluator expressionEvaluator, IHttpContextAccessor httpContextAccessor) + { + this.expressionEvaluator = expressionEvaluator; + this.httpContextAccessor = httpContextAccessor; + } + + /// + /// The HTTP status code to return. + /// + public HttpStatusCode StatusCode + { + get => GetState(() => HttpStatusCode.OK); + set => SetState(value); + } + + /// + /// The content to send along with the response + /// + public WorkflowExpression Content + { + get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); + set => SetState(value); + } + + /// + /// The Content-Type header to send along with the response. + /// + public WorkflowExpression ContentType + { + get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); + set => SetState(value); + } + + /// + /// The headers to send along with the response. One 'header: value' pair per line. + /// + public WorkflowExpression ResponseHeaders + { + get => GetState(() => new WorkflowExpression(PlainTextEvaluator.SyntaxName, "")); + set => SetState(value); + } + + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var response = httpContextAccessor.HttpContext.Response; + + response.StatusCode = (int) StatusCode; + response.ContentType = await expressionEvaluator.EvaluateAsync(ContentType, workflowContext, cancellationToken); + + var headersText = await expressionEvaluator.EvaluateAsync(ResponseHeaders, workflowContext, cancellationToken); + + if (headersText != null) + { + var headersQuery = + from line in Regex.Split(headersText, "\\n", RegexOptions.Multiline) + let pair = line.Split(':', '=') + select new KeyValuePair(pair[0], pair[1]); + + foreach (var header in headersQuery) + { + var headerValueExpression = new WorkflowExpression(ResponseHeaders.Syntax, header.Value); + response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); + } + } + + var bodyText = await expressionEvaluator.EvaluateAsync(Content, workflowContext, cancellationToken); + + if (!string.IsNullOrWhiteSpace(bodyText)) + { + await response.WriteAsync(bodyText, cancellationToken); + } + + return Done(); + } + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj index 15587447e..36eff2f71 100644 --- a/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj +++ b/src/activities/Elsa.Activities.Http/Elsa.Activities.Http.csproj @@ -1,30 +1,30 @@ - - - - netstandard2.0 - 1.0.0 - Sipke Schoorstra - Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application. - 2019 - https://github.com/sfmskywalker/Elsa - https://github.com/sfmskywalker/Elsa - GitHub - elsa, workflows, orchard - - - - - - - - - - - - - - - - - - + + + + netstandard2.0 + 1.0.0 + Sipke Schoorstra + Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application. + 2019 + https://github.com/sfmskywalker/Elsa + https://github.com/sfmskywalker/Elsa + GitHub + elsa, workflows, orchard + + + + + + + + + + + + + + + + + + diff --git a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs index 140c5551c..75bf9091c 100644 --- a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs @@ -1,49 +1,49 @@ -using Elsa.Activities.Http.Activities; -using Elsa.Activities.Http.Formatters; -using Elsa.Activities.Http.RequestHandlers.Handlers; -using Elsa.Activities.Http.Scripting; -using Elsa.Activities.Http.Services; -using Elsa.Core.Extensions; -using Elsa.Scripting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Elsa.Activities.Http.Extensions -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddHttpActivities(this IServiceCollection services) - { - services.TryAddSingleton(); - services.AddHttpClient(nameof(HttpRequestAction)); - - services - .AddActivity() - .AddActivity() - .AddActivity() - .AddActivity(); - - services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddHttpContextAccessor() - .AddDataProtection(); - - return services - .AddScoped(sp => sp.GetRequiredService().HttpContext) - .AddRequestHandler() - .AddRequestHandler(); - } - - public static IServiceCollection AddRequestHandler(this IServiceCollection services) where THandler : class, IRequestHandler - { - return services.AddScoped(); - } - } +using Elsa.Activities.Http.Activities; +using Elsa.Activities.Http.Formatters; +using Elsa.Activities.Http.RequestHandlers.Handlers; +using Elsa.Activities.Http.Scripting; +using Elsa.Activities.Http.Services; +using Elsa.Core.Extensions; +using Elsa.Scripting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Elsa.Activities.Http.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddHttpActivities(this IServiceCollection services) + { + services.TryAddSingleton(); + services.AddHttpClient(nameof(HttpRequestAction)); + + services + .AddActivity() + .AddActivity() + .AddActivity() + .AddActivity(); + + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddHttpContextAccessor() + .AddDataProtection(); + + return services + .AddScoped(sp => sp.GetRequiredService().HttpContext) + .AddRequestHandler() + .AddRequestHandler(); + } + + public static IServiceCollection AddRequestHandler(this IServiceCollection services) where THandler : class, IRequestHandler + { + return services.AddScoped(); + } + } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/RequestHandlers/Handlers/TriggerRequestHandler.cs b/src/activities/Elsa.Activities.Http/RequestHandlers/Handlers/TriggerRequestHandler.cs index 49ab9674d..0e5e29cae 100644 --- a/src/activities/Elsa.Activities.Http/RequestHandlers/Handlers/TriggerRequestHandler.cs +++ b/src/activities/Elsa.Activities.Http/RequestHandlers/Handlers/TriggerRequestHandler.cs @@ -1,93 +1,93 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Activities.Http.Activities; -using Elsa.Activities.Http.RequestHandlers.Results; -using Elsa.Activities.Http.Services; -using Elsa.Core.Extensions; -using Elsa.Models; -using Elsa.Persistence; -using Elsa.Serialization.Models; -using Elsa.Services; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json.Linq; - -namespace Elsa.Activities.Http.RequestHandlers.Handlers -{ - public class TriggerRequestHandler : IRequestHandler - { - private readonly HttpContext httpContext; - private readonly IWorkflowInvoker workflowInvoker; - private readonly IWorkflowRegistry registry; - private readonly IWorkflowInstanceStore workflowInstanceStore; - private readonly CancellationToken cancellationToken; - - public TriggerRequestHandler(HttpContext httpContext, - IWorkflowInvoker workflowInvoker, - IWorkflowRegistry registry, - IWorkflowInstanceStore workflowInstanceStore) - { - this.httpContext = httpContext; - this.workflowInvoker = workflowInvoker; - this.registry = registry; - this.workflowInstanceStore = workflowInstanceStore; - this.cancellationToken = httpContext.RequestAborted; - } - - public async Task HandleRequestAsync() - { - // TODO: Optimize this by building up a hash of routes and workflows to execute. - var requestPath = new Uri(httpContext.Request.Path.ToString(), UriKind.Relative); - var method = httpContext.Request.Method; - var workflowsToStart = Filter(registry.ListByStartActivity(nameof(HttpRequestTrigger)), requestPath, method).ToList(); - var workflowsToResume = Filter(await workflowInstanceStore.ListByBlockingActivityAsync(cancellationToken), requestPath, method).ToList(); - - if (!workflowsToStart.Any() && !workflowsToResume.Any()) - { - return new NextResult(); - } - - await InvokeWorkflowsToStartAsync(workflowsToStart); - await InvokeWorkflowsToResumeAsync(workflowsToResume); - - return !httpContext.Items.ContainsKey(WorkflowHttpResult.Instance) - ? (IRequestHandlerResult) new AcceptedResult() - : new EmptyResult(); - } - - private IEnumerable<(WorkflowInstance, ActivityInstance)> Filter(IEnumerable<(WorkflowInstance, ActivityInstance)> items, Uri path, string method) - { - return items.Where(x => IsMatch(x.Item2.State, path, method)); - } - - private IEnumerable<(WorkflowDefinition, ActivityDefinition)> Filter(IEnumerable<(WorkflowDefinition, ActivityDefinition)> items, Uri path, string method) - { - return items.Where(x => IsMatch(x.Item2.State, path, method)); - } - - private bool IsMatch(JObject state, Uri path, string method) - { - var m = HttpRequestTrigger.GetMethod(state); - var p = HttpRequestTrigger.GetPath(state); - return (string.IsNullOrWhiteSpace(m) || m == method) && p == path; - } - - private async Task InvokeWorkflowsToStartAsync(IEnumerable<(WorkflowDefinition, ActivityDefinition)> items) - { - foreach (var item in items) - { - await workflowInvoker.InvokeAsync(item.Item1, Variables.Empty, startActivityIds: new[] { item.Item2.Id }, cancellationToken: cancellationToken); - } - } - - private async Task InvokeWorkflowsToResumeAsync(IEnumerable<(WorkflowInstance, ActivityInstance)> items) - { - foreach (var (workflowInstance, activity) in items) - { - await workflowInvoker.ResumeAsync(workflowInstance, Variables.Empty, new[]{activity.Id}, cancellationToken); - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Activities; +using Elsa.Activities.Http.RequestHandlers.Results; +using Elsa.Activities.Http.Services; +using Elsa.Core.Extensions; +using Elsa.Models; +using Elsa.Persistence; +using Elsa.Serialization.Models; +using Elsa.Services; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json.Linq; + +namespace Elsa.Activities.Http.RequestHandlers.Handlers +{ + public class TriggerRequestHandler : IRequestHandler + { + private readonly HttpContext httpContext; + private readonly IWorkflowInvoker workflowInvoker; + private readonly IWorkflowRegistry registry; + private readonly IWorkflowInstanceStore workflowInstanceStore; + private readonly CancellationToken cancellationToken; + + public TriggerRequestHandler(HttpContext httpContext, + IWorkflowInvoker workflowInvoker, + IWorkflowRegistry registry, + IWorkflowInstanceStore workflowInstanceStore) + { + this.httpContext = httpContext; + this.workflowInvoker = workflowInvoker; + this.registry = registry; + this.workflowInstanceStore = workflowInstanceStore; + this.cancellationToken = httpContext.RequestAborted; + } + + public async Task HandleRequestAsync() + { + // TODO: Optimize this by building up a hash of routes and workflows to execute. + var requestPath = new Uri(httpContext.Request.Path.ToString(), UriKind.Relative); + var method = httpContext.Request.Method; + var workflowsToStart = Filter(registry.ListByStartActivity(nameof(HttpRequestEvent)), requestPath, method).ToList(); + var workflowsToResume = Filter(await workflowInstanceStore.ListByBlockingActivityAsync(cancellationToken), requestPath, method).ToList(); + + if (!workflowsToStart.Any() && !workflowsToResume.Any()) + { + return new NextResult(); + } + + await InvokeWorkflowsToStartAsync(workflowsToStart); + await InvokeWorkflowsToResumeAsync(workflowsToResume); + + return !httpContext.Items.ContainsKey(WorkflowHttpResult.Instance) + ? (IRequestHandlerResult) new AcceptedResult() + : new EmptyResult(); + } + + private IEnumerable<(WorkflowInstance, ActivityInstance)> Filter(IEnumerable<(WorkflowInstance, ActivityInstance)> items, Uri path, string method) + { + return items.Where(x => IsMatch(x.Item2.State, path, method)); + } + + private IEnumerable<(WorkflowDefinition, ActivityDefinition)> Filter(IEnumerable<(WorkflowDefinition, ActivityDefinition)> items, Uri path, string method) + { + return items.Where(x => IsMatch(x.Item2.State, path, method)); + } + + private bool IsMatch(JObject state, Uri path, string method) + { + var m = HttpRequestEvent.GetMethod(state); + var p = HttpRequestEvent.GetPath(state); + return (string.IsNullOrWhiteSpace(m) || m == method) && p == path; + } + + private async Task InvokeWorkflowsToStartAsync(IEnumerable<(WorkflowDefinition, ActivityDefinition)> items) + { + foreach (var item in items) + { + await workflowInvoker.InvokeAsync(item.Item1, Variables.Empty, startActivityIds: new[] { item.Item2.Id }, cancellationToken: cancellationToken); + } + } + + private async Task InvokeWorkflowsToResumeAsync(IEnumerable<(WorkflowInstance, ActivityInstance)> items) + { + foreach (var (workflowInstance, activity) in items) + { + await workflowInvoker.ResumeAsync(workflowInstance, Variables.Empty, new[]{activity.Id}, cancellationToken); + } + } + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/ForEach.cs b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs similarity index 89% rename from src/core/Elsa.Core/Activities/Primitives/ForEach.cs rename to src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs index 0980d04f5..de053aded 100644 --- a/src/core/Elsa.Core/Activities/Primitives/ForEach.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/ForEach.cs @@ -1,22 +1,22 @@ -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services; -using Elsa.Services.Models; - -namespace Elsa.Core.Activities.Primitives -{ - public class ForEach : Activity - { - private readonly IWorkflowExpressionEvaluator expressionEvaluator; - - public ForEach(IWorkflowExpressionEvaluator expressionEvaluator) - { - this.expressionEvaluator = expressionEvaluator; - } - - protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) - { - return Done(); - } - } +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Core.Activities.ControlFlow +{ + public class ForEach : Activity + { + private readonly IWorkflowExpressionEvaluator expressionEvaluator; + + public ForEach(IWorkflowExpressionEvaluator expressionEvaluator) + { + this.expressionEvaluator = expressionEvaluator; + } + + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) + { + return Done(); + } + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/Fork.cs b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs similarity index 74% rename from src/core/Elsa.Core/Activities/Primitives/Fork.cs rename to src/core/Elsa.Core/Activities/ControlFlow/Fork.cs index f798631f9..8b74733cc 100644 --- a/src/core/Elsa.Core/Activities/Primitives/Fork.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Fork.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services.Models; - -namespace Elsa.Core.Activities.Primitives -{ - public class Fork : Activity - { - public IList Forks - { - get => GetState(() => new List()); - set => SetState(value); - } - - protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) - { - return Outcomes(Forks); - } - } +using System.Collections.Generic; +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services.Models; + +namespace Elsa.Core.Activities.ControlFlow +{ + public class Fork : Activity + { + public IList Branches + { + get => GetState(() => new List()); + set => SetState(value); + } + + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) + { + return Outcomes(Branches); + } + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/IfElse.cs b/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs similarity index 93% rename from src/core/Elsa.Core/Activities/Primitives/IfElse.cs rename to src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs index 182187f69..bdee5c246 100644 --- a/src/core/Elsa.Core/Activities/Primitives/IfElse.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/IfElse.cs @@ -1,33 +1,33 @@ -using System.Threading; -using System.Threading.Tasks; -using Elsa.Core.Expressions; -using Elsa.Core.Extensions; -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services; -using Elsa.Services.Models; - -namespace Elsa.Core.Activities.Primitives -{ - public class IfElse : Activity - { - private readonly IWorkflowExpressionEvaluator expressionEvaluator; - - public IfElse(IWorkflowExpressionEvaluator expressionEvaluator) - { - this.expressionEvaluator = expressionEvaluator; - } - - public WorkflowExpression ConditionExpression - { - get => GetState(() => new JavaScriptExpression("true")); - set => SetState(value); - } - - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) - { - var result = await expressionEvaluator.EvaluateAsync(ConditionExpression, workflowContext, cancellationToken); - return Outcome(result ? OutcomeNames.True: OutcomeNames.False); - } - } +using System.Threading; +using System.Threading.Tasks; +using Elsa.Core.Expressions; +using Elsa.Core.Extensions; +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Core.Activities.ControlFlow +{ + public class IfElse : Activity + { + private readonly IWorkflowExpressionEvaluator expressionEvaluator; + + public IfElse(IWorkflowExpressionEvaluator expressionEvaluator) + { + this.expressionEvaluator = expressionEvaluator; + } + + public WorkflowExpression ConditionExpression + { + get => GetState(() => new JavaScriptExpression("true")); + set => SetState(value); + } + + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var result = await expressionEvaluator.EvaluateAsync(ConditionExpression, workflowContext, cancellationToken); + return Outcome(result ? OutcomeNames.True: OutcomeNames.False); + } + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/Join.cs b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs similarity index 96% rename from src/core/Elsa.Core/Activities/Primitives/Join.cs rename to src/core/Elsa.Core/Activities/ControlFlow/Join.cs index 250562cfe..b13fc914d 100644 --- a/src/core/Elsa.Core/Activities/Primitives/Join.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Join.cs @@ -1,114 +1,114 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services; -using Elsa.Services.Extensions; -using Elsa.Services.Models; - -namespace Elsa.Core.Activities.Primitives -{ - public class Join : Activity, IWorkflowEventHandler - { - public Join() - { - InboundTransitions = new List().AsReadOnly(); - } - - public enum JoinMode - { - WaitAll, - WaitAny - } - - public JoinMode Mode - { - get => GetState(() => JoinMode.WaitAll); - set => SetState(value); - } - - public IReadOnlyCollection InboundTransitions - { - get => GetState>(); - set => SetState(value); - } - - protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) - { - var recordedInboundTransitions = InboundTransitions ?? new List(); - var workflow = workflowContext.Workflow; - var inboundConnections = workflow.GetInboundConnections(Id); - var done = false; - - switch (Mode) - { - case JoinMode.WaitAll: - done = inboundConnections.All(x => recordedInboundTransitions.Contains(GetTransitionKey(x))); - break; - case JoinMode.WaitAny: - done = inboundConnections.Any(x => recordedInboundTransitions.Contains(GetTransitionKey(x))); - - if (done) - { - // Remove any inbound blocking activities. - var ancestorActivityIds = workflow.GetInboundActivityPath(Id).ToList(); - var blockingActivities = workflow.BlockingActivities.Where(x => ancestorActivityIds.Contains(x.Id)).ToList(); - - foreach (var blockingActivity in blockingActivities) - { - workflow.BlockingActivities.Remove(blockingActivity); - } - } - - break; - } - - return done ? Done() : Noop(); - } - - private void RecordInboundTransitions(WorkflowExecutionContext workflowContext, IActivity activity) - { - var workflow = workflowContext.Workflow; - - // Get outbound connections of the executing activity. - var outboundConnections = workflow.GetOutboundConnections(activity.Id); - - // Get any connection that is pointing to this activity. - var inboundTransitionsQuery = - from connection in outboundConnections - let destinationActivity = connection.Target.Activity - where destinationActivity.TypeName == nameof(Join) - select connection; - - var inboundConnections = inboundTransitionsQuery.ToList(); - - // For each inbound connection, record the transition. - foreach (var inboundConnection in inboundConnections) - { - var joinActivity = (Join) inboundConnection.Target.Activity; - var inboundTransitions = joinActivity.InboundTransitions ?? new List(); - joinActivity.InboundTransitions = inboundTransitions.Union(new[] { GetTransitionKey(inboundConnection) }).Distinct().ToList(); - } - } - - private string GetTransitionKey(Connection connection) - { - var sourceActivityId = connection.Source.Activity.Id; - var sourceOutcomeName = connection.Source.Outcome; - - return $"@{sourceActivityId}_{sourceOutcomeName}"; - } - - Task IWorkflowEventHandler.ActivityExecutedAsync(WorkflowExecutionContext workflowContext, IActivity activity, CancellationToken cancellationToken) - { - RecordInboundTransitions(workflowContext, activity); - - return Task.CompletedTask; - } - - Task IWorkflowEventHandler.InvokingHaltedActivitiesAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; - Task IWorkflowEventHandler.WorkflowInvokedAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; - } +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services; +using Elsa.Services.Extensions; +using Elsa.Services.Models; + +namespace Elsa.Core.Activities.ControlFlow +{ + public class Join : Activity, IWorkflowEventHandler + { + public Join() + { + InboundTransitions = new List().AsReadOnly(); + } + + public enum JoinMode + { + WaitAll, + WaitAny + } + + public JoinMode Mode + { + get => GetState(() => JoinMode.WaitAll); + set => SetState(value); + } + + public IReadOnlyCollection InboundTransitions + { + get => GetState>(); + set => SetState(value); + } + + protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext) + { + var recordedInboundTransitions = InboundTransitions ?? new List(); + var workflow = workflowContext.Workflow; + var inboundConnections = workflow.GetInboundConnections(Id); + var done = false; + + switch (Mode) + { + case JoinMode.WaitAll: + done = inboundConnections.All(x => recordedInboundTransitions.Contains(GetTransitionKey(x))); + break; + case JoinMode.WaitAny: + done = inboundConnections.Any(x => recordedInboundTransitions.Contains(GetTransitionKey(x))); + + if (done) + { + // Remove any inbound blocking activities. + var ancestorActivityIds = workflow.GetInboundActivityPath(Id).ToList(); + var blockingActivities = workflow.BlockingActivities.Where(x => ancestorActivityIds.Contains(x.Id)).ToList(); + + foreach (var blockingActivity in blockingActivities) + { + workflow.BlockingActivities.Remove(blockingActivity); + } + } + + break; + } + + return done ? Done() : Noop(); + } + + private void RecordInboundTransitions(WorkflowExecutionContext workflowContext, IActivity activity) + { + var workflow = workflowContext.Workflow; + + // Get outbound connections of the executing activity. + var outboundConnections = workflow.GetOutboundConnections(activity.Id); + + // Get any connection that is pointing to this activity. + var inboundTransitionsQuery = + from connection in outboundConnections + let destinationActivity = connection.Target.Activity + where destinationActivity.TypeName == nameof(Join) + select connection; + + var inboundConnections = inboundTransitionsQuery.ToList(); + + // For each inbound connection, record the transition. + foreach (var inboundConnection in inboundConnections) + { + var joinActivity = (Join) inboundConnection.Target.Activity; + var inboundTransitions = joinActivity.InboundTransitions ?? new List(); + joinActivity.InboundTransitions = inboundTransitions.Union(new[] { GetTransitionKey(inboundConnection) }).Distinct().ToList(); + } + } + + private string GetTransitionKey(Connection connection) + { + var sourceActivityId = connection.Source.Activity.Id; + var sourceOutcomeName = connection.Source.Outcome; + + return $"@{sourceActivityId}_{sourceOutcomeName}"; + } + + Task IWorkflowEventHandler.ActivityExecutedAsync(WorkflowExecutionContext workflowContext, IActivity activity, CancellationToken cancellationToken) + { + RecordInboundTransitions(workflowContext, activity); + + return Task.CompletedTask; + } + + Task IWorkflowEventHandler.InvokingHaltedActivitiesAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; + Task IWorkflowEventHandler.WorkflowInvokedAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) => Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Activities/Primitives/Switch.cs b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs similarity index 93% rename from src/core/Elsa.Core/Activities/Primitives/Switch.cs rename to src/core/Elsa.Core/Activities/ControlFlow/Switch.cs index 69ce5042b..70827fe6c 100644 --- a/src/core/Elsa.Core/Activities/Primitives/Switch.cs +++ b/src/core/Elsa.Core/Activities/ControlFlow/Switch.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Elsa.Core.Expressions; -using Elsa.Core.Extensions; -using Elsa.Core.Services; -using Elsa.Results; -using Elsa.Services; -using Elsa.Services.Models; - -namespace Elsa.Core.Activities.Primitives -{ - public class Switch : Activity - { - private readonly IWorkflowExpressionEvaluator expressionEvaluator; - - public Switch(IWorkflowExpressionEvaluator expressionEvaluator) - { - this.expressionEvaluator = expressionEvaluator; - Cases = new List(); - } - - public WorkflowExpression Expression - { - get => GetState>(); - set => SetState(value); - } - - public IReadOnlyCollection Cases - { - get => GetState>(); - set => SetState(value); - } - - protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) - { - var result = await expressionEvaluator.EvaluateAsync(Expression, workflowContext, cancellationToken); - return Outcome(result); - } - } +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Core.Expressions; +using Elsa.Core.Extensions; +using Elsa.Core.Services; +using Elsa.Results; +using Elsa.Services; +using Elsa.Services.Models; + +namespace Elsa.Core.Activities.ControlFlow +{ + public class Switch : Activity + { + private readonly IWorkflowExpressionEvaluator expressionEvaluator; + + public Switch(IWorkflowExpressionEvaluator expressionEvaluator) + { + this.expressionEvaluator = expressionEvaluator; + Cases = new List(); + } + + public WorkflowExpression Expression + { + get => GetState>(); + set => SetState(value); + } + + public IReadOnlyCollection Cases + { + get => GetState>(); + set => SetState(value); + } + + protected override async Task OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var result = await expressionEvaluator.EvaluateAsync(Expression, workflowContext, cancellationToken); + return Outcome(result); + } + } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Elsa.Core.csproj b/src/core/Elsa.Core/Elsa.Core.csproj index 3bf66c6f2..287ca3e6a 100644 --- a/src/core/Elsa.Core/Elsa.Core.csproj +++ b/src/core/Elsa.Core/Elsa.Core.csproj @@ -1,32 +1,32 @@ - - - - netstandard2.0 - 7.1 - 1.0.0 - Sipke Schoorstra - Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application. - 2019 - https://github.com/sfmskywalker/Elsa - https://github.com/sfmskywalker/Elsa - GitHub - elsa, workflows, orchard - - - - - - - - - - - - - - - - - - + + + + netstandard2.0 + 7.1 + 1.0.0 + Sipke Schoorstra + Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application. + 2019 + https://github.com/sfmskywalker/Elsa + https://github.com/sfmskywalker/Elsa + GitHub + elsa, workflows, orchard + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs index 5c2038ce0..401db05d0 100644 --- a/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,69 +1,70 @@ -using System; -using Elsa.Core.Activities.Primitives; -using Elsa.Core.Expressions; -using Elsa.Core.Scripting; -using Elsa.Core.Serialization; -using Elsa.Core.Serialization.Formatters; -using Elsa.Core.Services; -using Elsa.Core.Services.WorkflowBuilders; -using Elsa.Scripting; -using Elsa.Serialization; -using Elsa.Serialization.Formatters; -using Elsa.Services; -using Elsa.Services.Models; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using NodaTime; - -namespace Elsa.Core.Extensions -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddWorkflows(this IServiceCollection services) - { - services.TryAddSingleton(SystemClock.Instance); - - return services - .AddLogging() - .AddLocalization() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddTransient() - .AddSingleton>(sp => sp.GetRequiredService) - .AddSingleton() - .AddPrimitiveActivities(); - } - - public static IServiceCollection AddActivity(this IServiceCollection services) - where T : class, IActivity - { - return services - .AddTransient() - .AddTransient(sp => sp.GetRequiredService()); - } - - private static IServiceCollection AddPrimitiveActivities(this IServiceCollection services) - { - return services - .AddActivity() - .AddActivity() - .AddActivity() - .AddActivity() - .AddSingleton(sp => sp.GetRequiredService()) - .AddActivity() - .AddActivity(); - } - } +using System; +using Elsa.Core.Activities.ControlFlow; +using Elsa.Core.Activities.Primitives; +using Elsa.Core.Expressions; +using Elsa.Core.Scripting; +using Elsa.Core.Serialization; +using Elsa.Core.Serialization.Formatters; +using Elsa.Core.Services; +using Elsa.Core.Services.WorkflowBuilders; +using Elsa.Scripting; +using Elsa.Serialization; +using Elsa.Serialization.Formatters; +using Elsa.Services; +using Elsa.Services.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NodaTime; + +namespace Elsa.Core.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddWorkflows(this IServiceCollection services) + { + services.TryAddSingleton(SystemClock.Instance); + + return services + .AddLogging() + .AddLocalization() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddTransient() + .AddSingleton>(sp => sp.GetRequiredService) + .AddSingleton() + .AddPrimitiveActivities(); + } + + public static IServiceCollection AddActivity(this IServiceCollection services) + where T : class, IActivity + { + return services + .AddTransient() + .AddTransient(sp => sp.GetRequiredService()); + } + + private static IServiceCollection AddPrimitiveActivities(this IServiceCollection services) + { + return services + .AddActivity() + .AddActivity() + .AddActivity() + .AddActivity() + .AddSingleton(sp => sp.GetRequiredService()) + .AddActivity() + .AddActivity(); + } + } } \ No newline at end of file