Updated readme and file formatting

This commit is contained in:
Sipke Schoorstra 2019-07-14 12:07:58 +02:00
parent 7cab475321
commit dafcc99c4e
17 changed files with 1254 additions and 1255 deletions

555
README.md
View file

@ -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<HelloWorld>()
.Then<GoodByeWorld>();
}
}
// Setup a service collection.
var services = new ServiceCollection()
.AddWorkflows()
.AddActivity<HelloWorld>()
.AddActivity<GoodByeWorld>()
.BuildServiceProvider();
// Invoke the workflow.
var invoker = services.GetService<IWorkflowInvoker>();
await invoker.InvokeAsync<HelloWorldWorkflow>();
// 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<IWorkflowSerializer>();
var workflowDefinition = await serializer.DeserializeAsync(data, format, CancellationToken.None);
// Invoke the workflow.
var invoker = services.GetService<IWorkflowInvoker>();
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<HelloWorld>()
.Then<GoodByeWorld>();
}
}
// Setup a service collection.
var services = new ServiceCollection()
.AddWorkflows()
.AddActivity<HelloWorld>()
.AddActivity<GoodByeWorld>()
.BuildServiceProvider();
// Invoke the workflow.
var invoker = services.GetService<IWorkflowInvoker>();
await invoker.InvokeAsync<HelloWorldWorkflow>();
// 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<IWorkflowSerializer>();
var workflowDefinition = await serializer.DeserializeAsync(data, format, CancellationToken.None);
// Invoke the workflow.
var invoker = services.GetService<IWorkflowInvoker>();
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

View file

@ -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<WriteLine>(x => x.TextExpression = new PlainTextExpression("Welcome to Calculator Workflow!"))
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Enter number 1:"), id: "start")
.Then<ReadLine>(x => x.ArgumentName = "number1")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Enter number 2:"))
.Then<ReadLine>(x => x.ArgumentName = "number2")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Now enter the operation you wish to apply. Options are: add, subtract, multiply or divide:"))
.Then<ReadLine>(x => x.ArgumentName = "operation")
.Then<Switch>(@switch =>
{
@switch.Expression = new JavaScriptExpression<string>("operation");
@switch.Cases = new[] { "add", "subtract", "multiply", "divide" };
},
@switch =>
{
@switch
.When("add")
.Then<Sum>(SetupOperation)
.Then("showResult");
@switch
.When("subtract")
.Then<Subtract>(SetupOperation)
.Then("showResult");
@switch
.When("multiply")
.Then<Multiply>(SetupOperation)
.Then("showResult");
@switch
.When("divide")
.Then<Divide>(SetupOperation)
.Then("showResult");
}
)
.Add<WriteLine>(x => x.TextExpression = new JavaScriptExpression<string>("`Result: ${result}`"), "showResult")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Try again? (y/n)"))
.Then<ReadLine>(x => x.ArgumentName = "retry")
.Then<IfElse>(
x => x.ConditionExpression = new JavaScriptExpression<bool>("retry.toLowerCase() === 'y'"),
ifElse =>
{
ifElse
.When(OutcomeNames.True)
.Then("start");
ifElse
.When(OutcomeNames.False)
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Bye!"));
});;
}
private void SetupOperation(ArithmeticOperation operation)
{
operation.Values = new JavaScriptExpression<double[]>("[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<WriteLine>(x => x.TextExpression = new PlainTextExpression("Welcome to Calculator Workflow!"))
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Enter number 1:"), id: "start")
.Then<ReadLine>(x => x.ArgumentName = "number1")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Enter number 2:"))
.Then<ReadLine>(x => x.ArgumentName = "number2")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Now enter the operation you wish to apply. Options are: add, subtract, multiply or divide:"))
.Then<ReadLine>(x => x.ArgumentName = "operation")
.Then<Switch>(@switch =>
{
@switch.Expression = new JavaScriptExpression<string>("operation");
@switch.Cases = new[] { "add", "subtract", "multiply", "divide" };
},
@switch =>
{
@switch
.When("add")
.Then<Sum>(SetupOperation)
.Then("showResult");
@switch
.When("subtract")
.Then<Subtract>(SetupOperation)
.Then("showResult");
@switch
.When("multiply")
.Then<Multiply>(SetupOperation)
.Then("showResult");
@switch
.When("divide")
.Then<Divide>(SetupOperation)
.Then("showResult");
}
)
.Add<WriteLine>(x => x.TextExpression = new JavaScriptExpression<string>("`Result: ${result}`"), "showResult")
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Try again? (y/n)"))
.Then<ReadLine>(x => x.ArgumentName = "retry")
.Then<IfElse>(
x => x.ConditionExpression = new JavaScriptExpression<bool>("retry.toLowerCase() === 'y'"),
ifElse =>
{
ifElse
.When(OutcomeNames.True)
.Then("start");
ifElse
.When(OutcomeNames.False)
.Then<WriteLine>(x => x.TextExpression = new PlainTextExpression("Bye!"));
});;
}
private void SetupOperation(ArithmeticOperation operation)
{
operation.Values = new JavaScriptExpression<double[]>("[number1, number2]");
operation.ResultVariableName = "result";
}
}
}

View file

@ -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<HttpRequestTrigger>(activity => activity.Path = new Uri("/hello-world", UriKind.RelativeOrAbsolute))
.Then<HttpResponseAction>(
activity =>
{
activity.Body = new PlainTextExpression("<h1>Hello World!</h1><p>Elsa says hi :)</p>");
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<HttpRequestEvent>(activity => activity.Path = new Uri("/hello-world", UriKind.RelativeOrAbsolute))
.Then<HttpResponseTask>(
activity =>
{
activity.Content = new PlainTextExpression("<h1>Hello World!</h1><p>Elsa says hi :)</p>");
activity.ContentType = new PlainTextExpression("text/html");
activity.StatusCode = HttpStatusCode.OK;
activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows");
}
);
}
}
}

View file

@ -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<HttpRequestTrigger>(
activity =>
{
activity.Method = HttpMethod.Post.Method;
activity.Path = new Uri("/documents", UriKind.RelativeOrAbsolute);
activity.ReadContent = true;
}
)
.Then<SetVariable>(
activity =>
{
activity.VariableName = "document";
activity.ValueExpression = new JavaScriptExpression<object>("lastResult().ParsedContent");
}
)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document received from ${document.author.name}`");
activity.Body = new JavaScriptExpression<string>(
"`Document from ${document.author.name} received for review. " +
"<a href=\"${signalUrl('approve')}\">Approve</a> or <a href=\"${signalUrl('reject')}\">Reject</a>`"
);
}
)
.Then<HttpResponseAction>(
activity =>
{
activity.Body = new PlainTextExpression("<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>");
activity.ContentType = new PlainTextExpression("text/html");
activity.StatusCode = HttpStatusCode.OK;
activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows");
}
)
.Then<Fork>(
activity => { activity.Forks = new[] { "Approve", "Reject" }; },
fork =>
{
fork
.When("Approve")
.Then<SignalEvent>(activity => activity.SignalName = "approve")
.Then("join-signals");
fork
.When("Reject")
.Then<SignalEvent>(activity => activity.SignalName = "reject")
.Then("join-signals");
}
)
.Add<Join>(activity => activity.Mode = Join.JoinMode.WaitAny, "join-signals")
.Then<IfElse>(activity => activity.ConditionExpression = new JavaScriptExpression<bool>("input('signal') === 'approve'"),
ifElse =>
{
ifElse
.When(OutcomeNames.True)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document ${document.id} approved!`");
activity.Body = new JavaScriptExpression<string>("`Great job ${document.author.name}, that document is perfect! Keep it up.`");
});
ifElse
.When(OutcomeNames.False)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document ${document.id} rejected`");
activity.Body = new JavaScriptExpression<string>("`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<HttpRequestEvent>(
activity =>
{
activity.Method = HttpMethod.Post.Method;
activity.Path = new Uri("/documents", UriKind.RelativeOrAbsolute);
activity.ReadContent = true;
}
)
.Then<SetVariable>(
activity =>
{
activity.VariableName = "document";
activity.ValueExpression = new JavaScriptExpression<object>("lastResult().ParsedContent");
}
)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document received from ${document.author.name}`");
activity.Body = new JavaScriptExpression<string>(
"`Document from ${document.author.name} received for review. " +
"<a href=\"${signalUrl('approve')}\">Approve</a> or <a href=\"${signalUrl('reject')}\">Reject</a>`"
);
}
)
.Then<HttpResponseTask>(
activity =>
{
activity.Content = new PlainTextExpression("<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>");
activity.ContentType = new PlainTextExpression("text/html");
activity.StatusCode = HttpStatusCode.OK;
activity.ResponseHeaders = new PlainTextExpression("X-Powered-By=Elsa Workflows");
}
)
.Then<Fork>(
activity => { activity.Branches = new[] { "Approve", "Reject" }; },
fork =>
{
fork
.When("Approve")
.Then<SignalEvent>(activity => activity.SignalName = "approve")
.Then("join-signals");
fork
.When("Reject")
.Then<SignalEvent>(activity => activity.SignalName = "reject")
.Then("join-signals");
}
)
.Add<Join>(activity => activity.Mode = Join.JoinMode.WaitAny, "join-signals")
.Then<IfElse>(activity => activity.ConditionExpression = new JavaScriptExpression<bool>("input('signal') === 'approve'"),
ifElse =>
{
ifElse
.When(OutcomeNames.True)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document ${document.id} approved!`");
activity.Body = new JavaScriptExpression<string>("`Great job ${document.author.name}, that document is perfect! Keep it up.`");
});
ifElse
.When(OutcomeNames.False)
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("approval@acme.com");
activity.To = new JavaScriptExpression<string>("document.author.email");
activity.Subject = new JavaScriptExpression<string>("`Document ${document.id} rejected`");
activity.Body = new JavaScriptExpression<string>("`Sorry ${document.author.name}, that document isn't good enough. Please try again.`");
});
});
}
}
}

View file

@ -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<HttpRequestTrigger>(
activity =>
{
activity.Method = HttpMethod.Post.Method;
activity.Path = new Uri("/orders", UriKind.RelativeOrAbsolute);
activity.ReadContent = true;
}
)
.Then<SetVariable>(
activity =>
{
activity.VariableName = "order";
activity.ValueExpression = new JavaScriptExpression<object>("lastResult().ParsedContent");
}
)
.Then<SendMassTransitMessage>(activity =>
{
activity.Message = new JavaScriptExpression<CreateOrder>("return {order: order};");
activity.MessageType = typeof(CreateOrder);
}
)
.Then<Fork>(
activity => activity.Forks = new[] { "Write-Response", "Await-Shipment" },
fork =>
{
fork
.When("Write-Response")
.Then<HttpResponseAction>(
activity =>
{
activity.Body = new PlainTextExpression("<h1>Order Received</h1><p>Your order has been received. Waiting for shipment.</p>");
activity.ContentType = new PlainTextExpression("text/html");
activity.StatusCode = HttpStatusCode.Accepted;
}
);
fork
.When("Await-Shipment")
.Then<ReceiveMassTransitMessage>(activity => activity.MessageType = typeof(OrderShipped))
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("shipment@acme.com");
activity.To = new JavaScriptExpression<string>("order.customer.email");
activity.Subject = new JavaScriptExpression<string>("`Your order with ID #${order.id} has been shipped!`");
activity.Body = new JavaScriptExpression<string>(
"`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<HttpRequestEvent>(
activity =>
{
activity.Method = HttpMethod.Post.Method;
activity.Path = new Uri("/orders", UriKind.RelativeOrAbsolute);
activity.ReadContent = true;
}
)
.Then<SetVariable>(
activity =>
{
activity.VariableName = "order";
activity.ValueExpression = new JavaScriptExpression<object>("lastResult().ParsedContent");
}
)
.Then<SendMassTransitMessage>(activity =>
{
activity.Message = new JavaScriptExpression<CreateOrder>("return {order: order};");
activity.MessageType = typeof(CreateOrder);
}
)
.Then<Fork>(
activity => activity.Branches = new[] { "Write-Response", "Await-Shipment" },
fork =>
{
fork
.When("Write-Response")
.Then<HttpResponseTask>(
activity =>
{
activity.Content = new PlainTextExpression("<h1>Order Received</h1><p>Your order has been received. Waiting for shipment.</p>");
activity.ContentType = new PlainTextExpression("text/html");
activity.StatusCode = HttpStatusCode.Accepted;
}
);
fork
.When("Await-Shipment")
.Then<ReceiveMassTransitMessage>(activity => activity.MessageType = typeof(OrderShipped))
.Then<SendEmail>(
activity =>
{
activity.From = new PlainTextExpression("shipment@acme.com");
activity.To = new JavaScriptExpression<string>("order.customer.email");
activity.Subject = new JavaScriptExpression<string>("`Your order with ID #${order.id} has been shipped!`");
activity.Body = new JavaScriptExpression<string>(
"`Dear ${order.customer.name}, your order has shipped!`"
);
}
);
}
);
}
}
}

View file

@ -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<Uri>(nameof(Path));
}
public static string GetMethod(JObject state)
{
return state.GetState<string>(nameof(Method));
}
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IEnumerable<IContentFormatter> contentFormatters;
public HttpRequestTrigger(
IHttpContextAccessor httpContextAccessor,
IEnumerable<IContentFormatter> contentFormatters)
{
this.httpContextAccessor = httpContextAccessor;
this.contentFormatters = contentFormatters;
}
/// <summary>
/// The path that triggers this activity.
/// </summary>
[Display(Description = "The relative path that triggers this activity.")]
[Required]
[UIHint("RelativePath")]
public Uri Path
{
get => GetState<Uri>();
set => SetState(value);
}
/// <summary>
/// The HTTP method that triggers this activity.
/// </summary>
[Display(Description = "The HTTP method that triggers this activity.")]
[Required]
[UIHint("Dropdown")]
public string Method {
get => GetState<string>();
set => SetState(value);
}
/// <summary>
/// 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.
/// </summary>
[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<bool>();
set => SetState(value);
}
protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext)
{
return Halt(true);
}
protected override async Task<ActivityExecutionResult> 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<Uri>(nameof(Path));
}
public static string GetMethod(JObject state)
{
return state.GetState<string>(nameof(Method));
}
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IEnumerable<IContentFormatter> contentFormatters;
public HttpRequestEvent(
IHttpContextAccessor httpContextAccessor,
IEnumerable<IContentFormatter> contentFormatters)
{
this.httpContextAccessor = httpContextAccessor;
this.contentFormatters = contentFormatters;
}
/// <summary>
/// The path that triggers this activity.
/// </summary>
[Display(Description = "The relative path that triggers this activity.")]
[Required]
[UIHint("RelativePath")]
public Uri Path
{
get => GetState<Uri>();
set => SetState(value);
}
/// <summary>
/// The HTTP method that triggers this activity.
/// </summary>
[Display(Description = "The HTTP method that triggers this activity.")]
[Required]
[UIHint("Dropdown")]
public string Method {
get => GetState<string>();
set => SetState(value);
}
/// <summary>
/// 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.
/// </summary>
[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<bool>();
set => SetState(value);
}
protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext)
{
return Halt(true);
}
protected override async Task<ActivityExecutionResult> 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();
}
}
}

View file

@ -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;
}
/// <summary>
/// The HTTP status code to return.
/// </summary>
public HttpStatusCode StatusCode
{
get => GetState(() => HttpStatusCode.OK);
set => SetState(value);
}
/// <summary>
/// The body to send along with the response
/// </summary>
public WorkflowExpression<string> Body
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
/// <summary>
/// The Content-Type header to send along with the response.
/// </summary>
public WorkflowExpression<string> ContentType
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
/// <summary>
/// The headers to send along with the response, one header: value pair per line.
/// </summary>
public WorkflowExpression<string> ResponseHeaders
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> 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<string, string>(pair[0], pair[1]);
foreach (var header in headersQuery)
{
var headerValueExpression = new WorkflowExpression<string>(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;
}
/// <summary>
/// The HTTP status code to return.
/// </summary>
public HttpStatusCode StatusCode
{
get => GetState(() => HttpStatusCode.OK);
set => SetState(value);
}
/// <summary>
/// The content to send along with the response
/// </summary>
public WorkflowExpression<string> Content
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
/// <summary>
/// The Content-Type header to send along with the response.
/// </summary>
public WorkflowExpression<string> ContentType
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
/// <summary>
/// The headers to send along with the response. One 'header: value' pair per line.
/// </summary>
public WorkflowExpression<string> ResponseHeaders
{
get => GetState(() => new WorkflowExpression<string>(PlainTextEvaluator.SyntaxName, ""));
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> 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<string, string>(pair[0], pair[1]);
foreach (var header in headersQuery)
{
var headerValueExpression = new WorkflowExpression<string>(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();
}
}
}

View file

@ -1,30 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageVersion>1.0.0</PackageVersion>
<Authors>Sipke Schoorstra</Authors>
<Description>Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application.</Description>
<Copyright>2019</Copyright>
<PackageProjectUrl>https://github.com/sfmskywalker/Elsa</PackageProjectUrl>
<RepositoryUrl>https://github.com/sfmskywalker/Elsa</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<PackageTags>elsa, workflows, orchard</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSharpFunctionalExtensions" Version="1.18.2" />
<PackageReference Include="LanguageExt.Core" Version="3.1.24" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageVersion>1.0.0</PackageVersion>
<Authors>Sipke Schoorstra</Authors>
<Description>Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application.</Description>
<Copyright>2019</Copyright>
<PackageProjectUrl>https://github.com/sfmskywalker/Elsa</PackageProjectUrl>
<RepositoryUrl>https://github.com/sfmskywalker/Elsa</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<PackageTags>elsa, workflows, orchard</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSharpFunctionalExtensions" Version="1.18.2" />
<PackageReference Include="LanguageExt.Core" Version="3.1.24" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
</ItemGroup>
</Project>

View file

@ -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<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient(nameof(HttpRequestAction));
services
.AddActivity<HttpRequestTrigger>()
.AddActivity<HttpResponseAction>()
.AddActivity<HttpRequestAction>()
.AddActivity<SignalEvent>();
services
.AddSingleton<ISharedAccessSignatureService, SharedAccessSignatureService>()
.AddSingleton<IContentFormatter, NullContentFormatter>()
.AddSingleton<IContentFormatter, JsonContentFormatter>()
.AddSingleton<IScriptEngineConfigurator, HttpScriptEngineConfigurator>()
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
.AddHttpContextAccessor()
.AddDataProtection();
return services
.AddScoped(sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext)
.AddRequestHandler<TriggerRequestHandler>()
.AddRequestHandler<SignalRequestHandler>();
}
public static IServiceCollection AddRequestHandler<THandler>(this IServiceCollection services) where THandler : class, IRequestHandler
{
return services.AddScoped<THandler>();
}
}
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<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient(nameof(HttpRequestAction));
services
.AddActivity<HttpRequestEvent>()
.AddActivity<HttpResponseTask>()
.AddActivity<HttpRequestAction>()
.AddActivity<SignalEvent>();
services
.AddSingleton<ISharedAccessSignatureService, SharedAccessSignatureService>()
.AddSingleton<IContentFormatter, NullContentFormatter>()
.AddSingleton<IContentFormatter, JsonContentFormatter>()
.AddSingleton<IScriptEngineConfigurator, HttpScriptEngineConfigurator>()
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
.AddHttpContextAccessor()
.AddDataProtection();
return services
.AddScoped(sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext)
.AddRequestHandler<TriggerRequestHandler>()
.AddRequestHandler<SignalRequestHandler>();
}
public static IServiceCollection AddRequestHandler<THandler>(this IServiceCollection services) where THandler : class, IRequestHandler
{
return services.AddScoped<THandler>();
}
}
}

View file

@ -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<IRequestHandlerResult> 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<HttpRequestTrigger>(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<IRequestHandlerResult> 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<HttpRequestEvent>(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);
}
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<string> Forks
{
get => GetState(() => new List<string>());
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<string> Branches
{
get => GetState(() => new List<string>());
set => SetState(value);
}
protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext)
{
return Outcomes(Branches);
}
}
}

View file

@ -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<bool> ConditionExpression
{
get => GetState(() => new JavaScriptExpression<bool>("true"));
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> 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<bool> ConditionExpression
{
get => GetState(() => new JavaScriptExpression<bool>("true"));
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken)
{
var result = await expressionEvaluator.EvaluateAsync(ConditionExpression, workflowContext, cancellationToken);
return Outcome(result ? OutcomeNames.True: OutcomeNames.False);
}
}
}

View file

@ -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<string>().AsReadOnly();
}
public enum JoinMode
{
WaitAll,
WaitAny
}
public JoinMode Mode
{
get => GetState(() => JoinMode.WaitAll);
set => SetState(value);
}
public IReadOnlyCollection<string> InboundTransitions
{
get => GetState<IReadOnlyCollection<string>>();
set => SetState(value);
}
protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext)
{
var recordedInboundTransitions = InboundTransitions ?? new List<string>();
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<string>();
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<string>().AsReadOnly();
}
public enum JoinMode
{
WaitAll,
WaitAny
}
public JoinMode Mode
{
get => GetState(() => JoinMode.WaitAll);
set => SetState(value);
}
public IReadOnlyCollection<string> InboundTransitions
{
get => GetState<IReadOnlyCollection<string>>();
set => SetState(value);
}
protected override ActivityExecutionResult OnExecute(WorkflowExecutionContext workflowContext)
{
var recordedInboundTransitions = InboundTransitions ?? new List<string>();
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<string>();
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;
}
}

View file

@ -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<string>();
}
public WorkflowExpression<string> Expression
{
get => GetState<WorkflowExpression<string>>();
set => SetState(value);
}
public IReadOnlyCollection<string> Cases
{
get => GetState<IReadOnlyCollection<string>>();
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> 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<string>();
}
public WorkflowExpression<string> Expression
{
get => GetState<WorkflowExpression<string>>();
set => SetState(value);
}
public IReadOnlyCollection<string> Cases
{
get => GetState<IReadOnlyCollection<string>>();
set => SetState(value);
}
protected override async Task<ActivityExecutionResult> OnExecuteAsync(WorkflowExecutionContext workflowContext, CancellationToken cancellationToken)
{
var result = await expressionEvaluator.EvaluateAsync(Expression, workflowContext, cancellationToken);
return Outcome(result);
}
}
}

View file

@ -1,32 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.1</LangVersion>
<PackageVersion>1.0.0</PackageVersion>
<Authors>Sipke Schoorstra</Authors>
<Description>Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application.</Description>
<Copyright>2019</Copyright>
<PackageProjectUrl>https://github.com/sfmskywalker/Elsa</PackageProjectUrl>
<RepositoryUrl>https://github.com/sfmskywalker/Elsa</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<PackageTags>elsa, workflows, orchard</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.2.0" />
<PackageReference Include="YamlDotNet.NetStandard" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Abstractions\Elsa.Abstractions.csproj" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.1</LangVersion>
<PackageVersion>1.0.0</PackageVersion>
<Authors>Sipke Schoorstra</Authors>
<Description>Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application.</Description>
<Copyright>2019</Copyright>
<PackageProjectUrl>https://github.com/sfmskywalker/Elsa</PackageProjectUrl>
<RepositoryUrl>https://github.com/sfmskywalker/Elsa</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<PackageTags>elsa, workflows, orchard</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.2.0" />
<PackageReference Include="YamlDotNet.NetStandard" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Abstractions\Elsa.Abstractions.csproj" />
</ItemGroup>
</Project>

View file

@ -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<IClock>(SystemClock.Instance);
return services
.AddLogging()
.AddLocalization()
.AddSingleton<IIdGenerator, IdGenerator>()
.AddSingleton<IWorkflowSerializer, WorkflowSerializer>()
.AddSingleton<ITokenFormatter, JsonTokenFormatter>()
.AddSingleton<ITokenFormatter, YamlTokenFormatter>()
.AddSingleton<ITokenFormatter, XmlTokenFormatter>()
.AddSingleton<IExpressionEvaluator, PlainTextEvaluator>()
.AddSingleton<IExpressionEvaluator, JavaScriptEvaluator>()
.AddSingleton<IScriptEngineConfigurator, CommonScriptEngineConfigurator>()
.AddSingleton<IWorkflowInvoker, WorkflowInvoker>()
.AddSingleton<IWorkflowFactory, WorkflowFactory>()
.AddSingleton<IActivityInvoker, ActivityInvoker>()
.AddSingleton<IActivityResolver, ActivityResolver>()
.AddSingleton<IWorkflowExpressionEvaluator, WorkflowExpressionEvaluator>()
.AddSingleton<IWorkflowSerializerProvider, WorkflowSerializerProvider>()
.AddTransient<IWorkflowBuilder, WorkflowBuilder>()
.AddSingleton<Func<IWorkflowBuilder>>(sp => sp.GetRequiredService<IWorkflowBuilder>)
.AddSingleton<IWorkflowRegistry, WorkflowRegistry>()
.AddPrimitiveActivities();
}
public static IServiceCollection AddActivity<T>(this IServiceCollection services)
where T : class, IActivity
{
return services
.AddTransient<T>()
.AddTransient<IActivity>(sp => sp.GetRequiredService<T>());
}
private static IServiceCollection AddPrimitiveActivities(this IServiceCollection services)
{
return services
.AddActivity<SetVariable>()
.AddActivity<ForEach>()
.AddActivity<Fork>()
.AddActivity<Join>()
.AddSingleton<IWorkflowEventHandler>(sp => sp.GetRequiredService<Join>())
.AddActivity<IfElse>()
.AddActivity<Switch>();
}
}
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<IClock>(SystemClock.Instance);
return services
.AddLogging()
.AddLocalization()
.AddSingleton<IIdGenerator, IdGenerator>()
.AddSingleton<IWorkflowSerializer, WorkflowSerializer>()
.AddSingleton<ITokenFormatter, JsonTokenFormatter>()
.AddSingleton<ITokenFormatter, YamlTokenFormatter>()
.AddSingleton<ITokenFormatter, XmlTokenFormatter>()
.AddSingleton<IExpressionEvaluator, PlainTextEvaluator>()
.AddSingleton<IExpressionEvaluator, JavaScriptEvaluator>()
.AddSingleton<IScriptEngineConfigurator, CommonScriptEngineConfigurator>()
.AddSingleton<IWorkflowInvoker, WorkflowInvoker>()
.AddSingleton<IWorkflowFactory, WorkflowFactory>()
.AddSingleton<IActivityInvoker, ActivityInvoker>()
.AddSingleton<IActivityResolver, ActivityResolver>()
.AddSingleton<IWorkflowExpressionEvaluator, WorkflowExpressionEvaluator>()
.AddSingleton<IWorkflowSerializerProvider, WorkflowSerializerProvider>()
.AddTransient<IWorkflowBuilder, WorkflowBuilder>()
.AddSingleton<Func<IWorkflowBuilder>>(sp => sp.GetRequiredService<IWorkflowBuilder>)
.AddSingleton<IWorkflowRegistry, WorkflowRegistry>()
.AddPrimitiveActivities();
}
public static IServiceCollection AddActivity<T>(this IServiceCollection services)
where T : class, IActivity
{
return services
.AddTransient<T>()
.AddTransient<IActivity>(sp => sp.GetRequiredService<T>());
}
private static IServiceCollection AddPrimitiveActivities(this IServiceCollection services)
{
return services
.AddActivity<SetVariable>()
.AddActivity<ForEach>()
.AddActivity<Fork>()
.AddActivity<Join>()
.AddSingleton<IWorkflowEventHandler>(sp => sp.GetRequiredService<Join>())
.AddActivity<IfElse>()
.AddActivity<Switch>();
}
}
}