Merge branch 'main' into feature/batch-processing

This commit is contained in:
Sipke Schoorstra 2024-01-26 19:30:20 +01:00
commit 1293b7af29
82 changed files with 1647 additions and 370 deletions

View file

@ -31,12 +31,8 @@ jobs:
else
echo "VERSION=3.1.0-preview.${{github.run_number}}" >> $GITHUB_ENV
fi
- name: Build
run: dotnet build --configuration Release /p:Version=${VERSION}
- name: Test
run: dotnet test --configuration Release /p:Version=${VERSION} --no-build
- name: Pack
run: dotnet pack --configuration Release /p:Version=${VERSION} /p:PackageOutputPath=$(pwd)/packages
- name: Compile+Test+Pack
run: ./build.sh Compile+Test+Pack --version ${VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:

44
.github/workflows/pr.yml vendored Normal file
View file

@ -0,0 +1,44 @@
# ------------------------------------------------------------------------------
# <auto-generated>
#
# This code was generated.
#
# - To turn off auto-generation set:
#
# [CustomGitHubActions (AutoGenerate = false)]
#
# - To trigger manual generation invoke:
#
# nuke --generate-configuration GitHubActions_pr --host GitHubActions
#
# </auto-generated>
# ------------------------------------------------------------------------------
name: pr
on:
pull_request:
branches:
- main
- 'v3*'
paths:
- '**/*'
- '!docs/**/*'
- '!package.json'
- '!package-lock.json'
- '!readme.md'
jobs:
ubuntu-latest:
name: ubuntu-latest
runs-on: ubuntu-latest
steps:
- if: ${{ runner.os == 'Windows' }}
name: 'Use GNU tar'
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- uses: actions/checkout@v3
- name: 'Run: Compile, Test, Pack'
run: ./build.cmd Compile Test Pack

6
.gitignore vendored
View file

@ -51,9 +51,6 @@ node_modules/
.stylelintrc.yml
.eslintrc.yml
#github
.github/
#volatile
App_Data/
*.db
@ -76,3 +73,6 @@ src/designer/elsa-workflows-designer/loader
src/modules/Elsa.Workflows.Designer/package-lock.json
unlist.sh
# build artifacts
/artifacts

120
.nuke/build.schema.json Normal file
View file

@ -0,0 +1,120 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/build",
"title": "Build Schema",
"definitions": {
"build": {
"type": "object",
"properties": {
"Configuration": {
"type": "string",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
"Help": {
"type": "boolean",
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"IgnoreFailedSources": {
"type": "boolean",
"description": "Ignore unreachable sources during Restore"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
"Partition": {
"type": "string",
"description": "Partition to use on CI"
},
"Plan": {
"type": "boolean",
"description": "Shows the execution plan (HTML)"
},
"Profile": {
"type": "array",
"description": "Defines the profiles to load",
"items": {
"type": "string"
}
},
"Root": {
"type": "string",
"description": "Root directory during build execution"
},
"Skip": {
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"Clean",
"Compile",
"Pack",
"Restore",
"Test"
]
}
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"Clean",
"Compile",
"Pack",
"Restore",
"Test"
]
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
},
"Version": {
"type": "string"
}
}
}
}
}

4
.nuke/parameters.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "./build.schema.json",
"Solution": "Elsa.sln"
}

View file

@ -1,3 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
@ -16,12 +17,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D
common.props = common.props
configureawait.props = configureawait.props
docker-compose.yml = docker-compose.yml
frameworks.props = frameworks.props
icon.png = icon.png
NuGet.Config = NuGet.Config
packages.props = packages.props
README.md = README.md
update-migrations.sh = update-migrations.sh
frameworks.props = frameworks.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
@ -254,51 +255,52 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.My
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj", "{84AB9951-3DBF-43A4-8F5D-AEED1102CE88}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.ServiceBus.IntegrationTests", "test\integration\Elsa.ServiceBus.IntegrationTests\Elsa.ServiceBus.IntegrationTests.csproj", "{9504E3F3-F77F-437A-8644-D23D7F5FCF8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{B818988E-639C-4E6E-85C1-B231BCAD9DAB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{B789B743-0FBE-4BBC-9F45-84C20C8D4814}"
ProjectSection(SolutionItems) = preProject
.github\workflows\elsa-server-and-studio.yml = .github\workflows\elsa-server-and-studio.yml
.github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml
.github\workflows\packages.yml = .github\workflows\packages.yml
.github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml
.github\workflows\packages.yml = .github\workflows\packages.yml
.github\workflows\pr.yml = .github\workflows\pr.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}"
EndProject
@ -881,8 +883,8 @@ Global
{05F3B23E-CF28-467B-AD3C-595EA4ED6B96} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
{74ACD9D0-8B4C-42FA-A582-E93BF0075023} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D} = {AB797AF0-C12C-46DE-A157-7E25625C6200}
{B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{732BF088-6AD7-4C4D-9A48-8074253596D4} = {B818988E-639C-4E6E-85C1-B231BCAD9DAB}
{B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
{790E94F2-5393-47DF-AC52-D9247F5B243A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6} = {DD089B8B-DA73-492A-9010-F772D1C178DA}

7
build.cmd Executable file
View file

@ -0,0 +1,7 @@
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

74
build.ps1 Normal file
View file

@ -0,0 +1,74 @@
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetChannel = "STS"
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_NOLOGO = 1
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
$env:PATH = "$DotNetDirectory;$env:PATH"
}
Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
& $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null
& $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
}
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

67
build.sh Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="STS"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_NOLOGO=1
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
export PATH="$DOTNET_DIRECTORY:$PATH"
fi
echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then
"$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true
"$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true
fi
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

11
build/.editorconfig Normal file
View file

@ -0,0 +1,11 @@
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning

View file

@ -0,0 +1,56 @@
using System.Collections.Generic;
using Nuke.Common.CI.GitHubActions;
using Nuke.Common.CI.GitHubActions.Configuration;
using Nuke.Common.Execution;
using Nuke.Common.Utilities;
using Nuke.Components;
[CustomGitHubActions(
"pr",
GitHubActionsImage.UbuntuLatest,
OnPullRequestBranches = ["main"],
OnPullRequestIncludePaths = ["**/*"],
OnPullRequestExcludePaths = ["docs/**/*", "package.json", "package-lock.json", "readme.md"],
PublishArtifacts = false,
InvokedTargets = [nameof(ICompile.Compile), nameof(ITest.Test), nameof(IPack.Pack)],
CacheKeyFiles = []
)
]
public partial class Build;
class CustomGitHubActionsAttribute : GitHubActionsAttribute
{
public CustomGitHubActionsAttribute(string name, GitHubActionsImage image, params GitHubActionsImage[] images) : base(name, image, images)
{
}
protected override GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCollection<ExecutableTarget> relevantTargets)
{
var job = base.GetJobs(image, relevantTargets);
var newSteps = new List<GitHubActionsStep>(job.Steps);
newSteps.Insert(0, new GitHubActionsUseGnuTarStep());
job.Steps = newSteps.ToArray();
return job;
}
}
class GitHubActionsUseGnuTarStep : GitHubActionsStep
{
public override void Write(CustomFileWriter writer)
{
writer.WriteLine("- if: ${{ runner.os == 'Windows' }}");
using (writer.Indent())
{
writer.WriteLine("name: 'Use GNU tar'");
writer.WriteLine("shell: cmd");
writer.WriteLine("run: |");
using (writer.Indent())
{
writer.WriteLine("echo \"Adding GNU tar to PATH\"");
writer.WriteLine("echo C:\\Program Files\\Git\\usr\\bin>>\"%GITHUB_PATH%\"");
}
}
}
}

69
build/Build.cs Normal file
View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Nuke.Common;
using Nuke.Common.CI;
using Nuke.Common.Git;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Utilities.Collections;
using Nuke.Components;
using Serilog;
[ShutdownDotNetAfterServerBuild]
partial class Build : NukeBuild, ITest, IPack
{
public static int Main() => Execute<Build>(x => ((ICompile) x).Compile);
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
[GitRepository] readonly GitRepository GitRepository;
AbsolutePath SourceDirectory => RootDirectory / "src";
public AbsolutePath PackagesDirectory => RootDirectory / "packages";
string TagVersion => GitRepository.Tags.SingleOrDefault(x => "v".StartsWith(x))?[1..];
bool IsTaggedBuild => !string.IsNullOrWhiteSpace(TagVersion);
string VersionSuffix;
[Parameter]
string Version;
protected override void OnBuildInitialized()
{
VersionSuffix = !IsTaggedBuild
? $"preview-{DateTime.UtcNow:yyyyMMdd-HHmm}"
: "";
if (IsLocalBuild)
{
VersionSuffix = $"dev-{DateTime.UtcNow:yyyyMMdd-HHmm}";
}
Log.Information("BUILD SETUP");
Log.Information("Configuration:\t{Configuration}", Configuration);
Log.Information("Version suffix:\t{VersionSuffix}", VersionSuffix);
Log.Information("Version:\t\t{Version}", Version);
Log.Information("Tagged build:\t{IsTaggedBuild}", IsTaggedBuild);
}
Target Clean => _ => _
.Before<IRestore>(x => x.Restore)
.Executes(() =>
{
SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(x => x.DeleteDirectory());
((IHazArtifacts) this).ArtifactsDirectory.CreateOrCleanDirectory();
});
public Configure<DotNetBuildSettings> CompileSettings => _ => _
// ensure we don't generate too much output in CI run
// 0 Turns off emission of all warning messages
// 1 Displays severe warning messages
.SetWarningLevel(IsServerBuild ? 0 : 1);
public IEnumerable<Project> TestProjects => ((IHazSolution) this).Solution.AllProjects.Where(x => x.Name.EndsWith("Tests"));
}

16
build/Configuration.cs Normal file
View file

@ -0,0 +1,16 @@
using System;
using System.ComponentModel;
using System.Linq;
using Nuke.Common.Tooling;
[TypeConverter(typeof(TypeConverter<Configuration>))]
public class Configuration : Enumeration
{
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
public static Configuration Release = new Configuration { Value = nameof(Release) };
public static implicit operator string(Configuration configuration)
{
return configuration.Value;
}
}

View file

@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<UseArtifactsOutput>false</UseArtifactsOutput>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.targets file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

22
build/_build.csproj Normal file
View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
<NukeScriptDirectory>..</NukeScriptDirectory>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<NukeExcludeLogs>true</NukeExcludeLogs>
<NukeExcludeDirectoryBuild>true</NukeExcludeDirectoryBuild>
<IsPackable>false</IsPackable>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Components" Version="8.0.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=HeapView_002EDelegateAllocation/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableHidesOuterVariable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InterpolatedStringExpressionIsNotIFormattable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">0</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_USER_LINEBREAKS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_INVOCATION_LPAR/@EntryValue">False</s:Boolean>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ATTRIBUTE_LENGTH_FOR_SAME_LINE/@EntryValue">120</s:Int64>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">IF_OWNER_IS_SINGLE_LINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -10,7 +10,6 @@ using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Http.Handlers;
using Elsa.Http.Options;
using Elsa.MongoDb.Extensions;
using Elsa.MongoDb.Modules.Identity;

View file

@ -17,7 +17,7 @@
"Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;",
"MongoDb": "mongodb://localhost:27017/elsa-workflows",
"AzureServiceBus": "",
"RabbitMq": "amqp://guest:guest@localhost:5672/elsa"
"RabbitMq": "amqp://guest:guest@localhost:5672"
},
"Smtp": {
"Host": "localhost",

View file

@ -18,6 +18,7 @@
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.MassTransit\Elsa.MassTransit.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Python\Elsa.Python.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Webhooks\Elsa.Webhooks.csproj"/>
@ -34,8 +35,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio" Version="3.0.3"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.3"/>
<PackageReference Include="Elsa.Studio" Version="3.0.4"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.4"/>
</ItemGroup>
</Project>

View file

@ -3,9 +3,10 @@ using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Webhooks.Extensions;
using Microsoft.AspNetCore.Mvc;
const bool useMassTransit = true;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
var services = builder.Services;
@ -16,32 +17,39 @@ var identityTokenSection = identitySection.GetSection("Tokens");
// Add Elsa services.
services
.AddElsa(elsa => elsa
.UseSasTokens()
.UseIdentity(identity =>
.AddElsa(elsa =>
{
elsa
.UseSasTokens()
.UseIdentity(identity =>
{
identity.IdentityOptions = options => identitySection.Bind(options);
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseScheduling()
.UseJavaScript(options => options.AllowClrAccess = true)
.UseLiquid()
.UseCSharp()
.UsePython()
.UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options))
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
.UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options))
.UseWorkflowsApi()
.UseRealTimeWorkflows()
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>();
if (useMassTransit)
{
identity.IdentityOptions = options => identitySection.Bind(options);
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseScheduling()
.UseJavaScript(options => options.AllowClrAccess = true)
.UseLiquid()
.UseCSharp()
.UsePython()
.UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options))
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
.UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options))
.UseWorkflowsApi()
.UseRealTimeWorkflows()
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()
);
elsa.UseMassTransit();
}
});
services.AddHealthChecks();

View file

@ -13,7 +13,8 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;"
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;",
"RabbitMq": "rabbitmq://guest:guest@localhost:5672"
},
"Hosting": {
"BasePath": ""

View file

@ -21,8 +21,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio" Version="3.0.2"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.2"/>
<PackageReference Include="Elsa.Studio" Version="3.0.4"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.4"/>
</ItemGroup>
<ItemGroup>

View file

@ -19,9 +19,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Elsa.Studio" Version="3.0.2"/>
<PackageReference Include="Elsa.Studio.Core.BlazorWasm" Version="3.0.2"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.2"/>
<PackageReference Include="Elsa.Studio" Version="3.0.4"/>
<PackageReference Include="Elsa.Studio.Core.BlazorWasm" Version="3.0.4"/>
<PackageReference Include="Elsa.Studio.Login.BlazorWasm" Version="3.0.4"/>
</ItemGroup>
<ItemGroup>

View file

@ -145,11 +145,26 @@ public static class ActivityExtensions
/// Sets the root activity in the specified activity.
/// </summary>
public static void SetRoot(this JsonObject container, JsonObject root) => container.SetProperty(root, "root");
/// <summary>
/// Gets the root activity in the specified activity.
/// </summary>
public static string? GetWorkflowDefinitionId(this JsonObject activity) => activity.GetProperty<string?>("workflowDefinitionId");
public static string? GetWorkflowDefinitionId(this JsonObject activity)
{
// Determine if this activity represents a workflow definition activity based on the presence of the following properties.
var workflowDefinitionIdNode = activity.GetProperty("workflowDefinitionId");
var workflowDefinitionVersionId = activity.GetProperty("workflowDefinitionVersionId");
var latestAvailablePublishedVersion = activity.GetProperty("latestAvailablePublishedVersion");
var latestAvailablePublishedVersionId = activity.GetProperty("latestAvailablePublishedVersionId");
if (workflowDefinitionIdNode == null || workflowDefinitionVersionId == null || latestAvailablePublishedVersion == null || latestAvailablePublishedVersionId == null)
return null; // Not a workflow definition activity.
if (workflowDefinitionIdNode is JsonValue value)
return value.ToString();
return null; // Not a workflow definition activity.
}
/// <summary>
/// Determines whether the given activity is a workflow definition activity.
@ -157,7 +172,7 @@ public static class ActivityExtensions
/// <param name="activity">The JsonObject representing the activity.</param>
/// <returns>Returns true if the activity is a workflow definition activity; otherwise, false.</returns>
public static bool GetIsWorkflowDefinitionActivity(this JsonObject activity) => activity.ContainsKey("workflowDefinitionId") && activity.ContainsKey("workflowDefinitionVersionId");
/// <summary>
/// Gets a value indicating whether the specified activity can trigger the workflow.
/// </summary>

View file

@ -28,7 +28,7 @@ public class ScheduleActivityHandler : AlterationHandlerBase<ScheduleActivity>
{
// If the activity is in a faulted state, reset it to Running.
if (existingActivityExecutionContext.Status == ActivityStatus.Faulted)
existingActivityExecutionContext.Status = ActivityStatus.Running;
existingActivityExecutionContext.TransitionTo(ActivityStatus.Running);
// Schedule the activity execution context.
var parentContext = existingActivityExecutionContext.ParentActivityExecutionContext;

View file

@ -32,7 +32,7 @@
<PackageReference Include="Dapper" Version="2.1.24"/>
<PackageReference Include="FluentMigrator" Version="3.3.2"/>
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2"/>
<PackageReference Include="System.Data.SqlClient" Version="4.8.5"/>
<PackageReference Include="System.Data.SqlClient" Version="4.8.6"/>
</ItemGroup>
</Project>

View file

@ -211,8 +211,8 @@ public class HttpEndpoint : Trigger<HttpRequest>
var routeData = GetRouteData(httpContext, path);
var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!);
var queryStringDictionary = httpContext.Request.Query.ToDictionary<KeyValuePair<string, StringValues>, string, object>(queryString => queryString.Key, queryString => queryString.Value[0]!);
var headersDictionary = httpContext.Request.Headers.ToDictionary<KeyValuePair<string, StringValues>, string, object>(header => header.Key, header => header.Value[0]!);
var queryStringDictionary = httpContext.Request.Query.ToObjectDictionary();
var headersDictionary = httpContext.Request.Headers.ToObjectDictionary();
context.Set(RouteData, routeDictionary);
context.Set(QueryStringData, queryStringDictionary);
@ -225,48 +225,60 @@ public class HttpEndpoint : Trigger<HttpRequest>
return;
}
// Read content, if any.
try
// Handle Form Fields
if (request.HasFormContentType)
{
var content = await ParseContentAsync(context, request);
ParsedContent.Set(context, content);
var formFields = request.Form.ToObjectDictionary();
ParsedContent.Set(context, formFields);
// Read files, if any.
var files = ReadFilesAsync(context, request);
if (files.Any())
{
if (!ValidateFileSizes(context, httpContext, files))
{
await HandleFileSizeTooLargeAsync(context, httpContext);
return;
}
if (!ValidateFileExtensionWhitelist(context, httpContext, files))
{
await HandleInvalidFileExtensionWhitelistAsync(context, httpContext);
return;
}
if (!ValidateFileExtensionBlacklist(context, httpContext, files))
{
await HandleInvalidFileExtensionBlacklistAsync(context, httpContext);
return;
}
if (!ValidateFileMimeTypes(context, httpContext, files))
{
await HandleInvalidFileMimeTypesAsync(context, httpContext);
return;
}
Files.Set(context, files.ToArray());
}
}
catch (JsonException e)
else
{
await HandleInvalidJsonPayloadAsync(context, httpContext, e);
throw;
}
// Read files, if any.
var files = ReadFilesAsync(context, request);
if (files.Any())
{
if (!ValidateFileSizes(context, httpContext, files))
// Parse Non-Form content.
try
{
await HandleFileSizeTooLargeAsync(context, httpContext);
return;
var content = await ParseContentAsync(context, request);
ParsedContent.Set(context, content);
}
catch (JsonException e)
{
await HandleInvalidJsonPayloadAsync(context, httpContext, e);
throw;
}
if (!ValidateFileExtensionWhitelist(context, httpContext, files))
{
await HandleInvalidFileExtensionWhitelistAsync(context, httpContext);
return;
}
if (!ValidateFileExtensionBlacklist(context, httpContext, files))
{
await HandleInvalidFileExtensionBlacklistAsync(context, httpContext);
return;
}
if (!ValidateFileMimeTypes(context, httpContext, files))
{
await HandleInvalidFileMimeTypesAsync(context, httpContext);
return;
}
Files.Set(context, files.ToArray());
}
// Complete.

View file

@ -0,0 +1,28 @@
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elsa.Extensions;
/// <summary>
/// Contains extension methods for the <see cref="StringValues"/> Enumerable dictonaries.
/// </summary>
public static class StringValueExtensions
{
/// <summary>
/// Convert the collection to the desired dictionary type.
/// </summary>
/// <param name="collection"></param>
/// <returns></returns>
public static Dictionary<string, object> ToObjectDictionary(this IEnumerable<KeyValuePair<string, StringValues>> collection)
{
return collection.ToDictionary<KeyValuePair<string, StringValues>, string, object>(
item => item.Key,
item => item.Value.Count <= 1 ?
item.Value[0]!
: item.Value.ToArray());
}
}

View file

@ -158,12 +158,10 @@ public class HttpFeature : FeatureBase
.AddRequestHandler<ValidateWorkflowRequestHandler, ValidateWorkflowRequest, ValidateWorkflowResponse>()
.AddNotificationHandler<UpdateRouteTable>()
// Content parsers.
.AddScoped<IHttpContentParser, StringHttpContentParser>()
.AddScoped<IHttpContentParser, JsonHttpContentParser>()
.AddScoped<IHttpContentParser, XmlHttpContentParser>()
.AddScoped<IHttpContentParser, FormHttpContentParser>()
// Content parsers.
.AddSingleton<IHttpContentParser, JsonHttpContentParser>()
.AddSingleton<IHttpContentParser, XmlHttpContentParser>()
// HTTP content factories.
.AddScoped<IHttpContentFactory, TextContentFactory>()
.AddScoped<IHttpContentFactory, JsonContentFactory>()

View file

@ -1,28 +0,0 @@
using System.Xml.Serialization;
using Elsa.Extensions;
using Elsa.Http.Contracts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
namespace Elsa.Http.Parsers;
/// <summary>
/// Reads application/xml and text/xml content type streams.
/// </summary>
public class FormHttpContentParser : IHttpContentParser
{
/// <inheritdoc />
public int Priority => 0;
/// <inheritdoc />
public bool GetSupportsContentType(string contentType) => contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.InvariantCultureIgnoreCase);
/// <inheritdoc />
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
{
using var reader = new FormReader(content);
return (await reader.ReadFormAsync(cancellationToken)).ToDictionary<KeyValuePair<string, StringValues>, string, object>(formField => formField.Key, formField => formField.Value[0]!); ;
}
}

View file

@ -1,24 +0,0 @@
using Elsa.Expressions.Helpers;
using Elsa.Http.Contracts;
namespace Elsa.Http.Parsers;
/// <summary>
/// Reads any content type streams as a string and attempts to convert the string to the specified return type.
/// </summary>
public class StringHttpContentParser : IHttpContentParser
{
/// <inheritdoc />
public int Priority => -10;
/// <inheritdoc />
public bool GetSupportsContentType(string contentType) => !contentType.StartsWith("multipart/form-data");
/// <inheritdoc />
public async Task<object> ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken)
{
using var reader = new StreamReader(content, leaveOpen: true);
var text = await reader.ReadToEndAsync();
return returnType == null ? text : text.ConvertTo(returnType)!;
}
}

View file

@ -6,6 +6,7 @@ using Elsa.MassTransit.Implementations;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
using MassTransit;
namespace Elsa.MassTransit.Activities;
@ -29,7 +30,10 @@ public class PublishMessage : CodeActivity
/// <summary>
/// The message to send. Must be a concrete implementation of the configured <see cref="MessageType"/>.
/// </summary>
[Input(Description = "The message to send. Must be a concrete implementation of the configured message type.")]
[Input(
Description = "The message to send. Must be a concrete implementation of the configured message type.",
UIHint = InputUIHints.MultiLine
)]
public Input<object> Message { get; set; } = default!;
/// <inheritdoc />

View file

@ -6,8 +6,8 @@ using Elsa.MassTransit.Options;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using Humanizer;
using Microsoft.Extensions.Options;
@ -17,40 +17,31 @@ namespace Elsa.MassTransit.Implementations;
/// <summary>
/// Provides activities to the system from the configured MassTransit message types.
/// </summary>
public class MassTransitActivityTypeProvider : IActivityProvider
public class MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions<MassTransitActivityOptions> options, IActivityDescriber activityDescriber) : IActivityProvider
{
private readonly IActivityFactory _activityFactory;
private readonly MassTransitActivityOptions _options;
/// <summary>
/// Constructor.
/// </summary>
public MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions<MassTransitActivityOptions> options)
{
_activityFactory = activityFactory;
_options = options.Value;
}
/// <inheritdoc />
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var messageTypes = _options.MessageTypes;
var descriptors = CreateDescriptors(messageTypes).ToList();
return new(descriptors);
var messageTypes = options.Value.MessageTypes;
var descriptors = await CreateDescriptorsAsync(messageTypes, cancellationToken);
return descriptors.ToList();
}
private IEnumerable<ActivityDescriptor> CreateDescriptors(IEnumerable<Type> messageTypes)
private async Task<IEnumerable<ActivityDescriptor>> CreateDescriptorsAsync(IEnumerable<Type> messageTypes, CancellationToken cancellationToken = default)
{
var descriptors = new List<ActivityDescriptor>();
foreach (var messageType in messageTypes)
{
yield return CreateMessageReceivedDescriptor(messageType);
descriptors.Add(await CreateMessageReceivedDescriptor(messageType, cancellationToken));
if(messageType.IsClass)
yield return CreatePublishMessageDescriptor(messageType);
descriptors.Add(await CreatePublishMessageDescriptor(messageType, cancellationToken));
}
return descriptors;
}
private ActivityDescriptor CreateMessageReceivedDescriptor(Type messageType)
private async Task<ActivityDescriptor> CreateMessageReceivedDescriptor(Type messageType, CancellationToken cancellationToken = default)
{
var activityAttr = messageType.GetCustomAttribute<ActivityAttribute>();
var typeName = activityAttr?.Type ?? messageType.Name;
@ -61,6 +52,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider
var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit";
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
var outputDescriptor = await activityDescriber.DescribeOutputProperty<MessageReceived, object>(x => x.Result!, cancellationToken);
var openOutputType = typeof(Output<>);
var outputType = openOutputType.MakeGenericType(messageType);
outputDescriptor.Type = outputType;
return new()
{
@ -73,17 +69,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider
IsBrowsable = true,
Outputs =
{
new OutputDescriptor
{
Description = "The received message",
DisplayName = "Received Message",
Name = nameof(MessageReceived.Result),
Type = typeof(object)
}
outputDescriptor
},
Constructor = context =>
{
var activity = _activityFactory.Create<MessageReceived>(context);
var activity = activityFactory.Create<MessageReceived>(context);
activity.Type = fullTypeName;
activity.MessageType = messageType;
return activity;
@ -91,7 +81,7 @@ public class MassTransitActivityTypeProvider : IActivityProvider
};
}
private ActivityDescriptor CreatePublishMessageDescriptor(Type messageType)
private async Task<ActivityDescriptor> CreatePublishMessageDescriptor(Type messageType, CancellationToken cancellationToken = default)
{
var activityAttr = messageType.GetCustomAttribute<ActivityAttribute>();
var typeName = activityAttr?.Type ?? messageType.Name;
@ -104,6 +94,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider
var descriptionAttr = messageType.GetCustomAttribute<DescriptionAttribute>();
var description = descriptionAttr?.Description ?? activityAttr?.Description;
var messageInputDescriptor = await activityDescriber.DescribeInputPropertyAsync<PublishMessage, object>(x => x.Message, cancellationToken: cancellationToken);
var openInputType = typeof(Input<>);
var inputType = openInputType.MakeGenericType(messageType);
messageInputDescriptor.Type = inputType;
return new()
{
TypeName = fullTypeName,
@ -115,18 +110,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider
IsBrowsable = true,
Inputs =
{
new InputDescriptor
{
Description = "The message to publish.",
UIHint = InputUIHints.MultiLine,
DisplayName = "Message",
Type = typeof(Input<object>),
Name = nameof(PublishMessage.Message)
}
messageInputDescriptor
},
Constructor = context =>
{
var activity = _activityFactory.Create<PublishMessage>(context);
var activity = activityFactory.Create<PublishMessage>(context);
activity.Type = fullTypeName;
activity.MessageType = messageType;
return activity;

View file

@ -3,16 +3,22 @@ using Elsa.ProtoActor.Extensions;
using Elsa.ProtoActor.Mappers;
using Elsa.ProtoActor.ProtoBuf;
using Elsa.ProtoActor.Snapshots;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.State;
using Microsoft.Extensions.DependencyInjection;
using Proto;
using Proto.Cluster;
using Proto.Persistence;
using CancellationTokens = Elsa.Workflows.Models.CancellationTokens;
using WorkflowStatus = Elsa.Workflows.WorkflowStatus;
using WorkflowSubStatus = Elsa.Workflows.WorkflowSubStatus;
namespace Elsa.ProtoActor.Grains;
@ -38,6 +44,8 @@ internal class WorkflowInstance : WorkflowInstanceBase
private IWorkflowHost _workflowHost = default!;
private WorkflowState _workflowState = default!;
private readonly ICollection<CancellationTokenSource> _cancellationTokenSources = new List<CancellationTokenSource>();
/// <inheritdoc />
public WorkflowInstance(
IServiceScopeFactory scopeFactory,
@ -139,6 +147,10 @@ internal class WorkflowInstance : WorkflowInstanceBase
var versionOptions = VersionOptions.FromString(request.VersionOptions);
var cancellationToken = Context.CancellationToken;
var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_cancellationTokenSources.Add(cancellationTokenSource);
cancellationToken = cancellationTokenSource.Token;
// Only need to reconstruct a workflow host if not already done so during CanStart.
if (_workflowHost == null!)
{
@ -155,7 +167,9 @@ internal class WorkflowInstance : WorkflowInstanceBase
CorrelationId = correlationId,
Input = input,
Properties = properties,
TriggerActivityId = request.TriggerActivityId
TriggerActivityId = request.TriggerActivityId,
StatusUpdatedCallback = StatusUpdated,
CancellationTokens = new CancellationTokens(cancellationToken)
};
var task = _workflowHost.StartWorkflowAsync(startWorkflowOptions, cancellationToken);
@ -187,6 +201,31 @@ internal class WorkflowInstance : WorkflowInstanceBase
});
}
private void StatusUpdated(WorkflowExecutionContext context)
{
_ = Task.Run(async () => await Update(context));
}
private async Task Update(WorkflowExecutionContext context)
{
using var scope = _scopeFactory.CreateScope();
var extractor = scope.ServiceProvider.GetRequiredService<IWorkflowStateExtractor>();
var bookmarkPersistor = scope.ServiceProvider.GetRequiredService<IBookmarksPersister>();
var workflowState = extractor.Extract(context);
var originalBookmarks = _workflowHost.WorkflowState.Bookmarks;
_workflowState = workflowState;
await SaveSnapshotAsync();
SaveWorkflowInstance(workflowState);
var newBookmarks = workflowState.Bookmarks;
var diff = Diff.For(originalBookmarks, newBookmarks);
var bookmarkRequest = new UpdateBookmarksRequest(workflowState.DefinitionId, diff, workflowState.CorrelationId);
await bookmarkPersistor.PersistBookmarksAsync(bookmarkRequest);
}
/// <inheritdoc />
public override Task Stop()
{
@ -208,6 +247,10 @@ internal class WorkflowInstance : WorkflowInstanceBase
var activityInstanceId = request.ActivityInstanceId.NullIfEmpty();
var activityHash = request.ActivityHash.NullIfEmpty();
var cancellationToken = Context.CancellationToken;
var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_cancellationTokenSources.Add(cancellationTokenSource);
cancellationToken = cancellationTokenSource.Token;
var resumeWorkflowHostOptions = new ResumeWorkflowHostOptions
{
@ -218,7 +261,8 @@ internal class WorkflowInstance : WorkflowInstanceBase
ActivityInstanceId = activityInstanceId,
ActivityHash = activityHash,
Input = _input,
Properties = _properties
Properties = _properties,
CancellationTokens = cancellationToken
};
var definitionId = _definitionId;
@ -260,6 +304,18 @@ internal class WorkflowInstance : WorkflowInstanceBase
/// <inheritdoc />
public override Task<WorkflowExecutionResponse> Resume(ResumeWorkflowRequest request) => Task.FromResult(new WorkflowExecutionResponse());
public override async Task Cancel()
{
if (_workflowState.Status != WorkflowStatus.Finished)
{
_workflowState.SubStatus = WorkflowSubStatus.Cancelled;
_workflowState.Status = WorkflowStatus.Finished;
}
foreach(var source in _cancellationTokenSources)
source.Cancel();
}
/// <inheritdoc />
public override async Task<ExportWorkflowStateResponse> ExportState(ExportWorkflowStateRequest request)
{

View file

@ -12,6 +12,7 @@ service WorkflowInstance {
rpc Start (StartWorkflowRequest) returns (WorkflowExecutionResponse);
rpc Stop (Empty) returns (Empty);
rpc Resume (ResumeWorkflowRequest) returns (WorkflowExecutionResponse);
rpc Cancel (Empty) returns (Empty);
rpc ExportState(ExportWorkflowStateRequest) returns (ExportWorkflowStateResponse);
rpc ImportState(ImportWorkflowStateRequest) returns (ImportWorkflowStateResponse);
}

View file

@ -16,6 +16,7 @@ using Elsa.Workflows.State;
using Proto.Cluster;
using Bookmark = Elsa.Workflows.Models.Bookmark;
using CountRunningWorkflowsRequest = Elsa.Workflows.Runtime.Requests.CountRunningWorkflowsRequest;
using WorkflowStatus = Elsa.Workflows.WorkflowStatus;
namespace Elsa.ProtoActor.Services;
@ -246,6 +247,13 @@ internal class ProtoActorWorkflowRuntime : IWorkflowRuntime
return result!;
}
/// <inheritdoc />
public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken)
{
var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId);
await client.Cancel(cancellationToken);
}
/// <inheritdoc />
public async Task<IEnumerable<WorkflowMatch>> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default)
{
@ -296,7 +304,7 @@ internal class ProtoActorWorkflowRuntime : IWorkflowRuntime
DefinitionId = request.DefinitionId,
Version = request.Version,
CorrelationId = request.CorrelationId,
WorkflowStatus = Workflows.WorkflowStatus.Running
WorkflowStatus = WorkflowStatus.Running
};
return await _workflowInstanceStore.CountAsync(filter, cancellationToken);
}

View file

@ -1,7 +1,8 @@
using Elsa.Features.Services;
using Elsa.Webhooks.Features;
namespace Elsa.Webhooks.Extensions;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds extensions to <see cref="IModule"/> that enables the <see cref="WebhooksFeature"/> feature.

View file

@ -5,7 +5,6 @@
<Import Project="..\..\..\frameworks.props"/>
<PropertyGroup>
<Version>3.0.0</Version>
<Description>
Provides API endpoints for client applications such as the Elsa Designer web component.
</Description>

View file

@ -22,7 +22,7 @@ internal class BulkDelete : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request!.DefinitionIds, cancellationToken);
var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken);
return new Response(count);
}
}

View file

@ -1,25 +1,30 @@
using System.Text.Json.Serialization;
using Elsa.Abstractions;
using Elsa.Workflows.Runtime.Contracts;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkCancel;
public class BulkCancel : ElsaEndpoint<Request, Response>
{
private readonly IWorkflowRuntime _workflowRuntime;
public BulkCancel(IWorkflowRuntime workflowRuntime)
{
_workflowRuntime = workflowRuntime;
}
public override void Configure()
{
Post("/bulk-actions/cancel/workflow-instances/by-id");
ConfigurePermissions("cancel:workflow-instances");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
// TODO: Implement workflow cancellation.
var count = -1;
var tasks = request.Ids.Select(id => _workflowRuntime.CancelWorkflowAsync(id, cancellationToken)).ToList();
await Task.WhenAll(tasks);
var count = tasks.Count(t => t.IsCompletedSuccessfully);
return new(count);
}
public record BulkCancelWorkflowInstancesRequest(ICollection<string> Ids);
public record BulkCancelWorkflowInstancesResponse([property: JsonPropertyName("cancelled")] int CancelledCount);
}

View file

@ -0,0 +1,44 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Notifications;
using Elsa.Workflows.Signals;
namespace Elsa.Workflows;
public partial class ActivityExecutionContext
{
private readonly CancellationTokenRegistration _cancellationRegistration;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly INotificationSender _publisher;
private void CancelActivity()
{
// If the activity is not running, do nothing.
if (Status != ActivityStatus.Running && Status != ActivityStatus.Faulted)
return;
_ = Task.Run(async () => await CancelActivityAsync());
}
private async Task CancelActivityAsync()
{
// Select all child contexts.
var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this).ToList();
foreach (var childContext in childContexts)
childContext._cancellationTokenSource.Cancel();
TransitionTo(ActivityStatus.Canceled);
ClearBookmarks();
ClearCompletionCallbacks();
WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == NodeId);
// Add an execution log entry.
AddExecutionLogEntry("Canceled", payload: JournalData, includeActivityState: true);
_cancellationRegistration.Dispose();
await this.SendSignalAsync(new CancelSignal());
await _publisher.SendAsync(new ActivityCancelled(this));
}
}

View file

@ -0,0 +1,40 @@
using Elsa.Extensions;
using Elsa.Workflows.Models;
namespace Elsa.Workflows;
public partial class ActivityExecutionContext
{
/// <summary>
/// Adds a new <see cref="WorkflowExecutionLogEntry"/> to the execution log of the current <see cref="Workflows.WorkflowExecutionContext"/>.
/// </summary>
/// <param name="eventName">The name of the event.</param>
/// <param name="message">The message of the event.</param>
/// <param name="source">The source of the activity. For example, the source file name and line number in case of composite activities.</param>
/// <param name="payload">Any contextual data related to this event.</param>
/// <param name="includeActivityState">True to include activity state with this event, false otherwise.</param>
/// <returns>Returns the created <see cref="WorkflowExecutionLogEntry"/>.</returns>
public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = default, string? source = default, object? payload = default, bool includeActivityState = false)
{
var activityState = includeActivityState ? ActivityState : default;
var logEntry = new WorkflowExecutionLogEntry(
Id,
ParentActivityExecutionContext?.Id,
Activity.Id,
Activity.Type,
Activity.Version,
Activity.Name,
NodeId,
activityState,
_systemClock.UtcNow,
WorkflowExecutionContext.ExecutionLogSequence++,
eventName,
message,
source ?? Activity.GetSource(),
payload);
WorkflowExecutionContext.ExecutionLog.Add(logEntry);
return logEntry;
}
}

View file

@ -4,6 +4,7 @@ using Elsa.Common.Contracts;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
@ -15,7 +16,7 @@ namespace Elsa.Workflows;
/// <summary>
/// Represents the context of an activity execution.
/// </summary>
public class ActivityExecutionContext : IExecutionContext
public partial class ActivityExecutionContext : IExecutionContext
{
private readonly ISystemClock _systemClock;
private readonly List<Bookmark> _bookmarks = new();
@ -47,6 +48,10 @@ public class ActivityExecutionContext : IExecutionContext
Tag = tag;
CancellationToken = cancellationToken;
Id = id;
_publisher = GetRequiredService<INotificationSender>();
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_cancellationRegistration = _cancellationTokenSource.Token.Register(CancelActivity);
}
/// <summary>
@ -123,8 +128,21 @@ public class ActivityExecutionContext : IExecutionContext
/// <summary>
/// The current status of the activity.
/// </summary>
public ActivityStatus Status { get; set; }
public ActivityStatus Status { get; private set; }
/// <summary>
/// Sets the current status of the activity.
/// </summary>
public void TransitionTo(ActivityStatus status)
{
Status = status;
if (Status is ActivityStatus.Completed
or ActivityStatus.Canceled
or ActivityStatus.Faulted)
_cancellationRegistration.Dispose();
}
/// <summary>
/// Gets or sets the exception that occurred during the activity execution, if any.
/// </summary>

View file

@ -0,0 +1,37 @@
namespace Elsa.Workflows;
/// <summary>
/// Provides context to the currently executing workflow.
/// </summary>
public partial class WorkflowExecutionContext
{
private ICollection<CancellationTokenSource> _cancellationTokenSources = new List<CancellationTokenSource>();
private ICollection<CancellationTokenRegistration> _cancellationRegistrations = new List<CancellationTokenRegistration>();
/// <summary>
/// Cancels the workflow and all it's children.
/// </summary>
public void Cancel()
{
foreach (var source in _cancellationTokenSources)
source.Cancel();
_cancellationTokenSources.Clear();
}
private void CancelWorkflow()
{
Bookmarks.Clear();
_completionCallbackEntries.Clear();
if (Status != WorkflowStatus.Running && SubStatus != WorkflowSubStatus.Faulted)
return;
AddExecutionLogEntry("Workflow cancelled");
TransitionTo(WorkflowSubStatus.Cancelled);
foreach (var registration in _cancellationRegistrations)
registration.Dispose();
}
}

View file

@ -0,0 +1,36 @@
using Elsa.Extensions;
using Elsa.Workflows.Models;
namespace Elsa.Workflows;
public partial class WorkflowExecutionContext
{
/// <summary>
/// Adds a new <see cref="WorkflowExecutionLogEntry"/> to the execution log of the current <see cref="Workflows.WorkflowExecutionContext"/>.
/// </summary>
/// <param name="eventName">The name of the event.</param>
/// <param name="message">The message of the event.</param>
/// <param name="payload">Any contextual data related to this event.</param>
/// <returns>Returns the created <see cref="WorkflowExecutionLogEntry"/>.</returns>
public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = default, object? payload = default)
{
var logEntry = new WorkflowExecutionLogEntry(
Id,
default,
Workflow.Id,
Workflow.Type,
Workflow.Identity.Version,
Workflow.Name,
Workflow.Identity.Id,
default,
SystemClock.UtcNow,
ExecutionLogSequence++,
eventName,
message,
Workflow.GetSource(),
payload);
ExecutionLog.Add(logEntry);
return logEntry;
}
}

View file

@ -28,7 +28,7 @@ public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, Ac
/// Provides context to the currently executing workflow.
/// </summary>
[PublicAPI]
public class WorkflowExecutionContext : IExecutionContext
public partial class WorkflowExecutionContext : IExecutionContext
{
private static readonly object ActivityOutputRegistryKey = new();
private static readonly object LastActivityResultKey = new();
@ -37,6 +37,7 @@ public class WorkflowExecutionContext : IExecutionContext
private readonly IList<ActivityCompletionCallbackEntry> _completionCallbackEntries = new List<ActivityCompletionCallbackEntry>();
private IList<ActivityExecutionContext> _activityExecutionContexts;
private readonly IHasher _hasher;
private readonly Action<WorkflowExecutionContext>? _statusUpdatedCallback;
/// <summary>
/// Initializes a new instance of <see cref="WorkflowExecutionContext"/>.
@ -51,6 +52,7 @@ public class WorkflowExecutionContext : IExecutionContext
string? triggerActivityId,
IEnumerable<ActivityIncident> incidents,
DateTimeOffset createdAt,
Action<WorkflowExecutionContext>? statusUpdatedCallback,
CancellationTokens cancellationTokens)
{
ServiceProvider = serviceProvider;
@ -70,6 +72,13 @@ public class WorkflowExecutionContext : IExecutionContext
CreatedAt = createdAt;
CancellationTokens = cancellationTokens;
Incidents = incidents.ToList();
var appSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationTokens.ApplicationCancellationToken);
_cancellationTokenSources.Add(appSource);
var sysSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationTokens.SystemCancellationToken);
_cancellationTokenSources.Add(sysSource);
_cancellationRegistrations.Add(appSource.Token.Register(CancelWorkflow));
_cancellationRegistrations.Add(sysSource.Token.Register(CancelWorkflow));
}
/// <summary>
@ -84,6 +93,7 @@ public class WorkflowExecutionContext : IExecutionContext
IDictionary<string, object>? properties = default,
ExecuteActivityDelegate? executeDelegate = default,
string? triggerActivityId = default,
Action<WorkflowExecutionContext>? statusUpdatedCallback = default,
CancellationTokens cancellationTokens = default)
{
var systemClock = serviceProvider.GetRequiredService<ISystemClock>();
@ -99,6 +109,7 @@ public class WorkflowExecutionContext : IExecutionContext
properties,
executeDelegate,
triggerActivityId,
statusUpdatedCallback,
cancellationTokens
);
}
@ -115,6 +126,7 @@ public class WorkflowExecutionContext : IExecutionContext
IDictionary<string, object>? properties = default,
ExecuteActivityDelegate? executeDelegate = default,
string? triggerActivityId = default,
Action<WorkflowExecutionContext>? statusUpdatedCallback = default,
CancellationTokens cancellationTokens = default)
{
var workflowExecutionContext = await CreateAsync(
@ -128,6 +140,7 @@ public class WorkflowExecutionContext : IExecutionContext
properties,
executeDelegate,
triggerActivityId,
statusUpdatedCallback,
cancellationTokens);
var workflowStateExtractor = serviceProvider.GetRequiredService<IWorkflowStateExtractor>();
@ -150,6 +163,7 @@ public class WorkflowExecutionContext : IExecutionContext
IDictionary<string, object>? properties = default,
ExecuteActivityDelegate? executeDelegate = default,
string? triggerActivityId = default,
Action<WorkflowExecutionContext>? statusUpdatedCallback = default,
CancellationTokens cancellationTokens = default)
{
// Setup a workflow execution context.
@ -163,6 +177,7 @@ public class WorkflowExecutionContext : IExecutionContext
triggerActivityId,
incidents,
createdAt,
statusUpdatedCallback,
cancellationTokens)
{
MemoryRegister = workflow.CreateRegister()
@ -521,6 +536,20 @@ public class WorkflowExecutionContext : IExecutionContext
throw new Exception($"Cannot transition from {SubStatus} to {subStatus}");
SubStatus = subStatus;
//For now only trigger on Cancelled, since the other statuses are handling via the host/runner
if (SubStatus == WorkflowSubStatus.Cancelled
&& _statusUpdatedCallback is not null)
_statusUpdatedCallback(this);
if (Status == WorkflowStatus.Finished
|| SubStatus == WorkflowSubStatus.Suspended)
{
foreach (var registration in _cancellationRegistrations)
{
registration.Dispose();
}
}
}
/// <summary>

View file

@ -1,7 +1,6 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using Elsa.Common.Contracts;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
@ -74,45 +73,6 @@ public static class ActivityExecutionContextExtensions
/// </summary>
public static bool IsTriggerOfWorkflow(this ActivityExecutionContext context) => context.WorkflowExecutionContext.TriggerActivityId == context.Activity.Id;
/// <summary>
/// Adds a new <see cref="WorkflowExecutionLogEntry"/> to the execution log of the current <see cref="WorkflowExecutionContext"/>.
/// </summary>
/// <param name="context">The <see cref="ActivityExecutionContext"/></param> being extended.
/// <param name="eventName">The name of the event.</param>
/// <param name="message">The message of the event.</param>
/// <param name="source">The source of the activity. For example, the source file name and line number in case of composite activities.</param>
/// <param name="payload">Any contextual data related to this event.</param>
/// <param name="includeActivityState">True to include activity state with this event, false otherwise.</param>
/// <returns>Returns the created <see cref="WorkflowExecutionLogEntry"/>.</returns>
public static WorkflowExecutionLogEntry AddExecutionLogEntry(this ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default, bool includeActivityState = false)
{
var activity = context.Activity;
var activityInstanceId = context.Id;
var parentActivityInstanceId = context.ParentActivityExecutionContext?.Id;
var workflowExecutionContext = context.WorkflowExecutionContext;
var now = context.GetRequiredService<ISystemClock>().UtcNow;
var activityState = includeActivityState ? context.ActivityState : default;
var logEntry = new WorkflowExecutionLogEntry(
activityInstanceId,
parentActivityInstanceId,
activity.Id,
activity.Type,
activity.Version,
activity.Name,
context.NodeId,
activityState,
now,
workflowExecutionContext.ExecutionLogSequence++,
eventName,
message,
source ?? activity.GetSource(),
payload);
workflowExecutionContext.ExecutionLog.Add(logEntry);
return logEntry;
}
/// <summary>
/// Creates a workflow variable by name and optionally sets the value.
/// </summary>
@ -477,7 +437,7 @@ public static class ActivityExecutionContextExtensions
await childContext.CancelActivityAsync();
// Mark the activity as complete.
context.Status = ActivityStatus.Completed;
context.TransitionTo(ActivityStatus.Completed);
// Record the outcomes, if any.
if (outcomes != null)
@ -595,7 +555,7 @@ public static class ActivityExecutionContextExtensions
await CancelActivityAsync(childContext);
var publisher = context.GetRequiredService<INotificationSender>();
context.Status = ActivityStatus.Canceled;
context.TransitionTo(ActivityStatus.Canceled);
context.ClearBookmarks();
context.ClearCompletionCallbacks();
context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == context.NodeId);

View file

@ -292,7 +292,12 @@ public static class ExpressionExecutionContextExtensions
select v;
var variable = q.FirstOrDefault();
variable?.Set(context, value);
if(variable != null)
variable.Set(context, value);
if (variable == null)
CreateVariable(context, variableName, value);
}
/// <summary>

View file

@ -125,36 +125,4 @@ public static class WorkflowExecutionContextExtensions
/// Returns true if all activities have completed or canceled, false otherwise.
/// </summary>
public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.ActivityExecutionContexts.All(x => x.IsCompleted);
/// <summary>
/// Adds a new <see cref="WorkflowExecutionLogEntry"/> to the execution log of the current <see cref="WorkflowExecutionContext"/>.
/// </summary>
/// <param name="context">The <see cref="WorkflowExecutionContext"/></param> being extended.
/// <param name="eventName">The name of the event.</param>
/// <param name="message">The message of the event.</param>
/// <param name="payload">Any contextual data related to this event.</param>
/// <returns>Returns the created <see cref="WorkflowExecutionLogEntry"/>.</returns>
public static WorkflowExecutionLogEntry AddExecutionLogEntry(this WorkflowExecutionContext context, string eventName, string? message = default, object? payload = default)
{
var now = context.GetRequiredService<ISystemClock>().UtcNow;
var logEntry = new WorkflowExecutionLogEntry(
context.Id,
default,
context.Workflow.Id,
context.Workflow.Type,
context.Workflow.Identity.Version,
context.Workflow.Name,
context.Workflow.Identity.Id,
default,
now,
context.ExecutionLogSequence++,
eventName,
message,
context.Workflow.GetSource(),
payload);
context.ExecutionLog.Add(logEntry);
return logEntry;
}
}

View file

@ -41,16 +41,24 @@ public class DefaultActivityInvokerMiddleware : IActivityExecutionMiddleware
// Evaluate input properties.
await EvaluateInputPropertiesAsync(context);
// Prevent the activity from being started if cancellation is requested.
if (context.CancellationToken.IsCancellationRequested)
{
context.TransitionTo(ActivityStatus.Canceled);
context.AddExecutionLogEntry("Activity cancelled");
return;
}
// Check if the activity can be executed.
if (!await context.Activity.CanExecuteAsync(context))
{
context.Status = ActivityStatus.Pending;
context.TransitionTo(ActivityStatus.Pending);
context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time");
return;
}
context.Status = ActivityStatus.Running;
context.TransitionTo(ActivityStatus.Running);
// Execute activity.
await ExecuteActivityAsync(context);

View file

@ -50,7 +50,7 @@ public class ExceptionHandlingMiddleware : IActivityExecutionMiddleware
{
_logger.LogWarning(e, "An exception was caught from a downstream middleware component");
context.Exception = e;
context.Status = ActivityStatus.Faulted;
context.TransitionTo(ActivityStatus.Faulted);
var activity = context.Activity;
var exceptionState = ExceptionState.FromException(e);

View file

@ -39,6 +39,10 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware
while (scheduler.HasAny)
{
// Do not start a workflow if cancellation has been requested.
if (context.CancellationTokens.ApplicationCancellationToken.IsCancellationRequested)
break;
var currentWorkItem = scheduler.Take();
await ExecuteWorkItemAsync(context, currentWorkItem);
}

View file

@ -18,4 +18,5 @@ public class RunWorkflowOptions
public IDictionary<string, object>? Properties { get; set; }
public string? TriggerActivityId { get; set; }
public CancellationTokens CancellationTokens { get; set; }
public Action<WorkflowExecutionContext>? StatusUpdatedCallback { get; set; }
}

View file

@ -36,8 +36,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer
{
var options = GetSerializerOptions();
var serializingWorkflowState = new SerializingWorkflowState(options);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
return JsonSerializer.Serialize(workflowState, options);
}
@ -46,8 +46,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer
{
var options = GetSerializerOptions();
var serializingWorkflowState = new SerializingWorkflowState(options);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
return JsonSerializer.SerializeToUtf8Bytes(workflowState, options);
}
@ -56,8 +56,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer
{
var options = GetSerializerOptions();
var serializingWorkflowState = new SerializingWorkflowState(options);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken);
return JsonSerializer.SerializeToElement(workflowState, options);
}
@ -110,8 +110,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
options.Converters.Add(new PolymorphicObjectConverterFactory());
options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry));
options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory));
options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory));
return options;
}
}

View file

@ -61,7 +61,7 @@ public class VariablePersistenceManager : IVariablePersistenceManager
foreach (var context in contexts)
{
var variables = GetLocalVariables(context);
var variables = GetLocalVariables(context).ToList();
var storageDriverContext = new StorageDriverContext(context, cancellationToken);
foreach (var variable in variables)
@ -89,7 +89,7 @@ public class VariablePersistenceManager : IVariablePersistenceManager
public async Task DeleteVariablesAsync(ActivityExecutionContext context)
{
var register = context.ExpressionExecutionContext.Memory;
var variableList = GetLocalVariables(context);
var variableList = GetLocalVariables(context).ToList();
var cancellationToken = context.CancellationToken;
var storageDriverContext = new StorageDriverContext(context, cancellationToken);

View file

@ -94,6 +94,7 @@ public class WorkflowRunner : IWorkflowRunner
var properties = options?.Properties;
var correlationId = options?.CorrelationId;
var triggerActivityId = options?.TriggerActivityId;
var statusUpdatedCallback = options?.StatusUpdatedCallback;
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(
scope.ServiceProvider,
workflow,
@ -103,6 +104,7 @@ public class WorkflowRunner : IWorkflowRunner
properties,
default,
triggerActivityId,
statusUpdatedCallback,
options?.CancellationTokens ?? cancellationToken);
// Schedule the first activity.
@ -122,6 +124,7 @@ public class WorkflowRunner : IWorkflowRunner
var properties = options?.Properties;
var correlationId = options?.CorrelationId ?? workflowState.CorrelationId;
var triggerActivityId = options?.TriggerActivityId;
var statusUpdatedCallback = options?.StatusUpdatedCallback;
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(
scope.ServiceProvider,
workflow,
@ -130,6 +133,7 @@ public class WorkflowRunner : IWorkflowRunner
input, properties,
default,
triggerActivityId,
statusUpdatedCallback,
options?.CancellationTokens ?? cancellationToken);
var bookmarkId = options?.BookmarkId;

View file

@ -36,6 +36,31 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
return state;
}
/// <inheritdoc />
public WorkflowExecutionContext Apply(WorkflowExecutionContext workflowExecutionContext, WorkflowState state)
{
workflowExecutionContext.Id = state.Id;
workflowExecutionContext.CorrelationId = state.CorrelationId;
workflowExecutionContext.SubStatus = state.SubStatus;
workflowExecutionContext.Bookmarks = state.Bookmarks;
workflowExecutionContext.Output = state.Output;
workflowExecutionContext.ExecutionLogSequence = state.ExecutionLogSequence;
workflowExecutionContext.CreatedAt = state.CreatedAt;
ApplyInput(state, workflowExecutionContext);
ApplyProperties(state, workflowExecutionContext);
ApplyActivityExecutionContexts(state, workflowExecutionContext);
ApplyCompletionCallbacks(state, workflowExecutionContext);
ApplyScheduledActivities(state, workflowExecutionContext);
return workflowExecutionContext;
}
private void ApplyInput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)
{
// Only add input from state if the input doesn't already exist on the workflow execution context.
foreach (var inputItem in state.Input)
if (!workflowExecutionContext.Input.ContainsKey(inputItem.Key)) workflowExecutionContext.Input.Add(inputItem.Key, inputItem.Value);
}
private IDictionary<string, object> GetPersistableInput(WorkflowExecutionContext workflowExecutionContext)
{
// TODO: This is a temporary solution. We need to find a better way to handle this.
@ -52,24 +77,6 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
return filteredInput;
}
/// <inheritdoc />
public WorkflowExecutionContext Apply(WorkflowExecutionContext workflowExecutionContext, WorkflowState state)
{
// Do not map input. We don't want to overwrite the input that was passed to the workflow.
workflowExecutionContext.Id = state.Id;
workflowExecutionContext.CorrelationId = state.CorrelationId;
workflowExecutionContext.SubStatus = state.SubStatus;
workflowExecutionContext.Bookmarks = state.Bookmarks;
workflowExecutionContext.Output = state.Output;
workflowExecutionContext.ExecutionLogSequence = state.ExecutionLogSequence;
workflowExecutionContext.CreatedAt = state.CreatedAt;
ApplyProperties(state, workflowExecutionContext);
ApplyActivityExecutionContexts(state, workflowExecutionContext);
ApplyCompletionCallbacks(state, workflowExecutionContext);
ApplyScheduledActivities(state, workflowExecutionContext);
return workflowExecutionContext;
}
private void ExtractProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)
{
state.Properties = workflowExecutionContext.Properties;
@ -127,7 +134,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
activityExecutionContext.Id = activityExecutionContextState.Id;
activityExecutionContext.Properties = properties;
activityExecutionContext.ActivityState = activityExecutionContextState.ActivityState ?? new Dictionary<string, object>();
activityExecutionContext.Status = activityExecutionContextState.Status;
activityExecutionContext.TransitionTo(activityExecutionContextState.Status);
activityExecutionContext.StartedAt = activityExecutionContextState.StartedAt;
activityExecutionContext.CompletedAt = activityExecutionContextState.CompletedAt;
activityExecutionContext.Tag = activityExecutionContextState.Tag;

View file

@ -1,5 +1,4 @@
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Requests;
namespace Elsa.Workflows.Runtime.Contracts;
@ -11,5 +10,5 @@ public interface IBookmarksPersister
/// <summary>
/// Persists bookmarks and raises events.
/// </summary>
Task PersistBookmarksAsync(WorkflowExecutionContext context, Diff<Bookmark> diff);
Task PersistBookmarksAsync(UpdateBookmarksRequest updateBookmarksRequest);
}

View file

@ -0,0 +1,24 @@
namespace Elsa.Workflows.Runtime.Contracts;
/// <summary>
/// Stores <see cref="WorkflowExecutionContext"/> records.
/// </summary>
public interface IWorkflowExecutionContextStore
{
/// <summary>
/// Saves a record of the <see cref="WorkflowExecutionContext"/>.
/// </summary>
/// <param name="context">The <see cref="WorkflowExecutionContext"/> to save.</param>
Task SaveAsync(WorkflowExecutionContext context);
/// <summary>
/// Finds a <see cref="WorkflowExecutionContext"/> with the specified ID.
/// </summary>
/// <returns>The matching entity or null if no match was found.</returns>
Task<WorkflowExecutionContext?> FindAsync(string workflowExecutionContextId);
/// <summary>
/// Deletes the record of the <see cref="WorkflowExecutionContext"/> with the specified ID if it exists.
/// </summary>
Task DeleteAsync(string workflowExecutionContextId);
}

View file

@ -60,6 +60,13 @@ public interface IWorkflowRuntime
/// <param name="options">Options for executing the workflow.</param>
Task<WorkflowExecutionResult> ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowOptions? options = default);
/// <summary>
/// Cancels the execution of a workflow.
/// </summary>
/// <param name="workflowInstanceId">The ID of the workflow instance to cancel.</param>
/// <param name="cancellationToken"></param>
Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default);
/// <summary>
/// Finds all the workflows that can be started or resumed based on a query model.
/// </summary>

View file

@ -73,6 +73,11 @@ public class WorkflowRuntimeFeature : FeatureBase
/// </summary>
public Func<IServiceProvider, IWorkflowInboxMessageStore> WorkflowInboxStore { get; set; } = sp => sp.GetRequiredService<MemoryWorkflowInboxMessageStore>();
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowExecutionContextStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowExecutionContextStore> WorkflowExecutionContextStore { get; set; } = sp => sp.GetRequiredService<MemoryWorkflowExecutionContextStore>();
/// <summary>
/// A factory that instantiates an <see cref="IDistributedLockProvider"/>.
/// </summary>
@ -157,6 +162,7 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddScoped(WorkflowExecutionLogStore)
.AddScoped(ActivityExecutionLogStore)
.AddScoped(WorkflowInboxStore)
.AddScoped(WorkflowExecutionContextStore)
.AddSingleton(RunTaskDispatcher)
.AddSingleton(BackgroundActivityScheduler)
.AddScoped<IBookmarkManager, DefaultBookmarkManager>()
@ -188,6 +194,7 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddMemoryStore<WorkflowExecutionLogRecord, MemoryWorkflowExecutionLogStore>()
.AddMemoryStore<ActivityExecutionRecord, MemoryActivityExecutionStore>()
.AddMemoryStore<WorkflowInboxMessage, MemoryWorkflowInboxMessageStore>()
.AddMemoryStore<WorkflowExecutionContext, MemoryWorkflowExecutionContextStore>()
// Distributed locking.
.AddScoped(DistributedLockProvider)
@ -207,7 +214,8 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddNotificationHandler<DeleteActivityExecutionLogRecords>()
.AddNotificationHandler<ReadWorkflowInboxMessage>()
.AddNotificationHandler<DeliverWorkflowMessagesFromInbox>()
.AddNotificationHandler<DeleteWorkflowExecutionLogRecords>()
.AddNotificationHandler<DeleteWorkflowExecutionLogRecords>()
.AddNotificationHandler<WorkflowExecutionContextNotificationsHandler>()
// Workflow activation strategies.
.AddScoped<IWorkflowActivationStrategy, SingletonStrategy>()

View file

@ -0,0 +1,36 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Notifications;
using Elsa.Workflows.Runtime.Contracts;
namespace Elsa.Workflows.Runtime.Handlers;
/// <summary>
/// Deletes workflow execution log records in response to the <see cref="WorkflowInstancesDeleting"/> notification.
/// </summary>
internal class WorkflowExecutionContextNotificationsHandler :
INotificationHandler<WorkflowExecuting>,
INotificationHandler<WorkflowExecuted>
{
private readonly IWorkflowExecutionContextStore _store;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteWorkflowExecutionLogRecords"/> class.
/// </summary>
public WorkflowExecutionContextNotificationsHandler(IWorkflowExecutionContextStore store)
{
_store = store;
}
/// <inheritdoc />
public Task HandleAsync(WorkflowExecuting notification, CancellationToken cancellationToken)
{
return _store.SaveAsync(notification.WorkflowExecutionContext);
}
/// <inheritdoc />
public Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken)
{
return _store.DeleteAsync(notification.WorkflowExecutionContext.Id);
}
}

View file

@ -1,6 +1,7 @@
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
namespace Elsa.Workflows.Runtime.Middleware.Workflows;
@ -24,6 +25,7 @@ public class PersistBookmarkMiddleware : WorkflowExecutionMiddleware
await Next(context);
var updatedBookmarks = context.Bookmarks.ToList();
var diff = Diff.For(originalBookmarks, updatedBookmarks);
await _bookmarksPersister.PersistBookmarksAsync(context, diff);
var bookmarkRequest = new UpdateBookmarksRequest(context.Id, diff, context.CorrelationId);
await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest);
}
}

View file

@ -7,5 +7,5 @@ namespace Elsa.Workflows.Runtime.Notifications;
/// </summary>
/// <param name="WorkflowExecutionContext">The workflow execution context.</param>
/// <param name="IndexedWorkflowBookmarks">The bookmarks that were added, removed, or unchanged.</param>
public record WorkflowBookmarksIndexed(WorkflowExecutionContext WorkflowExecutionContext, IndexedWorkflowBookmarks IndexedWorkflowBookmarks) : INotification;
public record WorkflowBookmarksIndexed(IndexedWorkflowBookmarks IndexedWorkflowBookmarks) : INotification;

View file

@ -9,4 +9,4 @@ namespace Elsa.Workflows.Runtime.Notifications;
/// </summary>
/// <param name="Context">The workflow execution context.</param>
/// <param name="Diff">The bookmarks that were added, removed, or unchanged.</param>
public record WorkflowBookmarksPersisted(WorkflowExecutionContext Context, Diff<Bookmark> Diff) : INotification;
public record WorkflowBookmarksPersisted(Diff<Bookmark> Diff) : INotification;

View file

@ -24,4 +24,9 @@ public class StartWorkflowHostOptions
/// <summary>Cancellation tokens that can be used to cancel the workflow instance without cancelling system-level operations.</summary>
public CancellationTokens CancellationTokens { get; set; }
/// <summary>
/// Callback method that will be called when the status of the workflow has been updated
/// </summary>
public Action<WorkflowExecutionContext>? StatusUpdatedCallback { get; set; }
}

View file

@ -1,7 +1,5 @@
using Elsa.Mediator;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Notifications;
using Elsa.Workflows.Runtime.Requests;
@ -11,25 +9,15 @@ namespace Elsa.Workflows.Runtime.Services;
/// <inheritdoc />
public class BookmarksPersister(IBookmarkUpdater bookmarkUpdater, INotificationSender notificationSender) : IBookmarksPersister
{
/// <inheritdoc />
public async Task PersistBookmarksAsync(WorkflowExecutionContext context, Diff<Bookmark> diff)
public async Task PersistBookmarksAsync(UpdateBookmarksRequest updateBookmarksRequest)
{
var cancellationToken = context.CancellationTokens.SystemCancellationToken;
var updateBookmarksContext = new UpdateBookmarksRequest(context.Id, diff, context.CorrelationId);
await bookmarkUpdater.UpdateBookmarksAsync(updateBookmarksContext, cancellationToken);
await bookmarkUpdater.UpdateBookmarksAsync(updateBookmarksRequest);
// Publish domain event.
await notificationSender.SendAsync(new WorkflowBookmarksIndexed(context, new IndexedWorkflowBookmarks(context.Id, diff.Added, diff.Removed, diff.Unchanged)), cancellationToken);
// Notify all interested activities that the bookmarks have been persisted.
var activityExecutionContexts = context.ActivityExecutionContexts.Where(x => x.Activity is IBookmarksPersistedHandler && x.Bookmarks.Any()).ToList();
foreach (var activityExecutionContext in activityExecutionContexts)
await ((IBookmarksPersistedHandler)activityExecutionContext.Activity).BookmarksPersistedAsync(activityExecutionContext);
await notificationSender.SendAsync(new WorkflowBookmarksIndexed(new IndexedWorkflowBookmarks(updateBookmarksRequest.WorkflowInstanceId, updateBookmarksRequest.Diff.Added, updateBookmarksRequest.Diff.Removed, updateBookmarksRequest.Diff.Unchanged)));
// Publish domain event.
await notificationSender.SendAsync(new WorkflowBookmarksPersisted(context, diff), NotificationStrategy.Background, cancellationToken);
await notificationSender.SendAsync(new WorkflowBookmarksPersisted(updateBookmarksRequest.Diff), NotificationStrategy.Background);
}
}

View file

@ -1,6 +1,7 @@
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
@ -32,6 +33,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
private readonly IWorkflowInstanceFactory _workflowInstanceFactory;
private readonly WorkflowStateMapper _workflowStateMapper;
private readonly IIdentityGenerator _identityGenerator;
private readonly IWorkflowExecutionContextStore _workflowExecutionContextStore;
private readonly IWorkflowStateExtractor _workflowStateExtractor;
private readonly IServiceProvider _serviceProvider;
private readonly IBookmarksPersister _bookmarksPersister;
/// <summary>
/// Constructor.
@ -47,7 +52,11 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
IDistributedLockProvider distributedLockProvider,
IWorkflowInstanceFactory workflowInstanceFactory,
WorkflowStateMapper workflowStateMapper,
IIdentityGenerator identityGenerator)
IIdentityGenerator identityGenerator,
IWorkflowExecutionContextStore workflowExecutionContextStore,
IWorkflowStateExtractor workflowStateExtractor,
IServiceProvider serviceProvider,
IBookmarksPersister bookmarksPersister)
{
_workflowHostFactory = workflowHostFactory;
_workflowDefinitionService = workflowDefinitionService;
@ -60,6 +69,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
_workflowInstanceFactory = workflowInstanceFactory;
_workflowStateMapper = workflowStateMapper;
_identityGenerator = identityGenerator;
_workflowExecutionContextStore = workflowExecutionContextStore;
_workflowStateExtractor = workflowStateExtractor;
_serviceProvider = serviceProvider;
_bookmarksPersister = bookmarksPersister;
}
/// <inheritdoc />
@ -90,6 +103,67 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
{
return await StartWorkflowAsync(definitionId, options);
}
/// <inheritdoc />
public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken)
{
var workflowExecutionContext = await _workflowExecutionContextStore.FindAsync(workflowInstanceId);
if (workflowExecutionContext is null)
{
// The execution context is not running on this instance.
// It might not be running on any instance, so check the db and update the record.
// Use lock to prevent race conditions and other instances from updating the workflow context
await using var cancelLock = await _distributedLockProvider.TryAcquireLockAsync($"{workflowInstanceId}-cancel");
if (cancelLock == null)
return;
var workflowInstance = await _workflowInstanceStore.FindAsync(workflowInstanceId, cancellationToken);
if (workflowInstance is null
|| workflowInstance.SubStatus == WorkflowSubStatus.Cancelled
|| workflowInstance.SubStatus == WorkflowSubStatus.Faulted)
return;
var workflowState = await ExportWorkflowStateAsync(workflowInstanceId, cancellationToken);
if (workflowState == null)
throw new Exception("Workflow state not found");
var workflowDefinition = await _workflowDefinitionService.FindAsync(workflowState.DefinitionId, VersionOptions.SpecificVersion(workflowState.DefinitionVersion), cancellationToken);
if (workflowDefinition == null)
throw new Exception("Workflow definition not found");
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_serviceProvider, workflow, workflowState, cancellationTokens: cancellationToken);
if (!cancellationToken.IsCancellationRequested)
await CancelWorkflowExecutionContextAsync();
return;
}
await using var mainCancelLock = await _distributedLockProvider.AcquireLockAsync($"{workflowInstanceId}-cancel", TimeSpan.FromMinutes(1));
await CancelWorkflowExecutionContextAsync();
async Task CancelWorkflowExecutionContextAsync()
{
var originalBookmarks = workflowExecutionContext.Bookmarks.ToList();
workflowExecutionContext.Cancel();
var newBookmarks = workflowExecutionContext.Bookmarks.ToList();
var diff = Diff.For(originalBookmarks, newBookmarks);
var bookmarkRequest = new UpdateBookmarksRequest(workflowExecutionContext.Id,
diff,
workflowExecutionContext.CorrelationId);
await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest);
var instance = await _workflowInstanceManager.SaveAsync(workflowExecutionContext);
await _workflowInstanceStore.SaveAsync(instance);
}
}
/// <inheritdoc />
public async Task<ICollection<WorkflowExecutionResult>> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions options)

View file

@ -0,0 +1,42 @@
using Elsa.Common.Services;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Stores;
namespace Elsa.Workflows.Runtime.Services;
/// <summary>
/// Stores <see cref="WorkflowExecutionContext"/> in memory.
/// </summary>
public class MemoryWorkflowExecutionContextStore : IWorkflowExecutionContextStore
{
private readonly MemoryStore<WorkflowExecutionContext> _store;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryActivityExecutionStore"/> class.
/// </summary>
public MemoryWorkflowExecutionContextStore(MemoryStore<WorkflowExecutionContext> store)
{
_store = store;
}
/// <inheritdoc />
public Task SaveAsync(WorkflowExecutionContext context)
{
_store.Save(context, x => x.Id);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<WorkflowExecutionContext?> FindAsync(string workflowExecutionContextId)
{
var result = _store.Find((context) => context.Id == workflowExecutionContextId);
return Task.FromResult(result);
}
/// <inheritdoc />
public Task DeleteAsync(string workflowExecutionContextId)
{
_store.Delete(workflowExecutionContextId);
return Task.CompletedTask;
}
}

View file

@ -80,6 +80,7 @@ public class WorkflowHost : IWorkflowHost
Input = input,
Properties = properties,
TriggerActivityId = options?.TriggerActivityId,
StatusUpdatedCallback = options?.StatusUpdatedCallback,
CancellationTokens = options?.CancellationTokens ?? cancellationToken
};

View file

@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.CSharp\Elsa.CSharp.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />

View file

@ -28,16 +28,20 @@ builder.Services.AddElsa(elsa =>
// Add services for HTTP activities and workflow middleware.
elsa.UseHttp();
// Use JavaScript.
elsa.UseJavaScript(javaScript =>
// Use C#.
elsa.UseCSharp(options =>
{
javaScript.AllowClrAccess = true;
javaScript.ConfigureEngine(options =>
{
options.RegisterType<OrderCompleted>();
options.RegisterType<OrderCreated>();
});
options.Assemblies.Add(typeof(OrderCreated).Assembly);
options.Namespaces.Add(typeof(OrderCreated).Namespace!);
});
// Use JavaScript.
elsa.UseJavaScript(options =>
{
options.AllowClrAccess = true;
options.RegisterType<OrderCreated>();
options.RegisterType<OrderCompleted>();
});
// Use C#.
@ -56,7 +60,7 @@ builder.Services.AddElsa(elsa =>
identity.UseAdminUserProvider();
identity.TokenOptions = options =>
{
options.SigningKey = "secret-token-signing-key";
options.SigningKey = "super-secret-and-securely-stored-token-signing-key";
options.AccessTokenLifetime = TimeSpan.FromDays(1);
};
});
@ -67,7 +71,7 @@ builder.Services.AddElsa(elsa =>
// Configure MassTransit.
elsa.UseMassTransit(massTransit =>
{
massTransit.UseRabbitMq(rabbitMqConnectionString);
//massTransit.UseRabbitMq(rabbitMqConnectionString);
massTransit.AddMessageType<OrderCompleted>();
massTransit.AddMessageType<OrderCreated>();
});

View file

@ -2,7 +2,6 @@ using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Samples.AspNet.Onboarding.WorkflowServer.Workflows;
using Elsa.Webhooks.Extensions;
var builder = WebApplication.CreateBuilder(args);

View file

@ -1,7 +1,6 @@
using System;
using Elsa.Extensions;
using Elsa.Samples.AspNet.Webhooks.WorkflowServer.Workflows;
using Elsa.Webhooks.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

View file

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="Proto.Persistence.Sqlite" Version="1.4.0" />
<PackageReference Include="xunit" Version="2.6.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -23,6 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.JavaScript\Elsa.JavaScript.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.ProtoActor\Elsa.ProtoActor.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Scheduling\Elsa.Scheduling.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.WorkflowProviders.BlobStorage\Elsa.WorkflowProviders.BlobStorage.csproj"/>
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj"/>

View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
using Elsa.Mediator.HostedServices;
using Elsa.Mediator.Options;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation;
public class DefaultRuntimeTests
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IWorkflowRuntime _workflowRuntime;
private readonly BackgroundCommandSenderHostedService _backgroundCommandSenderHostedService;
private readonly BackgroundEventPublisherHostedService _backgroundEventPublisherHostedService;
public DefaultRuntimeTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.AddWorkflow<BulkSuspendedWorkflow>()
.AddWorkflow<ResumeDispatchWorkflow>()
.AddWorkflow<SimpleChildWorkflow>()
.AddWorkflow<SimpleSuspendedWorkflow>()
.ConfigureServices(services =>
{
services
.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<MediatorOptions>>().Value;
return ActivatorUtilities.CreateInstance<BackgroundCommandSenderHostedService>(sp, options.CommandWorkerCount);
})
.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<MediatorOptions>>().Value;
return ActivatorUtilities.CreateInstance<BackgroundEventPublisherHostedService>(sp, options.NotificationWorkerCount);
})
;
})
.Build();
_backgroundCommandSenderHostedService = _services.GetRequiredService<BackgroundCommandSenderHostedService>();
_backgroundEventPublisherHostedService = _services.GetRequiredService<BackgroundEventPublisherHostedService>();
_workflowRuntime = _services.GetRequiredService<IWorkflowRuntime>();
}
[Fact(DisplayName = "Cancelling a suspended workflow")]
public async Task SuspendedCancelTest()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
const string workflowDefinitionId = nameof(SimpleSuspendedWorkflow);
var workflowState = await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions());
Assert.Equal(WorkflowStatus.Running, workflowState.Status);
Assert.Equal(WorkflowSubStatus.Suspended, workflowState.SubStatus);
await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId);
var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId);
Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status);
Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus);
Assert.Empty(_capturingTextWriter.Lines);
}
[Fact(DisplayName = "Cancelling a running workflow",
Skip= "Unpredictable result, need to create a dispatcher for tests that will run outside of the unit test")]
public async Task RunningCancelTest()
{
await _backgroundCommandSenderHostedService.StartAsync(CancellationToken.None);
await _backgroundEventPublisherHostedService.StartAsync(CancellationToken.None);
// Populate registries.
await _services.PopulateRegistriesAsync();
const string workflowDefinitionId = nameof(BulkSuspendedWorkflow);
var workflowState = await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions());
var bookmarks = new Stack<Bookmark>(workflowState.Bookmarks);
var resumeOptions = new ResumeWorkflowRuntimeOptions { BookmarkId = bookmarks.Pop().Id };
var state = await _workflowRuntime.ResumeWorkflowAsync(workflowState.WorkflowInstanceId,resumeOptions);
await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId);
var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId);
Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status);
Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus);
Assert.NotEmpty(_capturingTextWriter.Lines);
}
}

View file

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Extensions;
using Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
using Elsa.Mediator.HostedServices;
using Elsa.Mediator.Options;
using Elsa.ProtoActor.HostedServices;
using Elsa.Testing.Shared;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Options;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Proto.Persistence.Sqlite;
using Xunit;
using Xunit.Abstractions;
using WorkflowStatus = Elsa.Workflows.WorkflowStatus;
using WorkflowSubStatus = Elsa.Workflows.WorkflowSubStatus;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation;
public class ProtoActorTests
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IWorkflowRuntime _workflowRuntime;
private readonly WorkflowServerHost _workflowServerHost;
private readonly BackgroundCommandSenderHostedService _backgroundCommandSenderHostedService;
private readonly BackgroundEventPublisherHostedService _backgroundEventPublisherHostedService;
public ProtoActorTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.AddWorkflow<BulkSuspendedWorkflow>()
.AddWorkflow<ResumeDispatchWorkflow>()
.AddWorkflow<SimpleChildWorkflow>()
.AddWorkflow<SimpleSuspendedWorkflow>()
.ConfigureServices(services =>
{
services
.AddSingleton(sp => ActivatorUtilities.CreateInstance<WorkflowServerHost>(sp));
services
.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<MediatorOptions>>().Value;
return ActivatorUtilities.CreateInstance<BackgroundCommandSenderHostedService>(sp,
options.CommandWorkerCount);
})
.AddSingleton(sp =>
{
var options = sp.GetRequiredService<IOptions<MediatorOptions>>().Value;
return ActivatorUtilities.CreateInstance<BackgroundEventPublisherHostedService>(sp,
options.NotificationWorkerCount);
});
}).ConfigureElsa(elsa => elsa.UseWorkflowRuntime(runtime => runtime.UseProtoActor(protoActor =>
{
protoActor.PersistenceProvider = _ =>
new SqliteProvider(
new SqliteConnectionStringBuilder("Data Source=elsa.sqlite.db;Cache=Shared;"));
}
)))
.Build();
_backgroundCommandSenderHostedService = _services.GetRequiredService<BackgroundCommandSenderHostedService>();
_backgroundEventPublisherHostedService = _services.GetRequiredService<BackgroundEventPublisherHostedService>();
_workflowServerHost = _services.GetRequiredService<WorkflowServerHost>();
_workflowRuntime = _services.GetRequiredService<IWorkflowRuntime>();
}
[Fact(DisplayName = "Cancelling a suspended workflow")]
public async Task SuspendedCancelTest()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
await _workflowServerHost.StartAsync(default);
const string workflowDefinitionId = nameof(SimpleSuspendedWorkflow);
var workflowState =
await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions());
Assert.Equal(WorkflowStatus.Running, workflowState.Status);
Assert.Equal(WorkflowSubStatus.Suspended, workflowState.SubStatus);
await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId);
var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId);
Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status);
Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus);
Assert.Empty(_capturingTextWriter.Lines);
}
[Fact(DisplayName = "Cancelling a running workflow",
Skip= "Unpredictable result, need to create a dispatcher for tests that will run outside of the unit test")]
public async Task RunningCancelTest()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
await _workflowServerHost.StartAsync(default);
await _backgroundCommandSenderHostedService.StartAsync(CancellationToken.None);
await _backgroundEventPublisherHostedService.StartAsync(CancellationToken.None);
const string workflowDefinitionId = nameof(BulkSuspendedWorkflow);
var workflowState =
await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions());
var bookmarks = new Stack<Bookmark>(workflowState.Bookmarks);
var resumeOptions = new ResumeWorkflowRuntimeOptions { BookmarkId = bookmarks.Pop().Id };
await _workflowRuntime.ResumeWorkflowAsync(workflowState.WorkflowInstanceId, resumeOptions);
await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId);
var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId);
Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status);
Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus);
Assert.NotEmpty(_capturingTextWriter.Lines);
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
public class BulkSuspendedWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
object[] items = Enumerable.Range(0,1500).Select(x => (object) x).ToArray();
builder.Root = new Sequence
{
Activities =
{
new Start(),
new Delay(TimeSpan.FromSeconds(10)),
new BulkDispatchWorkflows
{
WorkflowDefinitionId = new Input<string>(nameof(SimpleChildWorkflow)),
Items = new Input<ICollection<object>>(items)
}
},
};
}
}

View file

@ -0,0 +1,21 @@
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
public class ResumeDispatchWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
{
new PublishEvent { EventName = new Input<string>("ResumeBlockDispatch") }
}
};
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
public class SimpleChildWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
{
new WriteLine("Executed SimpleChildWorkflow")
}
};
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows;
public class SimpleSuspendedWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
{
new Start(),
new Event("BlockingEvent"),
new WriteLine("Workflow was not properly blocked")
},
};
}
}