Merge branch 'efcore-reusability' into webhook/url-management

# Conflicts:
#	src/designer/elsa-workflows-studio/package-lock.json
#	src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx
#	src/samples/server/Elsa.Samples.Server.Host/Startup.cs
This commit is contained in:
Sipke Schoorstra 2021-05-31 11:37:54 +02:00
commit 8456754e3c
681 changed files with 488047 additions and 13748 deletions

View file

@ -1,74 +0,0 @@
name: Build elsa-dashboard:[tag] and :stable
on:
push:
tags:
- 'v*.*.*'
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install NPM dependencies
uses: bahmutov/npm-install@v1
with:
useRollingCache: true
working-directory: |
src/designer/elsa-workflows-studio
src/designer/bindings/blazor/Elsa.Designer.Bindings.Blazor
- name: Build NPM packages for elsa-workflows-studio
run: npm run build
working-directory: src/designer/elsa-workflows-studio
- name: Build NPM packages for Elsa.Designer.Bindings.Blazor
run: npm run build
working-directory: src/designer/bindings/blazor/Elsa.Designer.Bindings.Blazor
- name: Generate Docker tag
id: docker-tag
uses: yuya-takeyama/docker-tag-from-github-ref-action@v1
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ghcr.io/elsa-workflows/elsa-core/elsa-dashboard:${{ steps.docker-tag.outputs.tag }}, ghcr.io/elsa-workflows/elsa-core/elsa-dashboard:stable
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -0,0 +1,65 @@
name: Build elsa-dashboard-and-server:latest
on:
workflow_dispatch:
push:
branches:
- master
pull_request:
branches:
- master
#release:
# types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-dashboard-and-server
flavor: |
latest=true
# generate Docker tags based on the following events/attributes
tags: |
type=sha
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile-elsa-dashboard-and-server
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -0,0 +1,62 @@
name: Build elsa-dashboard:latest
on:
workflow_dispatch:
push:
branches:
- master
#release:
# types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-dashboard
flavor: |
latest=true
# generate Docker tags based on the following events/attributes
tags: |
type=sha
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile-elsa-dashboard
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,73 +0,0 @@
name: Build elsa-dashboard:latest
on:
workflow_dispatch:
push:
branches:
- feature/elsa-2.0
#release:
# types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install NPM dependencies
uses: bahmutov/npm-install@v1
with:
useRollingCache: true
working-directory: |
src/designer/elsa-workflows-studio
src/designer/bindings/aspnet/Elsa.Designer.Components.Web
- name: Build NPM packages for elsa-workflows-studio
run: npm run build
working-directory: src/designer/elsa-workflows-studio
- name: Build NPM packages for Elsa.Designer.Components.Web
run: npm run build
working-directory: src/designer/bindings/aspnet/Elsa.Designer.Components.Web
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ghcr.io/elsa-workflows/elsa-core/elsa-dashboard:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -0,0 +1,62 @@
name: Build elsa-server:latest
on:
workflow_dispatch:
push:
branches:
- master
#release:
# types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
elsaworkflows/elsa-server
flavor: |
latest=true
# generate Docker tags based on the following events/attributes
tags: |
type=sha
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./docker/Dockerfile-elsa-server
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -241,8 +241,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "locking", "locking", "{DBBD
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.DistributedLocking.SqlServer", "src\locking\Elsa.DistributedLocking.SqlServer\Elsa.DistributedLocking.SqlServer.csproj", "{B09A6E42-EF42-4693-BA0E-5EE1D7F81FFD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.Redis", "src\locking\Elsa.DistributedLocking.Redis\Elsa.DistributedLocking.Redis.csproj", "{7E5514AC-3633-4237-9490-0992417F2273}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.AzureBlob", "src\locking\Elsa.DistributedLocking.AzureBlob\Elsa.DistributedLocking.AzureBlob.csproj", "{495BE954-E6EA-41A9-8054-A5CA5DD0924E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.DistributedLocking.AzureBlob", "src\locking\Elsa.DistributedLocking.AzureBlob\Elsa.DistributedLocking.AzureBlob.csproj", "{495BE954-E6EA-41A9-8054-A5CA5DD0924E}"
@ -267,8 +265,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{D7F754
ProjectSection(SolutionItems) = preProject
docker\docker-compose.yaml = docker\docker-compose.yaml
docker\Dockerfile = docker\Dockerfile
docker\Dockerfile-with-npm = docker\Dockerfile-with-npm
docker\README.md = docker\README.md
docker\Dockerfile-elsa-dashboard = docker\Dockerfile-elsa-dashboard
docker\Dockerfile-elsa-dashboard-and-server = docker\Dockerfile-elsa-dashboard-and-server
EndProjectSection
ProjectSection(SolutionItems) = preProject
docker\docker-compose.yaml = docker\docker-compose.yaml
@ -333,6 +332,33 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "clients", "clients", "{A998
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Webhooks.Client", "src\activities\Elsa.Webhooks.Client\Elsa.Webhooks.Client.csproj", "{8DA03FFB-8AE7-440E-9CDB-1EA1E93A6B4C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnetcore", "aspnetcore", "{28C5C63F-2DCF-46BE-9576-1F50A7CFE8C4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Web", "src\dashboards\aspnetcore\ElsaDashboard\ElsaDashboard.Web.csproj", "{5F81C84A-4C04-48FF-981C-23E48A7ACA6C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{3BF2C5A9-AC69-4587-833F-C16CFC97E100}"
ProjectSection(SolutionItems) = preProject
.github\workflows\publish-latest-dashboard-docker.yml = .github\workflows\publish-latest-dashboard-docker.yml
.github\workflows\publish-latest-server-docker.yml = .github\workflows\publish-latest-server-docker.yml
.github\workflows\publish-latest-dashboard-and-server-docker.yml = .github\workflows\publish-latest-dashboard-and-server-docker.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.SignalApi", "src\samples\aspnet\Elsa.Samples.SignalApi\Elsa.Samples.SignalApi.csproj", "{3E2423CF-50E6-4D2B-8749-17B1EF540FE4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "caching", "caching", "{9937FB02-72D9-4FCD-B31E-8E60CFF1B37F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching.Rebus", "src\caching\Elsa.Caching.Rebus\Elsa.Caching.Rebus.csproj", "{61C16CA0-B190-4642-A81A-5C03705CA2C1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "providers", "providers", "{EC5D4AFD-3F7F-4B51-9C38-F18C60618731}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Providers.Redis", "src\providers\Elsa.Providers.Redis\Elsa.Providers.Redis.csproj", "{9ACA06DA-9AFE-41A2-8109-EFED8B9B14A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.Redis", "src\locking\Elsa.DistributedLocking.Redis\Elsa.DistributedLocking.Redis.csproj", "{D1644D32-AA51-4D10-942E-F2722DB963B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Conductor", "src\activities\Elsa.Activities.Conductor\Elsa.Activities.Conductor.csproj", "{E62077E1-59F4-4937-B45B-59F86417BCF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleClientApp.Web", "src\samples\server\SampleClientApp.Web\SampleClientApp.Web.csproj", "{7756E22A-AD59-46C1-A019-1D28D7217347}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -671,10 +697,6 @@ Global
{B09A6E42-EF42-4693-BA0E-5EE1D7F81FFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B09A6E42-EF42-4693-BA0E-5EE1D7F81FFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B09A6E42-EF42-4693-BA0E-5EE1D7F81FFD}.Release|Any CPU.Build.0 = Release|Any CPU
{7E5514AC-3633-4237-9490-0992417F2273}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E5514AC-3633-4237-9490-0992417F2273}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E5514AC-3633-4237-9490-0992417F2273}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E5514AC-3633-4237-9490-0992417F2273}.Release|Any CPU.Build.0 = Release|Any CPU
{495BE954-E6EA-41A9-8054-A5CA5DD0924E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{495BE954-E6EA-41A9-8054-A5CA5DD0924E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{495BE954-E6EA-41A9-8054-A5CA5DD0924E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -771,6 +793,14 @@ Global
{8DA03FFB-8AE7-440E-9CDB-1EA1E93A6B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DA03FFB-8AE7-440E-9CDB-1EA1E93A6B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DA03FFB-8AE7-440E-9CDB-1EA1E93A6B4C}.Release|Any CPU.Build.0 = Release|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E62077E1-59F4-4937-B45B-59F86417BCF8}.Release|Any CPU.Build.0 = Release|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7756E22A-AD59-46C1-A019-1D28D7217347}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -885,7 +915,6 @@ Global
{A80FC28D-D865-428D-AF12-7A393D407803} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{DBBD242E-4437-4BDB-919F-A70839BE75FA} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925}
{B09A6E42-EF42-4693-BA0E-5EE1D7F81FFD} = {DBBD242E-4437-4BDB-919F-A70839BE75FA}
{7E5514AC-3633-4237-9490-0992417F2273} = {DBBD242E-4437-4BDB-919F-A70839BE75FA}
{495BE954-E6EA-41A9-8054-A5CA5DD0924E} = {DBBD242E-4437-4BDB-919F-A70839BE75FA}
{D86B94DC-A53C-4A67-A820-828DD359C49B} = {4673732F-2853-47BD-91B8-C95C229D2C89}
{C869CC72-9A98-4246-9A76-6A50F48AFD85} = {4673732F-2853-47BD-91B8-C95C229D2C89}
@ -920,6 +949,8 @@ Global
{A998F0D4-08D7-4B2A-AC17-D20E45BB40B3} = {9DBE0062-22F7-4E96-B3B0-B39D274C4A8D}
{8DA03FFB-8AE7-440E-9CDB-1EA1E93A6B4C} = {A998F0D4-08D7-4B2A-AC17-D20E45BB40B3}
{2B67E954-3B04-402D-A9A7-AAAB1D6C3215} = {9DBE0062-22F7-4E96-B3B0-B39D274C4A8D}
{E62077E1-59F4-4937-B45B-59F86417BCF8} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{7756E22A-AD59-46C1-A019-1D28D7217347} = {BBCFC623-2113-464D-8A1F-3CCDABF1241C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}

View file

@ -16,7 +16,10 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Interruptor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=localizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Materializer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=resumable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Resumer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Telnyx/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Trippable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unbridge/@EntryIndexedValue">True</s:Boolean>

View file

@ -11,18 +11,110 @@
[![Build status](https://ci.appveyor.com/api/projects/status/github/elsa-workflows/elsa-core?svg=true&branch=master)](https://ci.appveyor.com/project/sfmskywalker/elsa)
[![Discord](https://img.shields.io/discord/814605913783795763?label=chat&logo=discord)](https://discord.gg/hhChk5H472)
[![Stack Overflow questions](https://img.shields.io/badge/stackoverflow-elsa_workflows-orange.svg)]( http://stackoverflow.com/questions/tagged/elsa-workflows )
[![Build elsa-dashboard:latest](https://github.com/elsa-workflows/elsa-core/actions/workflows/publish-latest-dashboard.yml/badge.svg)](https://github.com/elsa-workflows/elsa-core/actions/workflows/publish-latest-dashboard.yml)
[![Build elsa-dashboard:latest](https://github.com/elsa-workflows/elsa-core/actions/workflows/publish-latest-dashboard-and-server-docker.yml/badge.svg)](https://github.com/elsa-workflows/elsa-core/actions/workflows/publish-latest-dashboard-and-server-docker.yml)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/elsaworkflows/elsa-dashboard-and-server?label=docker&logo=docker)](https://hub.docker.com/repository/docker/elsaworkflows/elsa-dashboard-and-server)
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 visually and of course JSON.
Workflows can be defined using code and using the visual workflow designer.
<p align="center">
<img src="./doc/elsa-2-hello-world-http.gif" alt="Elsa 2 Preview">
</p>
## Documentation
Documentation can be found [here](https://elsa-workflows.github.io/elsa-core/docs/next/quickstarts/quickstarts-console-hello-world).
## Getting Started
Working on it.
```bash
dotnet new console -n "MyConsoleApp"
cd MyConsoleApp
dotnet add package Elsa --prerelease
dotnet add package Elsa.Activities.Console --prerelease
```
Create a new file called `HelloWorldWorkflow.cs` and add the following:
```csharp
using Elsa.Activities.Console;
using Elsa.Builders;
namespace MyConsoleApp
{
public class HelloWorld : IWorkflow
{
public void Build(IWorkflowBuilder builder) => builder.WriteLine("Hello World!");
}
}
```
Modify `Program.cs` as follows:
```csharp
using System.Threading.Tasks;
using Elsa.Services;
using Microsoft.Extensions.DependencyInjection;
namespace MyConsoleApp
{
class Program
{
private static async Task Main()
{
var services = new ServiceCollection()
.AddElsa(options => options
.AddConsoleActivities()
.AddWorkflow<HelloWorld>())
.BuildServiceProvider();
var workflowRunner = services.GetRequiredService<IBuildsAndStartsWorkflow>();
await workflowRunner.BuildAndStartWorkflowAsync<HelloWorld>();
}
}
}
```
Run the program:
```bash
dotnet run
```
Output:
```bash
Hello World!
```
Check out the [Quickstart guides](https://elsa-workflows.github.io/elsa-core/docs/next/quickstarts/quickstarts-console-hello-world) for more examples, including how to setup the Elsa Dashboard to create and manage visual workflows.
## Docker
A quick and easy way to give Elsa a spin is to run the following Docker command:
```bash
docker run -t -i -e ELSA__SERVER__BASEURL='http://localhost:13000' -p 13000:80 elsaworkflows/elsa-dashboard-and-server:latest
```
Then navigate to http://localhost:13000.
## Building From Source
When you clone the repo, the solution file to open is `Elsa.sln` which should build with no issues.
### Elsa Dashboard & Client Assets
If you want to run the sample project `ElsaDashboard.Samples.AspNetCore.Monolith.csproj`, you should build the client assets first.
The easiest way to do that is by running the `.\build-assets.ps1` file in the root of the repo (where this README.md is as well).
Alternatively, you might run `.\build-assets-and-run-dashboard-monolith.ps1` that will first build the client assets and then run the dashboard application to give Elsa a quick whirl.
### Docker Compose
Another quick way to try out Elsa is to run `build-and-run-dashboard-monolith-with-docker.ps1`, which will use Docker Compose to build an image and start a container.
When the container starts, you can reach the Elsa Dashboard at http://localhost:6868
## Roadmap

View file

@ -11,7 +11,7 @@ environment:
sonarcloud_api_key:
secure: qaE9kOVkGGFM7ZMf3YeliZxDU9udTCy6W5xXHJ9T+tXv5qXPMYlIdR7hyNgxW04A
version: 2.0.0-rc1.{build}
version: 2.1.0-preview.{build}
init:
- cmd: git config --global core.autocrlf true
@ -51,25 +51,25 @@ install:
cd ..\..\..\..
# dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-sonarscanner
before_build:
- cmd: >-
docker-compose -f docker-compose.yaml up -d mongodb postgres
# - ps: >-
# if(-Not $env:APPVEYOR_PULL_REQUEST_NUMBER) {
# & dotnet-sonarscanner begin
# /k:$env:sonarcloud_project_name
# /v:AppVeyor_build_$env:APPVEYOR_BUILD_VERSION
# /o:$env:sonarcloud_org_name
# /s:$env:APPVEYOR_BUILD_FOLDER\.SonarQube.Analysis.xml
# /d:sonar.host.url="https://sonarcloud.io"
# /d:sonar.login=$env:sonarcloud_api_key
# /d:sonar.cs.xunit.reportsPaths=$env:APPVEYOR_BUILD_FOLDER\**\TestResults\TestResults.xml
# /d:sonar.cs.opencover.reportsPaths=$env:APPVEYOR_BUILD_FOLDER\**\TestResults\*\coverage.opencover.xml
# /d:sonar.branch.name=$env:APPVEYOR_REPO_BRANCH
# }
- ps: >-
if(-Not $env:APPVEYOR_PULL_REQUEST_NUMBER) {
& dotnet-sonarscanner begin
/k:$env:sonarcloud_project_name
/v:AppVeyor_build_$env:APPVEYOR_BUILD_VERSION
/o:$env:sonarcloud_org_name
/s:$env:APPVEYOR_BUILD_FOLDER\.SonarQube.Analysis.xml
/d:sonar.host.url="https://sonarcloud.io"
/d:sonar.login=$env:sonarcloud_api_key
/d:sonar.cs.xunit.reportsPaths=$env:APPVEYOR_BUILD_FOLDER\**\TestResults\TestResults.xml
/d:sonar.cs.opencover.reportsPaths=$env:APPVEYOR_BUILD_FOLDER\**\TestResults\*\coverage.opencover.xml
/d:sonar.branch.name=$env:APPVEYOR_REPO_BRANCH
}
build_script:
- ps: >-

View file

@ -0,0 +1,2 @@
docker build -t elsa-dashboard:latest -t elsaworkflows/elsa-dashboard:latest -f ./docker/Dockerfile-elsa-dashboard .
docker push elsaworkflows/elsa-dashboard:latest

View file

@ -0,0 +1,2 @@
docker build -t elsa-dashboard-and-server:local -f ./docker/Dockerfile-elsa-dashboard-and-server .
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e ELSA__SERVER__BASEURL=http://localhost:14000 -p 14000:80 elsa-dashboard-and-server:local

View file

@ -0,0 +1,2 @@
docker build -t elsa-dashboard:local -f ./docker/Dockerfile-elsa-dashboard .
docker run -t -i -e Elsa__Server__BaseAddress='http://localhost:11000' -e ASPNETCORE_ENVIRONMENT='Development' -p 13000:80 elsa-dashboard:latest

View file

@ -0,0 +1,2 @@
docker build -t elsa-server:local -f ./docker/Dockerfile-elsa-server .
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -p 11000:80 elsa-server:local

View file

@ -0,0 +1,3 @@
.\build-assets
cd ../../../..
.\run-dashboard-monolith

11
build-assets.ps1 Normal file
View file

@ -0,0 +1,11 @@
cd ./src/designer/elsa-workflows-studio
npm install
npm run build
cd ../bindings/aspnet/Elsa.Designer.Components.Web
npm install
npm run build
cd ../../../../dashboards/blazor/ElsaDashboard.Application
npm install
npm run build

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View file

@ -1,37 +1,38 @@
version: '3.7'
services:
mongodb:
image: mongo
ports:
- "27017:27017"
postgres:
image: postgres:11
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=Password12!
- POSTGRES_DB=yessql
ports:
- "5432:5432"
azureblobstorage:
image: mcr.microsoft.com/azure-blob-storage
redis:
image: redis
ports:
- "6379:6379"
rabbitmq:
image: "rabbitmq:3-management"
ports:
- "15672:15672"
- "5672:5672"
smtp4dev:
image: rnwood/smtp4dev:linux-amd64-3.1.0-ci0856
ports:
- "3000:80"
- "2525:25"
mongodb:
image: mongo
ports:
- "27017:27017"
postgres:
image: postgres:13.3-alpine
restart: always
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=Password12!
- POSTGRES_DB=elsa
ports:
- "5432:5432"
azureblobstorage:
image: mcr.microsoft.com/azure-blob-storage
redis:
image: redis
ports:
- "6379:6379"
rabbitmq:
image: "rabbitmq:3-management"
ports:
- "15672:15672"
- "5672:5672"
smtp4dev:
image: rnwood/smtp4dev:linux-amd64-3.1.0-ci0856
ports:
- "3000:80"
- "2525:25"

View file

@ -12,13 +12,11 @@ COPY src/. ./src
COPY *.props ./
COPY ./Nuget.Config ./
# build and publish (UseAppHost=false created platform independent binaries)
WORKDIR /source/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith
RUN dotnet build "ElsaDashboard.Samples.AspNetCore.Monolith.csproj" -c Release -o /app/build
RUN dotnet publish "ElsaDashboard.Samples.AspNetCore.Monolith.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
# move binaries into smaller base image
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app

View file

@ -0,0 +1,40 @@
FROM node:15-alpine as client-build
WORKDIR /app
COPY /src .
WORKDIR /app/designer/elsa-workflows-studio
RUN npm install --force
RUN npm run build
WORKDIR /app/designer/bindings/aspnet/Elsa.Designer.Components.Web
RUN npm install --force
RUN npm run build
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim-amd64 AS build
WORKDIR /source
# restore packages
COPY ./src/dashboards/aspnetcore/ElsaDashboard/ElsaDashboard.Web.csproj ./src/dashboards/aspnetcore/ElsaDashboard/ElsaDashboard.Web.csproj
RUN dotnet restore "./src/dashboards/aspnetcore/ElsaDashboard/ElsaDashboard.Web.csproj"
# copy required source code
COPY src/. ./src
COPY *.props ./
COPY ./Nuget.Config ./
COPY --from=client-build /app/designer/elsa-workflows-studio ./src/designer/elsa-workflows-studio
COPY --from=client-build /app/designer/bindings/aspnet/Elsa.Designer.Components.Web ./src/designer/bindings/aspnet/Elsa.Designer.Components.Web
# build and publish (UseAppHost=false created platform independent binaries)
WORKDIR /source/src/dashboards/aspnetcore/ElsaDashboard
RUN dotnet build "ElsaDashboard.Web.csproj" -c Release -o /app/build
RUN dotnet publish "ElsaDashboard.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
# move binaries into smaller base image
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
COPY --from=build /app/publish ./
EXPOSE 80/tcp
EXPOSE 443/tcp
ENTRYPOINT ["dotnet", "ElsaDashboard.Web.dll"]

View file

@ -11,7 +11,6 @@ WORKDIR /app/designer/bindings/aspnet/Elsa.Designer.Components.Web
RUN npm install --force
RUN npm run build
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim-amd64 AS build
WORKDIR /source
@ -26,13 +25,11 @@ COPY ./Nuget.Config ./
COPY --from=client-build /app/designer/elsa-workflows-studio ./src/designer/elsa-workflows-studio
COPY --from=client-build /app/designer/bindings/aspnet/Elsa.Designer.Components.Web ./src/designer/bindings/aspnet/Elsa.Designer.Components.Web
# build and publish (UseAppHost=false created platform independent binaries)
# build and publish (UseAppHost=false creates platform independent binaries)
WORKDIR /source/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith
RUN dotnet build "ElsaDashboard.Samples.AspNetCore.Monolith.csproj" -c Release -o /app/build
RUN dotnet publish "ElsaDashboard.Samples.AspNetCore.Monolith.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
# move binaries into smaller base image
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app

View file

@ -0,0 +1,30 @@
FROM node:15-alpine as client-build
WORKDIR /app
COPY /src .
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim-amd64 AS build
WORKDIR /source
# restore packages
COPY ./src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj ./src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj
RUN dotnet restore "./src/samples/server/Elsa.Samples.Server.Host/Elsa.Samples.Server.Host.csproj"
# copy required source code
COPY src/. ./src
COPY *.props ./
COPY ./Nuget.Config ./
# build and publish (UseAppHost=false creates platform independent binaries)
WORKDIR /source/src/samples/server/Elsa.Samples.Server.Host
RUN dotnet build "Elsa.Samples.Server.Host.csproj" -c Release -o /app/build
RUN dotnet publish "Elsa.Samples.Server.Host.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
# move binaries into smaller base image
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
COPY --from=build /app/publish ./
EXPOSE 80/tcp
EXPOSE 443/tcp
ENTRYPOINT ["dotnet", "Elsa.Samples.Server.Host.dll"]

View file

@ -3,7 +3,7 @@
Clone the repository and open a command line in the project root folder. Then you can run the two docker commands below to build and start the container. Alternatively you can use the provided docker-compose file in the docker folder that does it for you via `docker-compuse up -d`. The docker-compose file will also include a fake smtp server to test email delivery.
```
$ docker build -t elsadashboard:local -f ./docker/Dockerfile-with-npm .
$ docker build -t elsadashboardwithserver:local -f ./docker/Dockerfile-with-npm .
$ docker run -t -i -e Elsa__Server__BaseAddress='http://localhost:6868' -e ASPNETCORE_ENVIRONMENT='Development' -p 6868:80 elsadashboard:latest
```

1
pack-local-feed.ps1 Normal file
View file

@ -0,0 +1 @@
dotnet pack -o C:\Projects\Elsa\feed --version-suffix "rc2-5" -p:PackageVersion=2.0.0-rc2-5

View file

@ -0,0 +1,2 @@
cd ./src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith
dotnet run

View file

@ -20,10 +20,10 @@ namespace Elsa.Activities.AzureServiceBus
_serializer = serializer;
}
[ActivityProperty(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string QueueName { get; set; } = default!;
[ActivityProperty]
[ActivityInput]
public Type MessageType { get; set; } = default!;
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) => context.WorkflowExecutionContext.IsFirstPass ? ExecuteInternal(context) : Suspend();

View file

@ -22,13 +22,13 @@ namespace Elsa.Activities.AzureServiceBus
_serializer = serializer;
}
[ActivityProperty(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string TopicName { get; set; } = default!;
[ActivityProperty(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string SubscriptionName { get; set; } = default!;
[ActivityProperty]
[ActivityInput]
public Type MessageType { get; set; } = default!;
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) => context.WorkflowExecutionContext.IsFirstPass ? ExecuteInternal(context) : Suspend();

View file

@ -21,10 +21,10 @@ namespace Elsa.Activities.AzureServiceBus
_serializer = serializer;
}
[ActivityProperty(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string QueueName { get; set; } = default!;
[ActivityProperty(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json })]
[ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json })]
public object Message { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -20,8 +20,8 @@ namespace Elsa.Activities.AzureServiceBus
_serializer = serializer;
}
[ActivityProperty] public string TopicName { get; set; } = default!;
[ActivityProperty] public object Message { get; set; } = default!;
[ActivityInput] public string TopicName { get; set; } = default!;
[ActivityInput] public object Message { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{

View file

@ -23,14 +23,14 @@ namespace Elsa.Activities.AzureServiceBus.Bookmarks
public class QueueMessageReceivedBookmarkProvider : BookmarkProvider<QueueMessageReceivedBookmark, AzureServiceBusQueueMessageReceived>
{
public override async ValueTask<IEnumerable<IBookmark>> GetBookmarksAsync(BookmarkProviderContext<AzureServiceBusQueueMessageReceived> context, CancellationToken cancellationToken) =>
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<AzureServiceBusQueueMessageReceived> context, CancellationToken cancellationToken) =>
new[]
{
new QueueMessageReceivedBookmark
Result(new QueueMessageReceivedBookmark
{
QueueName = (await context.Activity.GetPropertyValueAsync(x => x.QueueName, cancellationToken))!,
QueueName = (await context.ReadActivityPropertyAsync(x => x.QueueName, cancellationToken))!,
CorrelationId = context.ActivityExecutionContext.WorkflowExecutionContext.CorrelationId
}
})
};
}
}

View file

@ -25,15 +25,15 @@ namespace Elsa.Activities.AzureServiceBus.Bookmarks
public class TopicMessageReceivedBookmarkProvider : BookmarkProvider<TopicMessageReceivedBookmark, AzureServiceBusTopicMessageReceived>
{
public override async ValueTask<IEnumerable<IBookmark>> GetBookmarksAsync(BookmarkProviderContext<AzureServiceBusTopicMessageReceived> context, CancellationToken cancellationToken) =>
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<AzureServiceBusTopicMessageReceived> context, CancellationToken cancellationToken) =>
new[]
{
new TopicMessageReceivedBookmark
Result(new TopicMessageReceivedBookmark
{
TopicName = (await context.Activity.GetPropertyValueAsync(x => x.TopicName, cancellationToken))!,
SubscriptionName = (await context.Activity.GetPropertyValueAsync(x => x.SubscriptionName, cancellationToken))!,
TopicName = (await context.ReadActivityPropertyAsync(x => x.TopicName, cancellationToken))!,
SubscriptionName = (await context.ReadActivityPropertyAsync(x => x.SubscriptionName, cancellationToken))!,
CorrelationId = context.ActivityExecutionContext.WorkflowExecutionContext.CorrelationId
}
})
};
}
}

View file

@ -1,7 +1,6 @@
using System.Threading.Tasks;
using Elsa.Activities.AzureServiceBus.Services;
using Elsa.Events;
using Elsa.Triggers;
using Rebus.Handlers;
namespace Elsa.Activities.AzureServiceBus.Consumers

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props"/>
<Import Project="..\..\..\configureawait.props"/>
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
@ -13,10 +13,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="5.1.2"/>
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="5.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj"/>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -38,10 +38,10 @@ namespace Elsa.Activities.AzureServiceBus.Extensions
.AddBookmarkProvider<TopicMessageReceivedBookmarkProvider>()
;
options.AddConsumer<RestartServiceBusQueuesConsumer, WorkflowDefinitionPublished>();
options.AddConsumer<RestartServiceBusQueuesConsumer, WorkflowDefinitionRetracted>();
options.AddConsumer<RestartServiceBusTopicsConsumer, WorkflowDefinitionPublished>();
options.AddConsumer<RestartServiceBusTopicsConsumer, WorkflowDefinitionRetracted>();
options.AddCompetingConsumer<RestartServiceBusQueuesConsumer, WorkflowDefinitionPublished>();
options.AddCompetingConsumer<RestartServiceBusQueuesConsumer, WorkflowDefinitionRetracted>();
options.AddCompetingConsumer<RestartServiceBusTopicsConsumer, WorkflowDefinitionPublished>();
options.AddCompetingConsumer<RestartServiceBusTopicsConsumer, WorkflowDefinitionRetracted>();
options
.AddActivity<AzureServiceBusQueueMessageReceived>()

View file

@ -4,10 +4,7 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Persistence;
using Elsa.Persistence.Specifications.WorkflowInstances;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.Azure.ServiceBus.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -22,6 +19,7 @@ namespace Elsa.Activities.AzureServiceBus.Services
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
private readonly ICollection<QueueWorker> _workers;
private readonly SemaphoreSlim _semaphore = new(1);
public ServiceBusQueuesStarter(
IQueueMessageReceiverClientFactory messageReceiverClientFactory,
@ -38,11 +36,20 @@ namespace Elsa.Activities.AzureServiceBus.Services
public async Task CreateWorkersAsync(CancellationToken cancellationToken = default)
{
await DisposeExistingWorkersAsync();
var queueNames = (await GetQueueNamesAsync(cancellationToken).ToListAsync(cancellationToken)).Distinct();
await _semaphore.WaitAsync(cancellationToken);
foreach (var queueName in queueNames)
await CreateAndAddWorkerAsync(queueName, cancellationToken);
try
{
await DisposeExistingWorkersAsync();
var queueNames = (await GetQueueNamesAsync(cancellationToken).ToListAsync(cancellationToken)).Distinct();
foreach (var queueName in queueNames)
await CreateAndAddWorkerAsync(queueName, cancellationToken);
}
finally
{
_semaphore.Release();
}
}
private async Task CreateAndAddWorkerAsync(string queueName, CancellationToken cancellationToken)
@ -89,7 +96,7 @@ namespace Elsa.Activities.AzureServiceBus.Services
foreach (var activity in workflowBlueprintWrapper.Filter<AzureServiceBusQueueMessageReceived>())
{
var queueName = await activity.GetPropertyValueAsync(x => x.QueueName, cancellationToken);
var queueName = await activity.EvaluatePropertyValueAsync(x => x.QueueName, cancellationToken);
if (string.IsNullOrWhiteSpace(queueName))
{

View file

@ -95,8 +95,8 @@ namespace Elsa.Activities.AzureServiceBus.Services
foreach (var activity in workflowBlueprintWrapper.Filter<AzureServiceBusTopicMessageReceived>())
{
var topicName = await activity.GetPropertyValueAsync(x => x.TopicName, cancellationToken);
var subscriptionName = await activity.GetPropertyValueAsync(x => x.SubscriptionName, cancellationToken);
var topicName = await activity.EvaluatePropertyValueAsync(x => x.TopicName, cancellationToken);
var subscriptionName = await activity.EvaluatePropertyValueAsync(x => x.SubscriptionName, cancellationToken);
yield return (topicName, subscriptionName)!;
}
}

View file

@ -97,7 +97,7 @@ namespace Elsa.Activities.AzureServiceBus.Services
private async Task OnMessageReceived(Message message, CancellationToken cancellationToken)
{
_logger.LogDebug("Message received with ID {MessageId}", message.MessageId);
await TriggerWorkflowsAsync(message, cancellationToken);
await TriggerWorkflowsAsync(message, CancellationToken.None);
await ReceiverClient.CompleteAsync(message.SystemProperties.LockToken);
}
}

View file

@ -20,7 +20,7 @@ namespace Elsa.Activities.BlobStorage
private readonly IBlobStorage _storage;
public BlobExists(IBlobStorage storage) => _storage = storage;
[ActivityProperty(Hint = "The ID of the blob.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The ID of the blob.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string BlobId { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -22,9 +22,9 @@ namespace Elsa.Activities.BlobStorage
private readonly IBlobStorage _storage;
public DeleteBlob(IBlobStorage storage) => _storage = storage;
[ActivityProperty(
[ActivityInput(
Hint = "The IDs of the blobs.",
UIHint = ActivityPropertyUIHints.MultiText,
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]

View file

@ -19,11 +19,11 @@ namespace Elsa.Activities.BlobStorage
private readonly IBlobStorage _storage;
public ReadBlob(IBlobStorage storage) => _storage = storage;
[ActivityProperty(Hint = "The Id assigned to the blob.")]
[ActivityInput(Hint = "The Id assigned to the blob.")]
[Required]
public string BlobId { get; set; } = default!;
[ActivityProperty(Hint = "If set, the output of this activity is written to the specified file. Otherwise, the bytes of the blob will be set as the activity output.")]
[ActivityInput(Hint = "If set, the output of this activity is written to the specified file. Otherwise, the bytes of the blob will be set as the activity output.")]
public string? DestinationFilePath { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -26,11 +26,11 @@ namespace Elsa.Activities.BlobStorage
private readonly IBlobStorage _storage;
[ActivityProperty(Hint = "The ID to be assigned to the blob.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The ID to be assigned to the blob.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[Required]
public string BlobId { get; set; } = default!;
[ActivityProperty(Hint = "The bytes to write.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript }, DefaultSyntax = SyntaxNames.JavaScript)]
[ActivityInput(Hint = "The bytes to write.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript }, DefaultSyntax = SyntaxNames.JavaScript)]
public byte[]? Bytes { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Activities.Conductor.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Conductor
{
[Trigger(
Category = "Conductor",
Description = "Waits for an event sent from your application."
)]
public class EventReceived : Activity
{
[ActivityInput(
Label = "Event",
Hint = "The event to wait for.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string EventName { get; set; } = default!;
[ActivityInput(
Hint = "Enter one or more possible outcomes for this event.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json }
)]
public ISet<string> Outcomes { get; set; } = new HashSet<string>();
[ActivityOutput(Hint = "Any input that was sent along with the event from your application.")]
public object? Payload { get; set; }
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) => context.IsFirstPass ? OnExecuteInternal(context) : Suspend();
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) => OnExecuteInternal(context);
private IActivityExecutionResult OnExecuteInternal(ActivityExecutionContext context)
{
var eventModel = context.GetInput<EventModel>()!;
var outcomes = eventModel.Outcomes;
if (outcomes?.Any() == false)
outcomes = new[] { OutcomeNames.Done };
Payload = eventModel.Payload;
return base.Outcomes(outcomes!);
}
}
}

View file

@ -0,0 +1,57 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Conductor
{
[Job(
Category = "Conductor",
Description = "Sends a task to your application and waits for the application to report the task as completed or cancelled."
)]
public class RunTask : Activity
{
private readonly IEventPublisher _eventPublisher;
public RunTask(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
[ActivityInput(
Label = "Run Task",
Hint = "The task to run.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string TaskName { get; set; } = default!;
[ActivityOutput(Hint = "Any input to send along with the task to your application.")]
public object? Payload { get; set; }
[ActivityOutput(Hint = "Any input that was received along with the task completion.")]
public object? ResultPayload { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await _eventPublisher.PublishAsync(new RunTaskModel(TaskName, Payload, context.WorkflowInstance.Id));
return Suspend();
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
var eventModel = context.GetInput<TaskResultModel>()!;
var outcomes = eventModel.Outcomes;
if (outcomes?.Any() == false)
outcomes = new[] { OutcomeNames.Done };
ResultPayload = eventModel.Payload;
return base.Outcomes(outcomes!);
}
}
}

View file

@ -0,0 +1,46 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Conductor
{
[Action(
Category = "Conductor",
Description = "Sends a command to your application.",
Outcomes = new[] { OutcomeNames.Done }
)]
public class SendCommand : Activity
{
private readonly IEventPublisher _eventPublisher;
public SendCommand(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
[ActivityInput(
Label = "Command",
Hint = "The command to send.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string CommandName { get; set; } = default!;
[ActivityInput(
UIHint = ActivityInputUIHints.MultiLine,
Hint = "Optional data to send to your application.",
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json })]
public object? Payload { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await _eventPublisher.PublishAsync(new SendCommandModel(CommandName, Payload, context.WorkflowInstance.Id));
return Done();
}
}
}

View file

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Rebus.Handlers;
namespace Elsa.Activities.Conductor.Consumers
{
public class RunTaskConsumer : IHandleMessages<RunTaskModel>
{
private readonly ApplicationTasksClient _applicationTasksClient;
public RunTaskConsumer(ApplicationTasksClient applicationTasksClient) => _applicationTasksClient = applicationTasksClient;
public async Task Handle(RunTaskModel message) => await _applicationTasksClient.RunTaskAsync(message);
}
}

View file

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Rebus.Handlers;
namespace Elsa.Activities.Conductor.Consumers
{
public class SendCommandConsumer : IHandleMessages<SendCommandModel>
{
private readonly ApplicationCommandsClient _applicationCommandsClient;
public SendCommandConsumer(ApplicationCommandsClient applicationCommandsClient) => _applicationCommandsClient = applicationCommandsClient;
public async Task Handle(SendCommandModel message) => await _applicationCommandsClient.SendCommandAsync(message);
}
}

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Description>
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
This package provides activities to implement conductor-style workflows where Elsa sends commands to your application, which responds asynchronously with events.
</Description>
<PackageTags>elsa, workflows, conductor</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="5.0.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Providers.Bookmarks;
using Elsa.Services;
using Microsoft.AspNetCore.Mvc;
namespace Elsa.Activities.Conductor.Endpoints.Conductor.Events
{
[ApiController]
[Route("conductor/events/{eventName}/dispatch")]
[Produces("application/json")]
public class Dispatch : ControllerBase
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
public Dispatch(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string eventName, EventModel model)
{
var bookmark = new EventBookmark(eventName.ToLowerInvariant());
var trigger = new EventBookmark(eventName.ToLowerInvariant());
var context = new CollectWorkflowsContext(nameof(EventReceived), bookmark, trigger, model.CorrelationId, model.WorkflowInstanceId);
var pendingWorkflows = await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, model);
return Accepted(pendingWorkflows);
}
}
}

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Providers.Bookmarks;
using Elsa.Services;
using Microsoft.AspNetCore.Mvc;
namespace Elsa.Activities.Conductor.Endpoints.Conductor.Events
{
[ApiController]
[Route("conductor/events/{eventName}/execute")]
[Produces("application/json")]
public class Execute : ControllerBase
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
public Execute(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string eventName, EventModel model)
{
var bookmark = new EventBookmark(eventName);
var trigger = new EventBookmark(eventName);
var context = new CollectWorkflowsContext(nameof(EventReceived), bookmark, trigger, model.CorrelationId, model.WorkflowInstanceId);
var pendingWorkflows = await _workflowLaunchpad.CollectAndExecuteWorkflowsAsync(context, model);
return Accepted(pendingWorkflows);
}
}
}

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Providers.Bookmarks;
using Elsa.Services;
using Microsoft.AspNetCore.Mvc;
namespace Elsa.Activities.Conductor.Endpoints.Conductor.Tasks
{
[ApiController]
[Route("conductor/tasks/{taskName}/dispatch")]
[Produces("application/json")]
public class Dispatch : ControllerBase
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
public Dispatch(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string taskName, TaskResultModel model)
{
var bookmark = new TaskBookmark(taskName.ToLowerInvariant());
var trigger = new TaskBookmark(taskName.ToLowerInvariant());
var context = new CollectWorkflowsContext(nameof(RunTask), bookmark, trigger, model.CorrelationId, model.WorkflowInstanceId);
var pendingWorkflows = await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, model);
return Accepted(pendingWorkflows);
}
}
}

View file

@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Providers.Bookmarks;
using Elsa.Services;
using Microsoft.AspNetCore.Mvc;
namespace Elsa.Activities.Conductor.Endpoints.Conductor.Tasks
{
[ApiController]
[Route("conductor/tasks/{taskName}/execute")]
[Produces("application/json")]
public class Execute : ControllerBase
{
private readonly IWorkflowLaunchpad _workflowLaunchpad;
public Execute(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad;
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string taskName, TaskResultModel model)
{
var bookmark = new TaskBookmark(taskName.ToLowerInvariant());
var trigger = new TaskBookmark(taskName.ToLowerInvariant());
var context = new CollectWorkflowsContext(nameof(RunTask), bookmark, trigger, model.CorrelationId, model.WorkflowInstanceId);
var pendingWorkflows = await _workflowLaunchpad.CollectAndExecuteWorkflowsAsync(context, model);
return Accepted(pendingWorkflows);
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using Elsa.Activities.Conductor.Consumers;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Elsa.Activities.Conductor.Providers.ActivityTypes;
using Elsa.Activities.Conductor.Providers.Bookmarks;
using Elsa.Activities.Conductor.Providers.Commands;
using Elsa.Activities.Conductor.Providers.Events;
using Elsa.Activities.Conductor.Providers.Tasks;
using Elsa.Activities.Conductor.Services;
using Elsa.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly;
namespace Elsa.Activities.Conductor.Extensions
{
public static class ElsaOptionsBuilderExtensions
{
public static ElsaOptionsBuilder AddConductorActivities(this ElsaOptionsBuilder elsa, Action<ConductorOptions>? configureOptions = default, Action<IHttpClientBuilder>? configureHttpClient = default)
{
var services = elsa.Services;
if (configureOptions != null)
services.Configure(configureOptions);
services.ConfigureCommandsHttpClient(configureHttpClient);
services.ConfigureTasksHttpClient(configureHttpClient);
services
.AddCommandsProvider<OptionsCommandsProvider>()
.AddEventsProvider<OptionsEventsProvider>()
.AddTasksProvider<OptionsTasksProvider>()
.AddActivityTypeProvider<CommandActivityTypeProvider>()
.AddActivityTypeProvider<EventActivityTypeProvider>()
.AddActivityTypeProvider<TaskActivityTypeProvider>()
.AddBookmarkProvider<EventBookmarkProvider>()
.AddBookmarkProvider<TaskBookmarkProvider>()
.AddSingleton<Scoped<IEnumerable<ICommandsProvider>>>()
.AddSingleton<Scoped<IEnumerable<IEventsProvider>>>()
.AddSingleton<Scoped<IEnumerable<ITasksProvider>>>();
elsa
.AddActivitiesFrom<SendCommand>()
.AddCompetingConsumer<SendCommandConsumer, SendCommandModel>()
.AddCompetingConsumer<RunTaskConsumer, RunTaskModel>();
return elsa;
}
public static IServiceCollection AddCommandsProvider<T>(this IServiceCollection services) where T : class, ICommandsProvider => services.AddScoped<ICommandsProvider, T>();
public static IServiceCollection AddEventsProvider<T>(this IServiceCollection services) where T : class, IEventsProvider => services.AddScoped<IEventsProvider, T>();
public static IServiceCollection AddTasksProvider<T>(this IServiceCollection services) where T : class, ITasksProvider => services.AddScoped<ITasksProvider, T>();
private static void ConfigureCommandsHttpClient(this IServiceCollection services, Action<IHttpClientBuilder>? configureHttpClient = default) => services.ConfigureHttpClient<ApplicationCommandsClient>(o => o.CommandsHookUrl, configureHttpClient);
private static void ConfigureTasksHttpClient(this IServiceCollection services, Action<IHttpClientBuilder>? configureHttpClient = default) => services.ConfigureHttpClient<ApplicationTasksClient>(o => o.CommandsHookUrl, configureHttpClient);
private static void ConfigureHttpClient<T>(this IServiceCollection services, Func<ConductorOptions, Uri> baseAddress, Action<IHttpClientBuilder>? configureHttpClient = default) where T : class
{
var httpClientBuilder = services.AddHttpClient<T>((sp, httpClient) =>
{
var options = sp.GetRequiredService<IOptions<ConductorOptions>>().Value;
httpClient.BaseAddress = baseAddress(options);
});
if (configureHttpClient == null)
httpClientBuilder.AddTransientHttpErrorPolicy(x => x.WaitAndRetryAsync(10, retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount))));
else
configureHttpClient(httpClientBuilder);
}
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}

View file

@ -0,0 +1,11 @@
using Elsa.Models;
namespace Elsa.Activities.Conductor.Models
{
public class CommandDefinition : Entity
{
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
public string? Description { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Elsa.Models;
namespace Elsa.Activities.Conductor.Models
{
public class EventDefinition : Entity
{
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
public string? Description { get; set; }
public ICollection<string>? Outcomes { get; set; }
}
}

View file

@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace Elsa.Activities.Conductor.Models
{
public record EventModel(string EventName, ICollection<string>? Outcomes, object? Payload, string? WorkflowInstanceId, string? CorrelationId);
}

View file

@ -0,0 +1,4 @@
namespace Elsa.Activities.Conductor.Models
{
public record RunTaskModel(string Task, object? Payload, string WorkflowInstanceId);
}

View file

@ -0,0 +1,4 @@
namespace Elsa.Activities.Conductor.Models
{
public record SendCommandModel(string Command, object? Payload, string WorkflowInstanceId);
}

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Elsa.Models;
namespace Elsa.Activities.Conductor.Models
{
public class TaskDefinition : Entity
{
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
public string? Description { get; set; }
public ICollection<string>? Outcomes { get; set; }
}
}

View file

@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace Elsa.Activities.Conductor.Models
{
public record TaskResultModel(string TaskName, ICollection<string>? Outcomes, object? Payload, string? WorkflowInstanceId, string? CorrelationId);
}

View file

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Elsa.Activities.Conductor.Models;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Serialization.JsonNet;
namespace Elsa.Activities.Conductor.Options
{
public class ConductorOptions
{
public ConductorOptions()
{
SerializerSettings = new JsonSerializerSettings();
SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}
/// <summary>
/// The URL to post commands to.
/// </summary>
public Uri CommandsHookUrl { get; set; } = default!;
/// <summary>
/// The URL to post tasks to.
/// </summary>
public Uri TasksHookUrl { get; set; } = default!;
/// <summary>
/// The serializer to use when serializing a command before sending to the application.
/// </summary>
public JsonSerializerSettings SerializerSettings { get; set; }
/// <summary>
/// A collection of commands that can be sent to the application.
/// </summary>
public ICollection<CommandDefinition> Commands { get; set; } = new List<CommandDefinition>();
/// <summary>
/// A collection of events that can be received from the application.
/// </summary>
public ICollection<EventDefinition> Events { get; set; } = new List<EventDefinition>();
/// <summary>
/// A collection of tasks that can be sent to the application.
/// </summary>
public ICollection<TaskDefinition> Tasks { get; set; } = new List<TaskDefinition>();
}
}

View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Elsa.Metadata;
using Elsa.Services;
using Elsa.Services.Models;
namespace Elsa.Activities.Conductor.Providers.ActivityTypes
{
public class CommandActivityTypeProvider : IActivityTypeProvider
{
private readonly IDescribesActivityType _describesActivityType;
private readonly IActivityActivator _activityActivator;
private readonly Scoped<IEnumerable<ICommandsProvider>> _scopedCommandsProviders;
public CommandActivityTypeProvider(IDescribesActivityType describesActivityType, IActivityActivator activityActivator, Scoped<IEnumerable<ICommandsProvider>> scopedCommandsProviders)
{
_describesActivityType = describesActivityType;
_activityActivator = activityActivator;
_scopedCommandsProviders = scopedCommandsProviders;
}
public async ValueTask<IEnumerable<ActivityType>> GetActivityTypesAsync(CancellationToken cancellationToken = default)
{
var commands = await GetCommandsAsync(cancellationToken);
var activityTypes = await GetActivityTypesAsync(commands, cancellationToken).ToListAsync(cancellationToken);
return activityTypes;
}
private async IAsyncEnumerable<ActivityType> GetActivityTypesAsync(IEnumerable<CommandDefinition> commands, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var command in commands)
yield return await CreateActivityTypeAsync(command, cancellationToken);
}
private async Task<ActivityType> CreateActivityTypeAsync(CommandDefinition command, CancellationToken cancellationToken)
{
var descriptor = await _describesActivityType.DescribeAsync<SendCommand>(cancellationToken);
descriptor.Type = command.Name;
descriptor.DisplayName = command.DisplayName ?? command.Name;
descriptor.Description = command.Description;
descriptor.InputProperties = descriptor.InputProperties.Where(x => x.Name != nameof(SendCommand.CommandName)).ToArray();
return new ActivityType
{
Type = typeof(SendCommand),
TypeName = descriptor.Type,
DisplayName = descriptor.DisplayName,
Describe = () => descriptor,
Description = descriptor.Description,
ActivateAsync = async context =>
{
var activity = await _activityActivator.ActivateActivityAsync<SendCommand>(context, cancellationToken);
activity.CommandName = command.Name;
return activity;
},
CanExecuteAsync = async (context, instance) => await instance.CanExecuteAsync(context),
ExecuteAsync = async (context, instance) => await instance.ExecuteAsync(context),
ResumeAsync = async (context, instance) => await instance.ResumeAsync(context)
};
}
private async Task<IEnumerable<CommandDefinition>> GetCommandsAsync(CancellationToken cancellationToken) =>
await _scopedCommandsProviders.UseServiceAsync(async commandProviders => await GetCommandsAsync(commandProviders, cancellationToken).ToListAsync(cancellationToken));
private static async IAsyncEnumerable<CommandDefinition> GetCommandsAsync(IEnumerable<ICommandsProvider> commandProviders, [EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var commandProvider in commandProviders)
{
var commands = await commandProvider.GetCommandsAsync(cancellationToken);
foreach (var command in commands)
yield return command;
}
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Elsa.Metadata;
using Elsa.Services;
using Elsa.Services.Models;
namespace Elsa.Activities.Conductor.Providers.ActivityTypes
{
public class EventActivityTypeProvider : IActivityTypeProvider
{
private readonly IDescribesActivityType _describesActivityType;
private readonly IActivityActivator _activityActivator;
private readonly Scoped<IEnumerable<IEventsProvider>> _scopedEventsProviders;
public EventActivityTypeProvider(IDescribesActivityType describesActivityType, IActivityActivator activityActivator, Scoped<IEnumerable<IEventsProvider>> scopedEventsProviders)
{
_describesActivityType = describesActivityType;
_activityActivator = activityActivator;
_scopedEventsProviders = scopedEventsProviders;
}
public async ValueTask<IEnumerable<ActivityType>> GetActivityTypesAsync(CancellationToken cancellationToken = default)
{
var events = await GetEventsAsync(cancellationToken);
return await GetActivityTypesAsync(events, cancellationToken).ToListAsync(cancellationToken);
}
private async IAsyncEnumerable<ActivityType> GetActivityTypesAsync(IEnumerable<EventDefinition> events, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var eventDefinition in events)
yield return await CreateActivityTypeAsync(eventDefinition, cancellationToken);
}
private async Task<ActivityType> CreateActivityTypeAsync(EventDefinition eventDefinition, CancellationToken cancellationToken)
{
var descriptor = await _describesActivityType.DescribeAsync<EventReceived>(cancellationToken);
descriptor.Type = eventDefinition.Name;
descriptor.DisplayName = eventDefinition.DisplayName ?? eventDefinition.Name;
descriptor.Description = eventDefinition.Description;
descriptor.InputProperties = Array.Empty<ActivityInputDescriptor>();
descriptor.Outcomes = eventDefinition.Outcomes?.ToArray() ?? new[] { OutcomeNames.Done };
return new ActivityType
{
Type = typeof(EventReceived),
TypeName = descriptor.Type,
DisplayName = descriptor.DisplayName,
Describe = () => descriptor,
Description = descriptor.Description,
ActivateAsync = async context =>
{
var activity = await _activityActivator.ActivateActivityAsync<EventReceived>(context, cancellationToken);
activity.EventName = eventDefinition.Name;
return activity;
},
CanExecuteAsync = async (context, instance) => await instance.CanExecuteAsync(context),
ExecuteAsync = async (context, instance) => await instance.ExecuteAsync(context),
ResumeAsync = async (context, instance) => await instance.ResumeAsync(context)
};
}
private async Task<IEnumerable<EventDefinition>> GetEventsAsync(CancellationToken cancellationToken) =>
await _scopedEventsProviders.UseServiceAsync(async eventProviders => await GetEventsAsync(eventProviders, cancellationToken).ToListAsync(cancellationToken));
private static async IAsyncEnumerable<EventDefinition> GetEventsAsync(IEnumerable<IEventsProvider> eventProviders, [EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var commandProvider in eventProviders)
{
var commands = await commandProvider.GetEventsAsync(cancellationToken);
foreach (var command in commands)
yield return command;
}
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Services;
using Elsa.Metadata;
using Elsa.Services;
using Elsa.Services.Models;
namespace Elsa.Activities.Conductor.Providers.ActivityTypes
{
public class TaskActivityTypeProvider : IActivityTypeProvider
{
private readonly IDescribesActivityType _describesActivityType;
private readonly IActivityActivator _activityActivator;
private readonly Scoped<IEnumerable<ITasksProvider>> _scopedTasksProviders;
public TaskActivityTypeProvider(IDescribesActivityType describesActivityType, IActivityActivator activityActivator, Scoped<IEnumerable<ITasksProvider>> scopedTasksProviders)
{
_describesActivityType = describesActivityType;
_activityActivator = activityActivator;
_scopedTasksProviders = scopedTasksProviders;
}
public async ValueTask<IEnumerable<ActivityType>> GetActivityTypesAsync(CancellationToken cancellationToken = default)
{
var events = await GetTasksAsync(cancellationToken);
return await GetActivityTypesAsync(events, cancellationToken).ToListAsync(cancellationToken);
}
private async IAsyncEnumerable<ActivityType> GetActivityTypesAsync(IEnumerable<TaskDefinition> events, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (var taskDefinition in events)
yield return await CreateActivityTypeAsync(taskDefinition, cancellationToken);
}
private async Task<ActivityType> CreateActivityTypeAsync(TaskDefinition taskDefinition, CancellationToken cancellationToken)
{
var descriptor = await _describesActivityType.DescribeAsync<RunTask>(cancellationToken);
descriptor.Type = taskDefinition.Name;
descriptor.DisplayName = taskDefinition.DisplayName ?? taskDefinition.Name;
descriptor.Description = taskDefinition.Description;
descriptor.InputProperties = Array.Empty<ActivityInputDescriptor>();
descriptor.Outcomes = taskDefinition.Outcomes?.ToArray() ?? new[] { OutcomeNames.Done };
return new ActivityType
{
Type = typeof(RunTask),
TypeName = descriptor.Type,
DisplayName = descriptor.DisplayName,
Describe = () => descriptor,
Description = descriptor.Description,
ActivateAsync = async context =>
{
var activity = await _activityActivator.ActivateActivityAsync<RunTask>(context, cancellationToken);
activity.TaskName = taskDefinition.Name;
return activity;
},
CanExecuteAsync = async (context, instance) => await instance.CanExecuteAsync(context),
ExecuteAsync = async (context, instance) => await instance.ExecuteAsync(context),
ResumeAsync = async (context, instance) => await instance.ResumeAsync(context)
};
}
private async Task<IEnumerable<TaskDefinition>> GetTasksAsync(CancellationToken cancellationToken) =>
await _scopedTasksProviders.UseServiceAsync(async taskProviders => await GetTasksAsync(taskProviders, cancellationToken).ToListAsync(cancellationToken));
private static async IAsyncEnumerable<TaskDefinition> GetTasksAsync(IEnumerable<ITasksProvider> taskProviders, [EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var commandProvider in taskProviders)
{
var commands = await commandProvider.GetTasksAsync(cancellationToken);
foreach (var command in commands)
yield return command;
}
}
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Bookmarks;
namespace Elsa.Activities.Conductor.Providers.Bookmarks
{
public record EventBookmark(string EventName) : IBookmark
{
}
public class EventBookmarkProvider : BookmarkProvider<EventBookmark, EventReceived>
{
public override bool SupportsActivity(BookmarkProviderContext<EventReceived> context) => context.ActivityType.Type == typeof(EventReceived);
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<EventReceived> context, CancellationToken cancellationToken) => await GetBookmarksInternalAsync(context, cancellationToken).ToListAsync(cancellationToken);
private async IAsyncEnumerable<BookmarkResult> GetBookmarksInternalAsync(BookmarkProviderContext<EventReceived> context, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var eventName = ToLower(await context.ReadActivityPropertyAsync(x => x.EventName, cancellationToken))!;
yield return Result(new EventBookmark(eventName), nameof(EventReceived));
}
private static string? ToLower(string? s) => s?.ToLowerInvariant();
}
}

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Bookmarks;
namespace Elsa.Activities.Conductor.Providers.Bookmarks
{
public record TaskBookmark(string TaskName) : IBookmark
{
}
public class TaskBookmarkProvider : BookmarkProvider<TaskBookmark, RunTask>
{
public override bool SupportsActivity(BookmarkProviderContext<RunTask> context) => context.ActivityType.Type == typeof(RunTask);
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<RunTask> context, CancellationToken cancellationToken) => await GetBookmarksInternalAsync(context, cancellationToken).ToListAsync(cancellationToken);
private async IAsyncEnumerable<BookmarkResult> GetBookmarksInternalAsync(BookmarkProviderContext<RunTask> context, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var taskName = ToLower(await context.ReadActivityPropertyAsync(x => x.TaskName, cancellationToken))!;
yield return Result(new TaskBookmark(taskName), nameof(RunTask));
}
private static string? ToLower(string? s) => s?.ToLowerInvariant();
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Elsa.Activities.Conductor.Services;
using Microsoft.Extensions.Options;
namespace Elsa.Activities.Conductor.Providers.Commands
{
public class OptionsCommandsProvider : ICommandsProvider
{
private readonly ConductorOptions _options;
public OptionsCommandsProvider(IOptions<ConductorOptions> options) => _options = options.Value;
public ValueTask<IEnumerable<CommandDefinition>> GetCommandsAsync(CancellationToken cancellationToken) => new(_options.Commands);
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Elsa.Activities.Conductor.Services;
using Microsoft.Extensions.Options;
namespace Elsa.Activities.Conductor.Providers.Events
{
public class OptionsEventsProvider : IEventsProvider
{
private readonly ConductorOptions _options;
public OptionsEventsProvider(IOptions<ConductorOptions> options) => _options = options.Value;
public ValueTask<IEnumerable<EventDefinition>> GetEventsAsync(CancellationToken cancellationToken) => new(_options.Events);
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Elsa.Activities.Conductor.Services;
using Microsoft.Extensions.Options;
namespace Elsa.Activities.Conductor.Providers.Tasks
{
public class OptionsTasksProvider : ITasksProvider
{
private readonly ConductorOptions _options;
public OptionsTasksProvider(IOptions<ConductorOptions> options) => _options = options.Value;
public ValueTask<IEnumerable<TaskDefinition>> GetTasksAsync(CancellationToken cancellationToken) => new(_options.Tasks);
}
}

View file

@ -0,0 +1,33 @@
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Elsa.Activities.Conductor.Services
{
public class ApplicationCommandsClient
{
private readonly HttpClient _httpClient;
private readonly ConductorOptions _options;
public ApplicationCommandsClient(HttpClient httpClient, IOptions<ConductorOptions> options)
{
_httpClient = httpClient;
_options = options.Value;
}
public async Task SendCommandAsync(SendCommandModel command, CancellationToken cancellationToken = default)
{
var json = JsonConvert.SerializeObject(command, _options.SerializerSettings);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("", content, cancellationToken);
// TODO: Handle response. If it contains instructions to resume the workflow, we can do so immediately.
}
}
}

View file

@ -0,0 +1,33 @@
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
using Elsa.Activities.Conductor.Options;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Elsa.Activities.Conductor.Services
{
public class ApplicationTasksClient
{
private readonly HttpClient _httpClient;
private readonly ConductorOptions _options;
public ApplicationTasksClient(HttpClient httpClient, IOptions<ConductorOptions> options)
{
_httpClient = httpClient;
_options = options.Value;
}
public async Task RunTaskAsync(RunTaskModel task, CancellationToken cancellationToken = default)
{
var json = JsonConvert.SerializeObject(task, _options.SerializerSettings);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("", content, cancellationToken);
// TODO: Handle response. If it contains instructions to resume the workflow, we can do so immediately.
}
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
namespace Elsa.Activities.Conductor.Services
{
public interface ICommandsProvider
{
ValueTask<IEnumerable<CommandDefinition>> GetCommandsAsync(CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
namespace Elsa.Activities.Conductor.Services
{
public interface IEventsProvider
{
ValueTask<IEnumerable<EventDefinition>> GetEventsAsync(CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Conductor.Models;
namespace Elsa.Activities.Conductor.Services
{
public interface ITasksProvider
{
ValueTask<IEnumerable<TaskDefinition>> GetTasksAsync(CancellationToken cancellationToken);
}
}

View file

@ -28,7 +28,7 @@ namespace Elsa.Activities.Console
_output = output;
}
[ActivityProperty(Hint = "The text to write.", SupportedSyntaxes = new[]{ SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The text to write.", SupportedSyntaxes = new[]{ SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Text { get; set; }
private readonly TextWriter _output;

View file

@ -23,10 +23,10 @@ namespace Elsa.Activities.Dropbox.Activities
_filesApi = filesApi;
}
[ActivityProperty(Hint = "The file to store.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The file to store.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public byte[] FileData { get; set; } = default!;
[ActivityProperty(Hint = "The path to which the file should be saved.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The path to which the file should be saved.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string Path { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -28,22 +28,32 @@ namespace Elsa.Activities.Email
_options = options.Value;
}
[ActivityProperty(Hint = "The sender's email address.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The sender's email address.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? From { get; set; }
[ActivityProperty(Hint = "The recipients email addresses.", UIHint = ActivityPropertyUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript })]
[ActivityInput(Hint = "The recipients email addresses.", UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript })]
public ICollection<string> To { get; set; } = new List<string>();
[ActivityProperty(Hint = "The cc recipients email addresses. (Optional)", UIHint = ActivityPropertyUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript })]
[ActivityInput(
Hint = "The cc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript },
Category = "More")]
public ICollection<string> Cc { get; set; } = new List<string>();
[ActivityProperty(Hint = "The Bcc recipients email addresses. (Optional)", UIHint = ActivityPropertyUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript })]
[ActivityInput(
Hint = "The Bcc recipients email addresses.",
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript },
Category = "More")]
public ICollection<string> Bcc { get; set; } = new List<string>();
[ActivityProperty(Hint = "The subject of the email message.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The subject of the email message.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Subject { get; set; }
[ActivityProperty(Hint = "The body of the email message.", UIHint = ActivityPropertyUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The body of the email message.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Body { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)

View file

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="2.11.1" />
<PackageReference Include="MailKit" Version="2.12.0" />
</ItemGroup>
</Project>

View file

@ -16,11 +16,11 @@ namespace Elsa.Activities.Entity
)]
public class EntityChanged : Activity
{
[ActivityProperty(UIHint = ActivityPropertyUIHints.SingleLine, Hint = "The Entity Name to observe. Matches any entity if no value is specified.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(UIHint = ActivityInputUIHints.SingleLine, Hint = "The Entity Name to observe. Matches any entity if no value is specified.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? EntityName { get; set; }
[ActivityProperty(
UIHint = ActivityPropertyUIHints.Dropdown,
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The Entity Changed Action to observe. Matches any action if no value is specified.",
Options = new[] { "Added", "Updated", "Deleted" },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }

View file

@ -23,15 +23,15 @@ namespace Elsa.Activities.Entity.Bookmarks
public class EntityChangedWorkflowTriggerProvider : BookmarkProvider<EntityChangedBookmark, EntityChanged>
{
public override async ValueTask<IEnumerable<IBookmark>> GetBookmarksAsync(BookmarkProviderContext<EntityChanged> context, CancellationToken cancellationToken) =>
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<EntityChanged> context, CancellationToken cancellationToken) =>
new[]
{
new EntityChangedBookmark(
entityName: await context.Activity.GetPropertyValueAsync(x => x.EntityName, cancellationToken),
action: await context.Activity.GetPropertyValueAsync(x => x.Action, cancellationToken),
Result(new EntityChangedBookmark(
entityName: await context.ReadActivityPropertyAsync(x => x.EntityName, cancellationToken),
action: await context.ReadActivityPropertyAsync(x => x.Action, cancellationToken),
contextId: context.ActivityExecutionContext.WorkflowExecutionContext.WorkflowInstance.ContextId,
correlationId: context.ActivityExecutionContext.WorkflowExecutionContext.WorkflowInstance.CorrelationId
)
))
};
}
}

View file

@ -9,7 +9,7 @@ namespace Elsa.Activities.Entity.Extensions
public static class WorkflowRunnerExtensions
{
// TODO: Design multi-tenancy
private const string TenantId = default;
private const string? TenantId = default;
public static async Task TriggerEntityChangedWorkflowsAsync(
this IWorkflowDispatcher workflowDispatcher,
@ -37,7 +37,7 @@ namespace Elsa.Activities.Entity.Extensions
correlationId
);
await workflowDispatcher.DispatchAsync(new TriggerWorkflowsRequest(activityType, bookmark, trigger, input, correlationId, contextId, TenantId), cancellationToken);
await workflowDispatcher.DispatchAsync(new TriggerWorkflowsRequest(activityType, bookmark, trigger, input, correlationId, default, contextId, TenantId), cancellationToken);
}
}
}

View file

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Services;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
@ -24,28 +20,17 @@ namespace Elsa.Activities.Http
)]
public class HttpEndpoint : Activity
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<IHttpRequestBodyParser> _parsers;
public HttpEndpoint(
IHttpContextAccessor httpContextAccessor,
IEnumerable<IHttpRequestBodyParser> parsers)
{
_httpContextAccessor = httpContextAccessor;
_parsers = parsers;
}
/// <summary>
/// The path that triggers this activity.
/// </summary>
[ActivityProperty(Hint = "The relative path that triggers this activity.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The relative path that triggers this activity.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public PathString Path { get; set; }
/// <summary>
/// The HTTP methods that triggers this activity.
/// </summary>
[ActivityProperty(
UIHint = ActivityPropertyUIHints.CheckList,
[ActivityInput(
UIHint = ActivityInputUIHints.CheckList,
Hint = "The HTTP methods that trigger this activity.",
Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" },
DefaultValue = new[] { "GET" },
@ -57,7 +42,7 @@ namespace Elsa.Activities.Http
/// 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>
[ActivityProperty(
[ActivityInput(
Hint = "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.",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public bool ReadContent { get; set; }
@ -66,39 +51,20 @@ namespace Elsa.Activities.Http
/// The <see cref="Type"/> to parse the received request content into if <seealso cref="ReadContent"/> is set to true.
/// If not set, the content will be parse into a default type, depending on the parser associated with the received content-type header.
/// </summary>
[ActivityProperty(Category = PropertyCategories.Advanced)]
[ActivityInput(Category = PropertyCategories.Advanced)]
public Type? TargetType { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context) => context.WorkflowExecutionContext.IsFirstPass ? await ExecuteInternalAsync(context.CancellationToken) : Suspend();
[ActivityOutput(Hint = "The received HTTP request.")]
public HttpRequestModel? Request { get; set; }
protected override ValueTask<IActivityExecutionResult> OnResumeAsync(ActivityExecutionContext context) => ExecuteInternalAsync(context.CancellationToken);
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) => context.WorkflowExecutionContext.IsFirstPass ? ExecuteInternal(context) : Suspend();
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) => ExecuteInternal(context);
private async ValueTask<IActivityExecutionResult> ExecuteInternalAsync(CancellationToken cancellationToken)
private IActivityExecutionResult ExecuteInternal(ActivityExecutionContext context)
{
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.ToString()),
Headers = request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString()),
Method = request.Method
};
if (ReadContent)
{
var parser = SelectContentParser(request.ContentType);
model.Body = await parser.ParseAsync(request, TargetType, cancellationToken);
}
var model = context.GetInput<HttpRequestModel>()!;
Request = model;
return Done(model);
}
private IHttpRequestBodyParser SelectContentParser(string contentType)
{
var simpleContentType = contentType?.Split(';').First();
var formatters = _parsers.OrderByDescending(x => x.Priority).ToList();
return formatters.FirstOrDefault(x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)) ?? formatters.Last();
}
}
}

View file

@ -29,10 +29,10 @@ namespace Elsa.Activities.Http
private IStringLocalizer<Redirect> T { get; }
[ActivityProperty(Hint = "The URL to redirect to (HTTP 302).", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The URL to redirect to (HTTP 302).", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public Uri Location { get; set; } = default!;
[ActivityProperty(Hint = "Whether or not the redirect is permanent (HTTP 301).", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "Whether or not the redirect is permanent (HTTP 301).", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public bool Permanent { get; set; }
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)

View file

@ -42,14 +42,14 @@ namespace Elsa.Activities.Http
/// <summary>
/// The URL to invoke.
/// </summary>
[ActivityProperty(Hint = "The URL to send the HTTP request to.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The URL to send the HTTP request to.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public Uri? Url { get; set; }
/// <summary>
/// The HTTP method to use.
/// </summary>
[ActivityProperty(
UIHint = ActivityPropertyUIHints.Dropdown,
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The HTTP method to use when making the request.",
Options = new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
@ -59,42 +59,42 @@ namespace Elsa.Activities.Http
/// <summary>
/// The body to send along with the request.
/// </summary>
[ActivityProperty(Hint = "The HTTP content to send along with the request.", UIHint = ActivityPropertyUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The HTTP content to send along with the request.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Content { get; set; }
/// <summary>
/// The Content Type header to send along with the request body.
/// </summary>
[ActivityProperty(
UIHint = ActivityPropertyUIHints.Dropdown,
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The content type to send with the request.",
Options = new[] { "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? ContentType { get; set; }
[ActivityProperty(Hint = "The Authorization header value to send.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The Authorization header value to send.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Authorization { get; set; }
/// <summary>
/// The headers to send along with the request.
/// </summary>
[ActivityProperty(Hint = "Additional headers to send along with the request.", UIHint = ActivityPropertyUIHints.Json)]
[ActivityInput(Hint = "Additional headers to send along with the request.", UIHint = ActivityInputUIHints.Json)]
public HttpRequestHeaders RequestHeaders { get; set; } = new();
[ActivityProperty(Hint = "Read the content of the response.", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "Read the content of the response.", SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public bool ReadContent { get; set; }
/// <summary>
/// A list of HTTP status codes this activity can handle.
/// </summary>
[ActivityProperty(
[ActivityInput(
Hint = "A list of possible HTTP status codes to handle.",
UIHint = ActivityPropertyUIHints.MultiText,
UIHint = ActivityInputUIHints.MultiText,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public ICollection<int> SupportedStatusCodes { get; set; } = new HashSet<int>(new[] { 200 });
public ICollection<int>? SupportedStatusCodes { get; set; } = new HashSet<int>(new[] { 200 });
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
@ -120,13 +120,13 @@ namespace Elsa.Activities.Http
var statusCode = (int) response.StatusCode;
var statusOutcome = statusCode.ToString();
var isSupportedStatusCode = SupportedStatusCodes.Contains(statusCode);
var isSupportedStatusCode = SupportedStatusCodes?.Contains(statusCode) == true;
var outcomes = new List<string> { OutcomeNames.Done, statusOutcome };
if (!isSupportedStatusCode)
outcomes.Add("UnSupportedStatusCode");
return Combine(Outcomes(outcomes), Output(responseModel));
return Combine(Output(responseModel), Outcomes(outcomes));
}
private IHttpResponseBodyParser SelectContentParser(string contentType)

View file

@ -3,10 +3,7 @@ using Elsa.Builders;
using Elsa.Services.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Elsa.Activities.Http
{

View file

@ -3,9 +3,6 @@ using Elsa.Builders;
using Elsa.Services.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Elsa.Activities.Http

View file

@ -34,35 +34,56 @@ namespace Elsa.Activities.Http
/// <summary>
/// The HTTP status code to return.
/// </summary>
[ActivityProperty(
UIHint = ActivityPropertyUIHints.Dropdown,
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The HTTP status code to write.",
Options = new[] { HttpStatusCode.OK, HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.NoContent, HttpStatusCode.Redirect, HttpStatusCode.BadRequest, HttpStatusCode.NotFound, HttpStatusCode.Conflict },
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
DefaultValue = HttpStatusCode.OK,
Category = PropertyCategories.Advanced
)]
public HttpStatusCode StatusCode { get; set; }
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
/// <summary>
/// The content to send along with the response
/// </summary>
[ActivityProperty(Hint = "The HTTP content to write.", UIHint = ActivityPropertyUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
[ActivityInput(Hint = "The HTTP content to write.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Content { get; set; }
/// <summary>
/// The Content-Type header to send along with the response.
/// </summary>
[ActivityProperty(
UIHint = ActivityPropertyUIHints.Dropdown,
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The HTTP content type header to write.",
Options = new[] { "text/plain", "text/html", "application/json", "application/xml" },
DefaultValue = "text/plain",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? ContentType { get; set; }
public string? ContentType { get; set; } = "text/plain";
/// <summary>
/// The character set to use when writing the response.
/// </summary>
[ActivityInput(
Hint = "The character set to use when writing the response.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "utf-8", "ASCII", "ANSI", "ISO-8859-1" },
DefaultValue = "utf-8",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = PropertyCategories.Advanced)]
public string CharSet { get; set; } = "utf-8";
/// <summary>
/// The headers to send along with the response.
/// </summary>
[ActivityProperty(Hint = "Additional headers to write.", UIHint = ActivityPropertyUIHints.Json)]
[ActivityInput(
Hint = "Additional headers to write.",
UIHint = ActivityInputUIHints.MultiLine,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[]{ SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json },
Category = PropertyCategories.Advanced
)]
public HttpResponseHeaders? ResponseHeaders { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
@ -74,7 +95,7 @@ namespace Elsa.Activities.Http
return Fault(T["Response has already started"]!);
response.StatusCode = (int) StatusCode;
response.ContentType = ContentType;
response.ContentType = $"{ContentType};charset={CharSet}";
var headers = ResponseHeaders;

View file

@ -13,13 +13,13 @@ namespace Elsa.Activities.Http.Bookmarks
public class HttpEndpointBookmarkProvider : BookmarkProvider<HttpEndpointBookmark, HttpEndpoint>
{
public override async ValueTask<IEnumerable<IBookmark>> GetBookmarksAsync(BookmarkProviderContext<HttpEndpoint> context, CancellationToken cancellationToken)
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<HttpEndpoint> context, CancellationToken cancellationToken)
{
var path = ToLower(await context.Activity.GetPropertyValueAsync(x => x.Path, cancellationToken))!;
var path = ToLower(await context.ReadActivityPropertyAsync(x => x.Path, cancellationToken))!;
var correlationId = ToLower(context.ActivityExecutionContext.WorkflowExecutionContext.CorrelationId);
var methods = (await context.Activity.GetPropertyValueAsync(x => x.Methods, cancellationToken))?.Select(x => x.ToLowerInvariant()) ?? Enumerable.Empty<string>();
var methods = (await context.ReadActivityPropertyAsync(x => x.Methods, cancellationToken))?.Select(x => x.ToLowerInvariant()) ?? Enumerable.Empty<string>();
HttpEndpointBookmark CreateBookmark(string method) => new(path, method, correlationId);
BookmarkResult CreateBookmark(string method) => Result(new(path, method, correlationId));
return methods.Select(CreateBookmark);
}

View file

@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.6" />
<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" />

View file

@ -1,9 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Services;
using Elsa.Activities.Signaling.Services;
using Microsoft.AspNetCore.Mvc;
using Open.Linq.AsyncExtensions;
namespace Elsa.Activities.Http.Endpoints.Signals
{
@ -12,23 +10,18 @@ namespace Elsa.Activities.Http.Endpoints.Signals
[Produces("application/json")]
public class DispatchEndpoint : ControllerBase
{
private readonly ITokenService _tokenService;
private readonly ISignaler _signaler;
public DispatchEndpoint(ITokenService tokenService, ISignaler signaler)
public DispatchEndpoint(ISignaler signaler)
{
_tokenService = tokenService;
_signaler = signaler;
}
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string token, CancellationToken cancellationToken)
public async Task<IActionResult> Handle(string token)
{
if (!_tokenService.TryDecryptToken(token, out Signal signal))
return NotFound();
await _signaler.DispatchSignalAsync(signal.Name, null, signal.WorkflowInstanceId, cancellationToken);
return Accepted();
var pendingWorkflows = await _signaler.DispatchSignalTokenAsync(token).ToList();
return Accepted(pendingWorkflows);
}
}
}

View file

@ -1,9 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Services;
using Elsa.Activities.Signaling.Services;
using Microsoft.AspNetCore.Mvc;
using Open.Linq.AsyncExtensions;
namespace Elsa.Activities.Http.Endpoints.Signals
{
@ -12,26 +10,17 @@ namespace Elsa.Activities.Http.Endpoints.Signals
[Produces("application/json")]
public class TriggerEndpoint : ControllerBase
{
private readonly ITokenService _tokenService;
private readonly ISignaler _signaler;
public TriggerEndpoint(ISignaler signaler) => _signaler = signaler;
public TriggerEndpoint(ITokenService tokenService, ISignaler signaler)
{
_tokenService = tokenService;
_signaler = signaler;
}
[HttpGet, HttpPost]
public async Task<IActionResult> Handle(string token, CancellationToken cancellationToken)
public async Task<IActionResult> Handle(string token)
{
if (!_tokenService.TryDecryptToken(token, out Signal signal))
return NotFound();
var result = await _signaler.TriggerSignalTokenAsync(token).ToList();
await _signaler.TriggerSignalAsync(signal.Name, null, signal.WorkflowInstanceId, cancellationToken);
return HttpContext.Response.HasStarted
? new EmptyResult()
: Accepted();
: Ok(result);
}
}
}

View file

@ -1,5 +1,5 @@
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Services;
using Elsa.Activities.Http.Services;
using Elsa.Activities.Signaling.Extensions;
using Elsa.Services.Models;
namespace Elsa.Activities.Http.Extensions
@ -8,12 +8,7 @@ namespace Elsa.Activities.Http.Extensions
{
public static string GenerateSignalUrl(this ActivityExecutionContext context, string signal)
{
var workflowInstanceId =
context.WorkflowExecutionContext.WorkflowInstance.Id;
var payload = new Signal(signal, workflowInstanceId);
var tokenService = context.GetService<ITokenService>();
var token = tokenService.CreateToken(payload);
var token = context.GenerateSignalToken(signal);
var url = $"/signals/trigger/{token}";
var absoluteUrlProvider = context.GetService<IAbsoluteUrlProvider>();
return absoluteUrlProvider.ToAbsoluteUrl(url).ToString();

View file

@ -1,4 +1,7 @@
using Elsa.Activities.Http.Middleware;
using Elsa.Activities.Http.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Builder
@ -7,8 +10,12 @@ namespace Microsoft.AspNetCore.Builder
{
public static IApplicationBuilder UseHttpActivities(this IApplicationBuilder app)
{
return app
.UseMiddleware<HttpEndpointMiddleware>();
var options = app.ApplicationServices.GetRequiredService<IOptions<HttpActivityOptions>>().Value;
var basePath = options.BasePath;
return basePath != null
? app.Map(basePath.Value, branch => branch.UseMiddleware<HttpEndpointMiddleware>())
: app.UseMiddleware<HttpEndpointMiddleware>();
}
}
}

View file

@ -54,5 +54,16 @@ namespace Elsa.Activities.Http.Extensions
correlationId = null;
return false;
}
public static bool GetUseDispatch(this HttpRequest request)
{
if (request.Query.ContainsKey("x-dispatch"))
return true;
if (request.Headers.ContainsKey("X-Dispatch"))
return true;
return false;
}
}
}

View file

@ -26,16 +26,13 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection AddHttpServices(this IServiceCollection services, Action<HttpActivityOptions>? configureOptions = null)
{
if (configureOptions != null)
{
if (configureOptions != null)
services.Configure(configureOptions);
}
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient(nameof(SendHttpRequest));
services
.AddSingleton<ITokenService, TokenService>()
.AddSingleton<IHttpRequestBodyParser, DefaultHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, JsonHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, FormHttpRequestBodyParser>()
@ -47,6 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
.AddHttpContextAccessor()
.AddNotificationHandlers(typeof(ConfigureJavaScriptEngine))
.AddLiquidFilter<SignalUrlFilter>("signal_url")
.AddJavaScriptTypeDefinitionProvider<HttpTypeDefinitionProvider>()
.AddDataProtection();
return services;

View file

@ -3,13 +3,14 @@ using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Http.Services;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Scripting.JavaScript.Messages;
using MediatR;
using Microsoft.AspNetCore.Http;
namespace Elsa.Activities.Http.JavaScript
{
public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScriptExpression>
public class ConfigureJavaScriptEngine : INotificationHandler<EvaluatingJavaScriptExpression>, INotificationHandler<RenderingTypeScriptDefinitions>
{
private readonly IAbsoluteUrlProvider _absoluteUrlProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
@ -42,5 +43,16 @@ namespace Elsa.Activities.Http.JavaScript
return Task.CompletedTask;
}
public Task Handle(RenderingTypeScriptDefinitions notification, CancellationToken cancellationToken)
{
var output = notification.Output;
output.AppendLine("declare function queryString(name: string): string;");
output.AppendLine("declare function absoluteUrl(url: string): string;");
output.AppendLine("declare function signalUrl(signal: string): string;");
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Elsa.Activities.Http.Models;
using Elsa.Scripting.JavaScript.Services;
namespace Elsa.Activities.Http.JavaScript
{
public class HttpTypeDefinitionProvider : TypeDefinitionProvider
{
public override IEnumerable<Type> CollectTypes(TypeDefinitionContext context)
{
return new[] { typeof(HttpRequestModel) };
}
}
}

View file

@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.Scripting.Liquid.Helpers;
using Elsa.Scripting.Liquid.Messages;
using Fluid;
@ -7,7 +8,6 @@ using Fluid.Values;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
namespace Elsa.Activities.Http.Liquid
{
@ -29,6 +29,7 @@ namespace Elsa.Activities.Http.Liquid
options.Scope.SetValue("Request", new ObjectValue(new LiquidRequestAccessor()));
options.MemberAccessStrategy.Register<HttpResponseModel>();
options.MemberAccessStrategy.Register<LiquidRequestAccessor, FluidValue>((_, name, _) =>
{
var request = _httpContextAccessor.HttpContext?.Request;

View file

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Bookmarks;
using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Services;
using Elsa.Persistence;
using Elsa.Persistence.Specifications.WorkflowInstances;
using Elsa.Services;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Open.Linq.AsyncExtensions;
namespace Elsa.Activities.Http.Middleware
{
public class HttpEndpointMiddleware
{
// TODO: Design multi-tenancy.
private const string? TenantId = default;
private readonly RequestDelegate _next;
public HttpEndpointMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(
HttpContext httpContext,
IWorkflowLaunchpad workflowLaunchpad,
IWorkflowInstanceStore workflowInstanceStore,
IWorkflowRegistry workflowRegistry,
IWorkflowBlueprintReflector workflowBlueprintReflector,
IEnumerable<IHttpRequestBodyParser> contentParsers)
{
var cancellationToken = CancellationToken.None; // Prevent half-way request abortion (which also happens when WriteHttpResponse writes to the response).
var path = httpContext.Request.Path.Value.ToLowerInvariant();
var method = httpContext.Request.Method!.ToLowerInvariant();
var request = httpContext.Request;
request.TryGetCorrelationId(out var correlationId);
var useDispatch = httpContext.Request.GetUseDispatch();
const string activityType = nameof(HttpEndpoint);
var trigger = new HttpEndpointBookmark(path, method, null);
var bookmark = new HttpEndpointBookmark(path, method, correlationId?.ToLowerInvariant());
var collectWorkflowsContext = new CollectWorkflowsContext(activityType, bookmark, trigger, correlationId, default, default, TenantId);
var pendingWorkflows = await workflowLaunchpad.CollectWorkflowsAsync(collectWorkflowsContext, cancellationToken).ToList();
var pendingWorkflowInstanceIds = pendingWorkflows.Select(x => x.WorkflowInstanceId).Distinct();
var pendingWorkflowInstances = (await workflowInstanceStore.FindManyAsync(new WorkflowInstanceIdsSpecification(pendingWorkflowInstanceIds), cancellationToken: cancellationToken)).ToDictionary(x => x.Id);
var workflowDefinitionIds = pendingWorkflowInstances.Values.Select(x => x.DefinitionId).Distinct().ToHashSet();
var workflowBlueprints = (await workflowRegistry.FindManyAsync(x => x.IsPublished && workflowDefinitionIds.Contains(x.Id), cancellationToken)).ToDictionary(x => x.Id);
var serviceProvider = httpContext.RequestServices;
var workflowBlueprintWrappers = (await Task.WhenAll(workflowBlueprints.Values.Select(async x => await workflowBlueprintReflector.ReflectAsync(serviceProvider, x, cancellationToken)))).ToDictionary(x => x.WorkflowBlueprint.Id);
var commonInputModel = new HttpRequestModel(
new Uri(request.Path.ToString(), UriKind.Relative),
request.Method,
request.Query.ToDictionary(x => x.Key, x => x.Value.ToString()),
request.Headers.ToDictionary(x => x.Key, x => x.Value.ToString())
);
var orderedContentParsers = contentParsers.OrderByDescending(x => x.Priority).ToList();
var simpleContentType = request.ContentType?.Split(';').First();
var contentParser = orderedContentParsers.FirstOrDefault(x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)) ?? orderedContentParsers.Last();
foreach (var pendingWorkflow in pendingWorkflows)
{
var pendingWorkflowInstance = pendingWorkflowInstances[pendingWorkflow.WorkflowInstanceId];
var workflowBlueprintWrapper = workflowBlueprintWrappers[pendingWorkflowInstance.DefinitionId];
var activityWrapper = workflowBlueprintWrapper.GetActivity<HttpEndpoint>(pendingWorkflow.ActivityId!);
var readContent = await activityWrapper!.EvaluatePropertyValueAsync(x => x.ReadContent, cancellationToken);
var inputModel = commonInputModel;
if (readContent)
{
var targetType = await activityWrapper.EvaluatePropertyValueAsync(x => x.TargetType, cancellationToken);
inputModel = inputModel with { Body = await contentParser.ParseAsync(request, targetType, cancellationToken) };
}
if (useDispatch)
await workflowLaunchpad.DispatchPendingWorkflowAsync(pendingWorkflow, inputModel, cancellationToken);
else
await workflowLaunchpad.ExecutePendingWorkflowsAsync(pendingWorkflows, inputModel, cancellationToken);
}
if (pendingWorkflows.Count > 0)
{
if (useDispatch)
{
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(pendingWorkflows), cancellationToken);
}
return;
}
await _next(httpContext);
}
}
}

View file

@ -1,36 +0,0 @@
using System.Threading.Tasks;
using Elsa.Activities.Http.Bookmarks;
using Elsa.Activities.Http.Extensions;
using Elsa.Dispatch;
using MediatR;
using Microsoft.AspNetCore.Http;
namespace Elsa.Activities.Http.Middleware
{
public class HttpEndpointMiddleware
{
// TODO: Design multi-tenancy.
private const string? TenantId = default;
private readonly RequestDelegate _next;
public HttpEndpointMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext httpContext, IMediator mediator)
{
var path = httpContext.Request.Path.Value.ToLowerInvariant();
var method = httpContext.Request.Method!.ToLowerInvariant();
var cancellationToken = httpContext.RequestAborted;
httpContext.Request.TryGetCorrelationId(out var correlationId);
const string activityType = nameof(HttpEndpoint);
var trigger = new HttpEndpointBookmark(path, method, null);
var bookmark = new HttpEndpointBookmark(path, method, correlationId?.ToLowerInvariant());
var workflowInstanceCount = await mediator.Send(new TriggerWorkflowsRequest(activityType, bookmark, trigger, default, correlationId, default, TenantId), cancellationToken);
if (workflowInstanceCount == 0)
{
await _next(httpContext);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more