Merge remote-tracking branch 'origin/blueberry' into enh/multi-tenancy-refactorings

This commit is contained in:
Sipke Schoorstra 2024-10-11 19:45:40 +02:00
commit ff487fad80
100 changed files with 1338 additions and 290 deletions

View file

@ -3,18 +3,7 @@ on:
workflow_dispatch:
push:
branches:
- 'main'
- 'feature/*'
- 'feat/*'
- 'issue/*'
- 'bug/*'
- 'enhancement/*'
- 'enh/*'
- 'patch/*'
- 'fix/*'
- 'perf/*'
- 'hotfix/*'
- 'chore/*'
- 'blueberry'
release:
types: [ prereleased, published ]
env:

View file

@ -38,5 +38,6 @@
</PropertyGroup>
<PropertyGroup>
<ElsaStudioVersion>3.3.0-preview.470</ElsaStudioVersion>
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
</PropertyGroup>
</Project>

View file

@ -38,7 +38,8 @@
<PackageVersion Include="Fody" Version="6.8.1" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Google.Protobuf" Version="3.27.2" />
<PackageVersion Include="Grpc.Tools" Version="2.64.0" />
<PackageVersion Include="Grpc.Net.Client" Version="2.66.0" />
<PackageVersion Include="Grpc.Tools" Version="2.66.0" />
<PackageVersion Include="Hangfire" Version="1.8.14" />
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1" />
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2" />
@ -67,7 +68,6 @@
<PackageVersion Include="NuGet.Protocol" Version="6.10.1" />
<PackageVersion Include="Nuke.Components" Version="8.0.0" />
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
<PackageVersion Include="Proto.Actor" Version="1.6.0" />
@ -149,40 +149,34 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.8" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.10" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Npgsql" Version="8.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.23.50" />
<PackageVersion Include="Polly" Version="8.4.1" />
<PackageVersion Include="Polly" Version="8.4.2" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.7.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.0.0-rc9.15" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
</ItemGroup>
</Project>

View file

@ -117,6 +117,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54
docker\docker-compose.yml = docker\docker-compose.yml
docker\docker-compose-datadog.yml = docker\docker-compose-datadog.yml
docker\ElsaServer-Datadog.Dockerfile = docker\ElsaServer-Datadog.Dockerfile
docker\otel-collector-config.yaml = docker\otel-collector-config.yaml
docker\init-db.sh = docker\init-db.sh
EndProjectSection
EndProject
@ -470,6 +471,45 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Secrets.Scripting", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.JavaScript.Libraries", "src\modules\Elsa.JavaScript.Libraries\Elsa.JavaScript.Libraries.csproj", "{5F80663C-B072-49DE-9A67-FD804BAD787E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.OpenTelemetry", "src\modules\Elsa.OpenTelemetry\Elsa.OpenTelemetry.csproj", "{25880971-403E-4872-93A1-D33089E07C91}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{FE1AFEC0-7C63-4EF8-8E24-D9703590A778}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k8s", "k8s", "{924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "elsa-server", "elsa-server", "{A2743EC6-7117-46E2-8AC1-8CDD52C8AAEF}"
ProjectSection(SolutionItems) = preProject
scripts\k8s\elsa-server\deployment.yaml = scripts\k8s\elsa-server\deployment.yaml
scripts\k8s\elsa-server\role-binding.yaml = scripts\k8s\elsa-server\role-binding.yaml
scripts\k8s\elsa-server\role.yaml = scripts\k8s\elsa-server\role.yaml
scripts\k8s\elsa-server\service-account.yml = scripts\k8s\elsa-server\service-account.yml
scripts\k8s\elsa-server\service.yaml = scripts\k8s\elsa-server\service.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "elsa-studio", "elsa-studio", "{A782913B-142F-4EA8-A2C7-82679CD2ABC1}"
ProjectSection(SolutionItems) = preProject
scripts\k8s\elsa-studio\deployment.yaml = scripts\k8s\elsa-studio\deployment.yaml
scripts\k8s\elsa-studio\service.yaml = scripts\k8s\elsa-studio\service.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plant-uml", "plant-uml", "{BFB87B50-3FE9-4AB1-8F18-96BE667C953D}"
ProjectSection(SolutionItems) = preProject
scripts\k8s\plant-uml\deployment.yaml = scripts\k8s\plant-uml\deployment.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postgres", "postgres", "{399C61BC-A51B-4B57-AA32-46BC24D4AE1C}"
ProjectSection(SolutionItems) = preProject
scripts\k8s\postgres\config-map.yaml = scripts\k8s\postgres\config-map.yaml
scripts\k8s\postgres\deployment.yaml = scripts\k8s\postgres\deployment.yaml
scripts\k8s\postgres\service.yml = scripts\k8s\postgres\service.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trace-lens", "trace-lens", "{2FDF67FD-2BD3-4F73-9A24-DA255641ED25}"
ProjectSection(SolutionItems) = preProject
scripts\k8s\trace-lens\deployment.yaml = scripts\k8s\trace-lens\deployment.yaml
scripts\k8s\trace-lens\service.yaml = scripts\k8s\trace-lens\service.yaml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1070,6 +1110,10 @@ Global
{5F80663C-B072-49DE-9A67-FD804BAD787E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F80663C-B072-49DE-9A67-FD804BAD787E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F80663C-B072-49DE-9A67-FD804BAD787E}.Release|Any CPU.Build.0 = Release|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25880971-403E-4872-93A1-D33089E07C91}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1257,6 +1301,13 @@ Global
{29D12ADC-55E9-40D0-9E4C-F0EBB6E098EC} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
{6C606FEB-9A1F-4816-ABE4-22AFA8CEE771} = {8CEEC194-820A-4C8D-AB9E-E51E6D3E9CC1}
{5F80663C-B072-49DE-9A67-FD804BAD787E} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76}
{25880971-403E-4872-93A1-D33089E07C91} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{924BA0AE-46B1-40B5-992C-CFB4EB14EDB0} = {FE1AFEC0-7C63-4EF8-8E24-D9703590A778}
{A2743EC6-7117-46E2-8AC1-8CDD52C8AAEF} = {924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}
{A782913B-142F-4EA8-A2C7-82679CD2ABC1} = {924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}
{BFB87B50-3FE9-4AB1-8F18-96BE667C953D} = {924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}
{399C61BC-A51B-4B57-AA32-46BC24D4AE1C} = {924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}
{2FDF67FD-2BD3-4F73-9A24-DA255641ED25} = {924BA0AE-46B1-40B5-992C-CFB4EB14EDB0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}

View file

@ -1,4 +1,4 @@
<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">
<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:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INITIALIZER_ARRANGEMENT/@EntryValue">False</s:Boolean>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ARRAY_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">1000</s:Int64>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">1</s:Int64>
@ -16,6 +16,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=initializable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=materializer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=materializers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=persistable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=otlp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Persister/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Populator/@EntryIndexedValue">True</s:Boolean>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,28 +1,23 @@
# Version: 1
# Version: 1
# Description: Dockerfile for building and running Elsa Server
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build
WORKDIR /source
# Determine the architecture and download the appropriate version of the tracer
RUN ARCH=$(if [ "$(uname -m)" = "x86_64" ]; then echo "amd64"; elif [ "$(uname -m)" = "aarch64" ]; then echo "arm64"; else echo "amd64"; fi) \
&& TRACER_VERSION=$(curl -s https://api.github.com/repos/DataDog/dd-trace-dotnet/releases/latest | grep tag_name | cut -d '"' -f 4 | cut -c2-) \
&& curl -Lo /tmp/datadog-dotnet-apm.deb https://github.com/DataDog/dd-trace-dotnet/releases/download/v${TRACER_VERSION}/datadog-dotnet-apm_${TRACER_VERSION}_${ARCH}.deb
# copy sources.
# Copy sources.
COPY src/. ./src
COPY ./NuGet.Config ./
COPY *.props ./
# restore packages.
RUN dotnet restore "./src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj"
# Restore packages.
RUN dotnet restore "./src/bundles/Elsa.Server.Web/Elsa.Server.Web.csproj"
# build and publish (UseAppHost=false creates platform independent binaries).
WORKDIR /source/src/apps/Elsa.Server.Web
# Build and publish (UseAppHost=false creates platform independent binaries).
WORKDIR /source/src/bundles/Elsa.Server.Web
RUN dotnet build "Elsa.Server.Web.csproj" -c Release -o /app/build
RUN dotnet publish "Elsa.Server.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore -f net8.0
# move binaries into smaller base image.
# Move binaries into smaller base image.
FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim AS base
WORKDIR /app
COPY --from=build /app/publish ./
@ -38,21 +33,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Set PYTHONNET_PYDLL environment variable
ENV PYTHONNET_PYDLL=/usr/lib/aarch64-linux-gnu/libpython3.11.so
# Copy the tracer from build target
COPY --from=build /tmp/datadog-dotnet-apm.deb /tmp/datadog-dotnet-apm.deb
# Install the tracer
RUN mkdir -p /opt/datadog \
&& mkdir -p /var/log/datadog \
&& dpkg -i /tmp/datadog-dotnet-apm.deb \
&& rm /tmp/datadog-dotnet-apm.deb
# Enable the tracer
ENV CORECLR_ENABLE_PROFILING=1
ENV CORECLR_PROFILER={846F5F1C-F9AE-4B07-969E-05C26BC060D8}
ENV CORECLR_PROFILER_PATH=/opt/datadog/Datadog.Trace.ClrProfiler.Native.so
ENV DD_DOTNET_TRACER_HOME=/opt/datadog
ENV DD_INTEGRATIONS=/opt/datadog/integrations.json
# Install dependencies
RUN apt-get update && apt-get install -y wget unzip curl
# Set environment variables for OpenTelemetry Auto-Instrumentation
ENV OTEL_DOTNET_AUTO_HOME=/otel
ENV OTEL_LOG_LEVEL="debug"
# Download and extract OpenTelemetry Auto-Instrumentation
ARG OTEL_VERSION=1.7.0
RUN mkdir /otel
RUN curl -L -o /otel/otel-dotnet-install.sh https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh
RUN chmod +x /otel/otel-dotnet-install.sh
RUN /bin/bash /otel/otel-dotnet-install.sh
# Provide necessary permissions for the script to execute
RUN chmod +x /otel/instrument.sh
EXPOSE 8080/tcp
EXPOSE 443/tcp
ENTRYPOINT ["dotnet", "Elsa.Server.Web.dll"]
# Instrument the application and start it
ENTRYPOINT ["/bin/bash", "-c", "source /otel/instrument.sh && dotnet Elsa.Server.Web.dll"]

View file

@ -1,5 +1,3 @@
version: '3.7'
services:
postgres:
image: postgres:latest
@ -12,24 +10,13 @@ services:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
cockroachdb:
image: cockroachdb/cockroach:v22.1.0
command: start-single-node --insecure
ports:
- "26257:26257" # CockroachDB SQL port
- "8080:8080" # CockroachDB UI port
volumes:
- cockroachdb-data:/cockroach/cockroach-data
environment:
- COCKROACH_DATABASE=elsa
rabbitmq:
image: "rabbitmq:3-management"
ports:
- "15672:15672"
- "5672:5672"
redis:
image: redis:latest
ports:
@ -44,7 +31,7 @@ services:
- postgres
- rabbitmq
- redis
- datadog-agent
- otel-collector
environment:
DD_AGENT_HOST: datadog-agent
DD_ENV: development
@ -52,6 +39,21 @@ services:
DD_TRACE_OTEL_ENABLED: true
DD_SERVICE: "Elsa Server"
DD_VERSION: "3.3.0"
# OpenTelemetry environment variables
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317" # Point to OpenTelemetry Collector
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc" # Use gRPC for OTLP
OTEL_TRACES_EXPORTER: "otlp"
OTEL_METRICS_EXPORTER: "otlp"
OTEL_LOGS_EXPORTER: "otpl"
OTEL_RESOURCE_ATTRIBUTES: "service.name=elsa-server-local,service.version=3.2.1-blueberry,deployment.environment=development"
OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES: "Elsa.Workflows"
OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED: "true"
OTEL_LOG_LEVEL: "debug"
OTEL_DOTNET_AUTO_RESOURCE_DETECTOR_ENABLED: "true"
OTEL_DOTNET_AUTO_LOGS_CONSOLE_EXPORTER_ENABLED: "true"
OTEL_DOTNET_AUTO_METRICS_CONSOLE_EXPORTER_ENABLED: "true"
OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED: "true"
ASPNETCORE_ENVIRONMENT: Development
PYTHONNET_PYDLL: /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11
PYTHONNET_RUNTIME: coreclr
@ -61,7 +63,7 @@ services:
DISTRIBUTEDLOCKPROVIDER: "Postgres"
ports:
- "13000:8080"
elsa-studio:
pull_policy: always
build:
@ -73,24 +75,41 @@ services:
ports:
- "14000:8080"
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
command: [ "--config", "/etc/otel-collector-config.yaml", "--feature-gates", "-component.UseLocalHostAsDefaultHost" ]
environment:
DD_API_KEY: "secret api key"
DD_SITE: "datadoghq.eu"
ports:
- "13133:13133"
- "4317:4317"
- "4318:4318"
datadog-agent:
image: datadog/agent:7
image: datadog/agent:latest
environment:
DD_API_KEY: "<HIDDEN>"
DD_SITE: "datadoghq.eu"
DD_HOSTNAME: "otel-collector"
DD_LOGS_ENABLED: "true"
DD_OTLP_CONFIG_LOGS_ENABLED: "true"
DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL: "true"
DD_APM_ENABLED: "true"
DD_APM_NON_LOCAL_TRAFFIC: "true"
DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT: 0.0.0.0:4317 # The Datadog Agent expects traces from OpenTelemetry Collector
DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT: 0.0.0.0:4318
# Service autodiscovery
DD_AC_INCLUDE: "name:postgres,name:rabbitmq,name:redis,name:elsa-server"
DD_AC_EXCLUDE: "name:datadog-agent"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /proc/:/host/proc/:ro
- /sys/fs/cgroup/:/host/sys/fs/cgroup:ro
ports:
- "8126:8126"
- "14317:4317"
- "14318:4318"
volumes:
postgres-data:
cockroachdb-data:
postgres-data:

View file

@ -1,6 +1,4 @@
version: '3.7'
services:
services:
postgres:
image: postgres:latest
command: -c 'max_connections=2000'

View file

@ -0,0 +1,49 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_max_size: 100
send_batch_size: 10 # Increased batch size for efficiency
timeout: 1s
tail_sampling:
decision_wait: 10s
num_traces: 10000 # Increased from 100 to handle more traces
expected_new_traces_per_sec: 100 # Increased from 10
decision_cache:
sampled_cache_size: 100000
policies: [
{
name: incidents-policy,
type: boolean_attribute,
boolean_attribute: { key: hasIncidents, value: true }
}
]
exporters:
debug:
verbosity: detailed
datadog:
api:
site: ${env:DD_SITE}
key: ${env:DD_API_KEY}
service:
pipelines:
metrics:
receivers: [ otlp ]
processors: [ batch ]
exporters: [ datadog ]
traces:
receivers: [ otlp ]
processors: [ batch ] # Added tail_sampling to the main traces pipeline
exporters: [ debug, datadog ] # Directly exporting to debug and datadog
logs:
receivers: [ otlp ]
processors: [ batch ]
exporters: [ debug, datadog ]

View file

@ -51,6 +51,7 @@
<PackageReference Include="DistributedLock.Redis"/>
<PackageReference Include="FluentStorage.Azure.Blobs"/>
<PackageReference Include="Proto.Cluster.Kubernetes" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Proto.Persistence.Sqlite"/>
<PackageReference Include="Proto.Persistence.SqlServer"/>
<PackageReference Include="OpenTelemetry.AutoInstrumentation"/>
@ -59,6 +60,10 @@
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
</ItemGroup>

View file

@ -373,6 +373,9 @@ services
elsa.UseRealTimeWorkflows();
}
if (useAzureServiceBus)
elsa.UseAzureServiceBus(asb => asb.AzureServiceBusOptions += options => configuration.GetSection("AzureServiceBus").Bind(options));
if (useMassTransit)
{
elsa.UseMassTransit(massTransit =>

View file

@ -48,4 +48,9 @@
<PackageReference Include="Elsa.Studio.Login.BlazorWasm"/>
</ItemGroup>
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -1,14 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Elsa.Mediator": "Warning",
"MassTransit": "Warning",
"Microsoft.Extensions.Http": "Warning",
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning",
"System.Net.Http": "Warning"
"OpenTelemetry": "Debug"
}
},
"AllowedHosts": "*",
@ -115,5 +111,27 @@
}
]
}
},
"Scripting": {
"Python": {
"PythonDllPath": "",
"Scripts": [
"def greet(name): return f'Hello {name}!'",
"def say_hello_world(): return greet('World')"
]
}
},
"AzureServiceBus": {
"ConnectionStringOrName": "AzureServiceBus",
"Queues": [
{
"name": "order-created"
},
{
"name": "order-completed"
}
],
"Topics": [],
"Subscriptions": []
}
}

View file

@ -16,6 +16,10 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>

View file

@ -0,0 +1,56 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Api.Client.Extensions;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Converters;
/// <summary>
/// Converts <see cref="Type"/> objects to and from their assembly-qualified name.
/// </summary>
[UsedImplicitly]
public class TypeJsonConverter : JsonConverter<Type>
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(Type) || typeToConvert.FullName == "System.RuntimeType";
}
/// <inheritdoc />
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var typeName = reader.GetString()!;
// Handle collection types.
if (typeName.EndsWith("[]"))
{
var elementTypeName = typeName[..^"[]".Length];
var elementType = Type.GetType(elementTypeName)!;
return typeof(List<>).MakeGenericType(elementType);
}
return Type.GetType(typeName);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
{
// Handle collection types.
if (value is { IsGenericType: true, GenericTypeArguments.Length: 1 })
{
var elementType = value.GenericTypeArguments.First();
var typedEnumerable = typeof(IEnumerable<>).MakeGenericType(elementType);
if (typedEnumerable.IsAssignableFrom(value))
{
var elementTypeName = value.GetSimpleAssemblyQualifiedName();
JsonSerializer.Serialize(writer, $"{elementTypeName}[]", options);
return;
}
}
var typeName = value.GetSimpleAssemblyQualifiedName();
JsonSerializer.Serialize(writer, typeName, options);
}
}

View file

@ -19,7 +19,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -121,7 +121,7 @@ public static class DependencyInjectionExtensions
/// <param name="httpClientBuilderOptions">An options object that can be used to configure the HTTP client builder.</param>
public static IServiceCollection AddApi(this IServiceCollection services, Type apiType, ElsaClientBuilderOptions? httpClientBuilderOptions = default)
{
var builder = services.AddRefitClient(apiType, _ => CreateRefitSettings(), apiType.Name).ConfigureHttpClient(ConfigureElsaApiHttpClient);
var builder = services.AddRefitClient(apiType, sp => CreateRefitSettings(sp, httpClientBuilderOptions?.ConfigureJsonSerializerOptions), apiType.Name).ConfigureHttpClient(ConfigureElsaApiHttpClient);
httpClientBuilderOptions?.ConfigureHttpClientBuilder(builder);
httpClientBuilderOptions?.ConfigureRetryPolicy?.Invoke(builder);
return services;
@ -136,7 +136,7 @@ public static class DependencyInjectionExtensions
public static void AddApiWithoutRetryPolicy<T>(this IServiceCollection services, ElsaClientBuilderOptions? httpClientBuilderOptions = default) where T : class
{
var builder = services
.AddRefitClient<T>(_ => CreateRefitSettings(), typeof(T).Name)
.AddRefitClient<T>(sp => CreateRefitSettings(sp), typeof(T).Name)
.ConfigureHttpClient(ConfigureElsaApiHttpClient);
httpClientBuilderOptions?.ConfigureHttpClientBuilder(builder);
}
@ -153,7 +153,7 @@ public static class DependencyInjectionExtensions
/// Creates an API client for the specified API type.
public static T CreateApi<T>(this IServiceProvider serviceProvider, HttpClient httpClient) where T : class
{
return RestService.For<T>(httpClient, CreateRefitSettings());
return RestService.For<T>(httpClient, CreateRefitSettings(serviceProvider));
}
private static void ConfigureElsaApiHttpClient(IServiceProvider serviceProvider, HttpClient httpClient)

View file

@ -10,17 +10,12 @@ namespace Elsa.Api.Client;
/// </summary>
public static class RefitSettingsHelper
{
private static JsonSerializerOptions? _jsonSerializerOptions;
/// <summary>
/// Creates a <see cref="RefitSettings"/> instance configured for Elsa.
/// </summary>
public static RefitSettings CreateRefitSettings()
public static RefitSettings CreateRefitSettings(IServiceProvider serviceProvider, Action<IServiceProvider, JsonSerializerOptions>? configureJsonSerializerOptions = null)
{
var settings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(CreateJsonSerializerOptions())
};
var settings = new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(CreateJsonSerializerOptions(serviceProvider, configureJsonSerializerOptions)) };
return settings;
}
@ -28,11 +23,8 @@ public static class RefitSettingsHelper
/// <summary>
/// Creates a <see cref="JsonSerializerOptions"/> instance configured for Elsa.
/// </summary>
public static JsonSerializerOptions CreateJsonSerializerOptions()
public static JsonSerializerOptions CreateJsonSerializerOptions(IServiceProvider serviceProvider, Action<IServiceProvider, JsonSerializerOptions>? configureJsonSerializerOptions = null)
{
if (_jsonSerializerOptions != null)
return _jsonSerializerOptions;
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@ -40,7 +32,10 @@ public static class RefitSettingsHelper
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new VersionOptionsJsonConverter());
options.Converters.Add(new TypeJsonConverter());
return _jsonSerializerOptions = options;
configureJsonSerializerOptions?.Invoke(serviceProvider, options);
return options;
}
}

View file

@ -1,3 +1,4 @@
using System.Text.Json;
using Elsa.Api.Client.HttpMessageHandlers;
using Microsoft.Extensions.DependencyInjection;
using Polly;
@ -39,4 +40,9 @@ public class ElsaClientBuilderOptions
/// Gets or sets a delegate that can be used to configure the retry policy.
/// </summary>
public Action<IHttpClientBuilder>? ConfigureRetryPolicy { get; set; } = builder => builder.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
/// <summary>
/// Gets or sets a delegate that can be used to configure the JSON serializer options.
/// </summary>
public Action<IServiceProvider, JsonSerializerOptions>? ConfigureJsonSerializerOptions { get; set; }
}

View file

@ -27,4 +27,22 @@ public interface IActivityExecutionsApi
/// <returns>The response containing a list of activity executions.</returns>
[Get("/activity-executions/list")]
Task<ListResponse<ActivityExecutionRecord>> ListAsync(ListActivityExecutionsRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Lists activity execution summaries for a given activity in a workflow instance.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The response containing a list of activity execution summaries.</returns>
[Get("/activity-execution-summaries/list")]
Task<ListResponse<ActivityExecutionRecordSummary>> ListSummariesAsync(ListActivityExecutionsRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a single activity execution by ID.
/// </summary>
/// <param name="id">The ID of the activity execution.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The activity execution.</returns>
[Get("/activity-executions/{id}")]
Task<ActivityExecutionRecord?> GetAsync(string id, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,82 @@
using System.Linq.Expressions;
using Elsa.Api.Client.Resources.WorkflowInstances.Models;
using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Resources.ActivityExecutions.Models;
/// <summary>
/// Represents a summarized view of a single activity execution of an activity instance.
/// </summary>
public class ActivityExecutionRecordSummary : Entity
{
/// <summary>
/// Gets or sets the workflow instance ID.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// Gets or sets the activity ID.
/// </summary>
public string ActivityId { get; set; } = default!;
/// <summary>
/// Gets or sets the activity node ID.
/// </summary>
public string ActivityNodeId { get; set; } = default!;
/// <summary>
/// The type of the activity.
/// </summary>
public string ActivityType { get; set; } = default!;
/// <summary>
/// The version of the activity type.
/// </summary>
public int ActivityTypeVersion { get; set; }
/// <summary>
/// The name of the activity.
/// </summary>
public string? ActivityName { get; set; }
/// <summary>
/// Gets or sets the time at which the activity execution began.
/// </summary>
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// Gets or sets whether the activity has any bookmarks.
/// </summary>
public bool HasBookmarks { get; set; }
/// <summary>
/// Gets or sets the status of the activity.
/// </summary>
public ActivityStatus Status { get; set; }
/// <summary>
/// Gets or sets the time at which the activity execution completed.
/// </summary>
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>
/// Returns a summary view of the specified <see cref="ActivityExecutionRecord"/>.
/// </summary>
public static ActivityExecutionRecordSummary FromRecord(ActivityExecutionRecord record)
{
return new ActivityExecutionRecordSummary
{
Id = record.Id,
WorkflowInstanceId = record.WorkflowInstanceId,
ActivityId = record.ActivityId,
ActivityNodeId = record.ActivityNodeId,
ActivityType = record.ActivityType,
ActivityTypeVersion = record.ActivityTypeVersion,
ActivityName = record.ActivityName,
StartedAt = record.StartedAt,
HasBookmarks = record.HasBookmarks,
Status = record.Status,
CompletedAt = record.CompletedAt
};
}
}

View file

@ -5,4 +5,4 @@ namespace Elsa.Api.Client.Resources.StorageDrivers.Models;
/// </summary>
/// <param name="TypeName">The type name of the storage driver.</param>
/// <param name="DisplayName">The display name of the storage driver.</param>
public record StorageDriverDescriptor(string TypeName, string DisplayName);
public record StorageDriverDescriptor(string TypeName, string DisplayName, double Priority = 0, bool Deprecated = false);

View file

@ -16,4 +16,6 @@ public record PagedListResponse<T>: LinkedResource
public ICollection<T> Items { get; set; } = default!;
public long TotalCount { get; set; }
public static PagedListResponse<T> From(Page<T> page) => new(page);
}

View file

@ -5,10 +5,10 @@ namespace Elsa.Testing.Shared.Extensions;
public static class HttpResponseMessageExtensions
{
public static async Task<T> ReadAsJsonAsync<T>(this HttpResponseMessage response, CancellationToken cancellationToken = default)
public static async Task<T> ReadAsJsonAsync<T>(this HttpResponseMessage response, IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
{
var json = await response.Content.ReadAsStringAsync(cancellationToken);
var options = RefitSettingsHelper.CreateJsonSerializerOptions();
var options = RefitSettingsHelper.CreateJsonSerializerOptions(serviceProvider);
return JsonSerializer.Deserialize<T>(json, options)!;
}
}

View file

@ -70,9 +70,8 @@ public class AzureServiceBusFeature : FeatureBase
.AddSingleton(ServiceBusAdministrationClientFactory)
.AddSingleton(ServiceBusClientFactory)
.AddSingleton<ConfigurationQueueTopicAndSubscriptionProvider>()
.AddSingleton<IServiceBusInitializer, ServiceBusInitializer>()
.AddSingleton<IWorkerManager, WorkerManager>()
;
.AddScoped<IServiceBusInitializer, ServiceBusInitializer>();
// Definition providers.
Services

View file

@ -1,4 +1,6 @@
using Elsa.AzureServiceBus.Contracts;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.AzureServiceBus.HostedServices;
@ -6,16 +8,16 @@ namespace Elsa.AzureServiceBus.HostedServices;
/// <summary>
/// A blocking hosted service that creates queues, topics and subscriptions.
/// </summary>
public class CreateQueuesTopicsAndSubscriptions : IHostedService
[UsedImplicitly]
public class CreateQueuesTopicsAndSubscriptions(IServiceScopeFactory scopeFactory) : IHostedService
{
private readonly IServiceBusInitializer _serviceBusInitializer;
/// <summary>
/// Constructor.
/// </summary>
public CreateQueuesTopicsAndSubscriptions(IServiceBusInitializer serviceBusInitializer) => _serviceBusInitializer = serviceBusInitializer;
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken) => _serviceBusInitializer.InitializeAsync(cancellationToken);
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = scopeFactory.CreateAsyncScope();
var initializer = scope.ServiceProvider.GetRequiredService<IServiceBusInitializer>();
await initializer.InitializeAsync(cancellationToken);
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

View file

@ -46,6 +46,6 @@ public class StartWorkers(IServiceScopeFactory scopeFactory) : IHostedService
private async Task EnsureWorkersAsync(IWorkerManager workerManager, IEnumerable<MessageReceivedStimulus> stimuli, CancellationToken cancellationToken)
{
foreach (var payload in stimuli) await workerManager.StartWorkerAsync(payload.QueueOrTopic, payload.Subscription, cancellationToken);
foreach (var stimulus in stimuli) await workerManager.StartWorkerAsync(stimulus.QueueOrTopic, stimulus.Subscription, cancellationToken);
}
}

View file

@ -101,7 +101,7 @@ internal class DapperActivityExecutionRecordStore(Store<ActivityExecutionRecordR
}
}
private async ValueTask<ActivityExecutionRecordRecord> Map(ActivityExecutionRecord source, CancellationToken cancellationToken)
private ActivityExecutionRecordRecord Map(ActivityExecutionRecord source, CancellationToken cancellationToken)
{
return new ActivityExecutionRecordRecord
{
@ -125,7 +125,7 @@ internal class DapperActivityExecutionRecordStore(Store<ActivityExecutionRecordR
};
}
private async ValueTask<ActivityExecutionRecord> MapAsync(ActivityExecutionRecordRecord source, CancellationToken cancellationToken)
private ActivityExecutionRecord Map(ActivityExecutionRecordRecord source, CancellationToken cancellationToken)
{
return new ActivityExecutionRecord
{

View file

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
@ -20,7 +20,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>

View file

@ -12,6 +12,10 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
</ItemGroup>

View file

@ -12,6 +12,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
</ItemGroup>

View file

@ -18,9 +18,10 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="System.Formats.Asn1" />
<PackageReference Include="Azure.Identity" VersionOverride="1.11.4" />
<PackageReference Include="Microsoft.Identity.Client" VersionOverride="4.61.3" />
<PackageReference Include="System.Formats.Asn1" VersionOverride="8.0.1" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -13,6 +13,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
</ItemGroup>

View file

@ -13,7 +13,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>

View file

@ -84,12 +84,12 @@ public class EFCoreActivityExecutionStore(
{
entity = entity.SanitizeLogMessage();
var compressionAlgorithm = options.Value.CompressionAlgorithm ?? nameof(None);
var serializedActivityState = entity.ActivityState != null ? await safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : null;
var serializedActivityState = entity.ActivityState != null ? safeSerializer.Serialize(entity.ActivityState, cancellationToken) : null;
var compressedSerializedActivityState = serializedActivityState != null ? await compressionCodecResolver.Resolve(compressionAlgorithm).CompressAsync(serializedActivityState, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = compressedSerializedActivityState;
dbContext.Entry(entity).Property("SerializedActivityStateCompressionAlgorithm").CurrentValue = compressionAlgorithm;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs?.Any() == true ? await safeSerializer.SerializeAsync(entity.Outputs, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs?.Any() == true ? safeSerializer.Serialize(entity.Outputs, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedProperties").CurrentValue = entity.Properties.Any() ? payloadSerializer.Serialize(entity.Properties) : null;
dbContext.Entry(entity).Property("SerializedException").CurrentValue = entity.Exception != null ? payloadSerializer.Serialize(entity.Exception) : null;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload?.Any() == true ? payloadSerializer.Serialize(entity.Payload) : null;
@ -103,7 +103,7 @@ public class EFCoreActivityExecutionStore(
entity.ActivityState = await DeserializeActivityState(dbContext, entity, cancellationToken);
entity.Outputs = Deserialize<IDictionary<string, object?>>(dbContext, entity, "SerializedOutputs");
entity.Properties = Deserialize<IDictionary<string, object>?>(dbContext, entity, "SerializedProperties") ?? new Dictionary<string, object>();
entity.Properties = DeserializePayload<IDictionary<string, object>?>(dbContext, entity, "SerializedProperties") ?? new Dictionary<string, object>();
entity.Exception = DeserializePayload<ExceptionState>(dbContext, entity, "SerializedException");
entity.Payload = DeserializePayload<IDictionary<string, object>>(dbContext, entity, "SerializedPayload");
}

View file

@ -75,8 +75,8 @@ public class EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, W
private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken)
{
entity = entity.SanitizeLogMessage();
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? await safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : default;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? await safeSerializer.SerializeAsync(entity.Payload, cancellationToken) : default;
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? safeSerializer.Serialize(entity.ActivityState, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? safeSerializer.Serialize(entity.Payload, cancellationToken) : null;
}
private async ValueTask OnLoadAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity, CancellationToken cancellationToken)

View file

@ -240,7 +240,7 @@ public static class ObjectConverter
}
/// <summary>
/// Returns true if the specified type is date-like type, false otherwise.
/// Returns true if the specified type is a date-like type, false otherwise.
/// </summary>
private static bool IsDateType(Type type)
{

View file

@ -17,7 +17,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>

View file

@ -13,7 +13,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>

View file

@ -1,4 +1,5 @@
using Azure.Messaging.ServiceBus.Administration;
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
@ -121,6 +122,13 @@ public class AzureServiceBusFeature : FeatureBase
}
configurator.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("Elsa", false));
configurator.ConfigureJsonSerializerOptions(serializerOptions =>
{
var serializer = context.GetRequiredService<IJsonSerializer>();
serializer.ApplyOptions(serializerOptions);
return serializerOptions;
});
});
};
});

View file

@ -1,3 +1,4 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
@ -87,6 +88,13 @@ public class RabbitMqServiceBusFeature : FeatureBase
}
configurator.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("Elsa", false));
configurator.ConfigureJsonSerializerOptions(serializerOptions =>
{
var serializer = context.GetRequiredService<IJsonSerializer>();
serializer.ApplyOptions(serializerOptions);
return serializerOptions;
});
});
};
});

View file

@ -119,4 +119,12 @@ public class DispatchWorkflowRequestConsumer(IWorkflowDefinitionService workflow
var workflowClient = await workflowRuntime.CreateClientAsync(message.InstanceId, cancellationToken);
await workflowClient.RunInstanceAsync(request, cancellationToken);
}
private IDictionary<string, object>? DeserializeInput(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
return jsonSerializer.Deserialize<IDictionary<string, object>>(json);
}
}

View file

@ -14,6 +14,10 @@ public class DispatchResumeWorkflows(string activityTypeName, object stimulus)
public string? CorrelationId { get; set; }
public string? WorkflowInstanceId { get; set; }
public string? ActivityInstanceId { get; set; }
[Obsolete("This property is no longer used and will be removed in a future version. Use the SerializedInput property instead.")]
public IDictionary<string, object>? Input { get; set; }
public string? SerializedInput { get; set; }
public IDictionary<string, object>? Properties { get; set; }
}

View file

@ -14,6 +14,10 @@ public class DispatchTriggerWorkflows(string activityTypeName, object stimulus)
public string? CorrelationId { get; set; }
public string? WorkflowInstanceId { get; set; }
public string? ActivityInstanceId { get; set; }
[Obsolete("This property is no longer used and will be removed in a future version. Use the SerializedInput property instead.")]
public IDictionary<string, object>? Input { get; set; }
public string? SerializedInput { get; set; }
public IDictionary<string, object>? Properties { get; set; }
}

View file

@ -32,8 +32,14 @@ public record DispatchWorkflowDefinition
/// The ID of the parent workflow instance.
public string? ParentWorkflowInstanceId { get; init; }
/// Deprecated. Use the <see cref="SerializedInput"/> property instead.
[Obsolete("This property is no longer used and will be removed in a future version. Use the SerializedInput property instead.")]
public IDictionary<string, object>? Input { get; set; }
/// <summary>
/// Any input to pass to the workflow.
public IDictionary<string, object>? Input { get; init; }
/// </summary>
public string? SerializedInput { get; set; }
/// Any properties to attach to the workflow.
public IDictionary<string, object>? Properties { get; init; }

View file

@ -8,6 +8,8 @@ public class DispatchWorkflowInstance(string instanceId)
public string? BookmarkId { get; set; }
public ActivityHandle? ActivityHandle { get; set; }
public IDictionary<string, object>? Input { get; set; }
public string? SerializedInput { get; set; }
public IDictionary<string, object>? Properties { get; set; }
public string? CorrelationId { get; set; }
}

View file

@ -25,6 +25,7 @@ public class MassTransitWorkflowDispatcher(
IStimulusHasher stimulusHasher,
ITriggerStore triggerStore,
IBookmarkStore bookmarkStore,
IPayloadSerializer jsonSerializer,
ILogger<MassTransitWorkflowDispatcher> logger)
: IWorkflowDispatcher
{
@ -53,6 +54,7 @@ public class MassTransitWorkflowDispatcher(
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
var sendEndpoint = await GetSendEndpointAsync(options);
var serializedInput = SerializeInput(request.Input);
await sendEndpoint.Send(new DispatchWorkflowInstance(request.InstanceId)
{
@ -173,4 +175,9 @@ public class MassTransitWorkflowDispatcher(
var sendEndpoint = await bus.GetSendEndpoint(new Uri($"queue:{endpointName}"));
return sendEndpoint;
}
private string? SerializeInput(object? input)
{
return input != null ? jsonSerializer.Serialize(input) : null;
}
}

View file

@ -13,6 +13,11 @@
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" />
<PackageReference Include="MongoDB.Driver.Extensions" />
</ItemGroup>
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />

View file

@ -11,8 +11,4 @@
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
</Project>

View file

@ -1,36 +1,82 @@
using System.Diagnostics;
using Elsa.Common.Contracts;
using Elsa.OpenTelemetry.Helpers;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Pipelines.ActivityExecution;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Activity = System.Diagnostics.Activity;
using ActivityKind = System.Diagnostics.ActivityKind;
namespace Elsa.OpenTelemetry.Middleware;
/// <inheritdoc />
[UsedImplicitly]
public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
public class OpenTelemetryTracingActivityExecutionMiddleware(ActivityMiddlewareDelegate next, ISystemClock systemClock) : IActivityExecutionMiddleware
{
/// <inheritdoc />
public async ValueTask InvokeAsync(ActivityExecutionContext context)
{
var activity = context.Activity;
using var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"ActivityExecution {context.ActivityDescriptor.TypeName}", ActivityKind.Internal, Activity.Current?.Context ?? default);
span?.AddTag("activity.nodeId", activity.NodeId);
span?.AddTag("activity.type", activity.Type);
span?.AddTag("activity.name", activity.Name);
span?.AddTag("activityInstance.id", context.Id);
span?.AddTag("activityInstance.originalStatus", context.Status.ToString());
span?.AddEvent(new ActivityEvent("Executing"));
using var span = ElsaOpenTelemetry.ActivitySource.StartActivity($"ActivityExecution", ActivityKind.Internal, Activity.Current?.Context ?? default);
if (span == null)
{
await next(context);
return;
}
span.SetTag("activity.nodeId", activity.NodeId);
span.SetTag("activity.type", activity.Type);
span.SetTag("activity.name", activity.Name);
span.SetTag("activityInstance.id", context.Id);
span.SetTag("activityExecution.startTimeUtc", span.StartTimeUtc);
span.AddEvent(new ActivityEvent("Executing", tags: CreateStatusTags(context)));
await next(context);
span?.AddEvent(new ActivityEvent("Executed"));
span?.AddTag("activityInstance.newStatus", context.Status.ToString());
if (context.Status == ActivityStatus.Faulted)
{
span.AddEvent(new ActivityEvent("Faulted", tags: CreateStatusTags(context)));
span.SetStatus(ActivityStatusCode.Error);
span.SetTag("error", true);
span.SetTag("activityInstance.hasIncidents", true);
var errorMessage = string.IsNullOrWhiteSpace(context.Exception?.Message) ? "Unknown error" : context.Exception.Message;
span.SetTag("error.message", errorMessage);
if (!string.IsNullOrEmpty(context.Exception?.StackTrace))
span.SetTag("error.stackTrace", context.Exception.StackTrace);
}
else
{
span.AddEvent(new ActivityEvent("Executed", tags: CreateStatusTags(context)));
span.SetStatus(ActivityStatusCode.Ok);
}
var now = systemClock.UtcNow;
span.SetTag("activityExecution.endTimeUtc", now);
span.SetTag("activityExecution.durationMs", (now - span.StartTimeUtc).TotalMilliseconds);
}
private ActivityTagsCollection CreateStatusTags(ActivityExecutionContext context)
{
return new ActivityTagsCollection(new Dictionary<string, object?>
{
["activityInstance.status"] = context.Status.ToString()
});
}
}
/// <summary>
/// Contains extension methods for <see cref="OpenTelemetryTracingActivityExecutionMiddleware"/>.
/// </summary>
[UsedImplicitly]
public static class OpenTelemetryTracingActivityExecutionMiddlewareExtensions
{
/// Installs the <see cref="OpenTelemetryTracingActivityExecutionMiddleware"/> component in the workflow execution pipeline.
public static IActivityExecutionPipelineBuilder UseActivityExecutionTracing(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert<OpenTelemetryTracingActivityExecutionMiddleware>(0);
}
}

View file

@ -1,42 +1,103 @@
using System.Diagnostics;
using System.Text.Json;
using Elsa.Common.Contracts;
using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.OpenTelemetry.Helpers;
using Elsa.Workflows;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Pipelines.WorkflowExecution;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Activity = System.Diagnostics.Activity;
using ActivityKind = System.Diagnostics.ActivityKind;
namespace Elsa.OpenTelemetry.Middleware;
/// <summary>
/// Middleware that traces workflow execution using OpenTelemetry.
/// </summary>
[UsedImplicitly]
public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next)
public class OpenTelemetryTracingWorkflowExecutionMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock) : WorkflowExecutionMiddleware(next)
{
private readonly JsonSerializerOptions? _incidentSerializerOptions = new JsonSerializerOptions().WithConverters(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault()));
/// <inheritdoc />
public override async ValueTask InvokeAsync(WorkflowExecutionContext context)
{
var workflowInstanceId = context.Id;
var workflow = context.Workflow;
using var activity = ElsaOpenTelemetry.ActivitySource.StartActivity($"WorkflowExecution {workflow.WorkflowMetadata.Name}", ActivityKind.Internal, Activity.Current?.Context ?? default);
using var span = ElsaOpenTelemetry.ActivitySource.StartActivity("WorkflowExecution", ActivityKind.Internal, Activity.Current?.Context ?? default);
if (span == null) // No listener is registered.
{
await Next(context);
return;
}
span.SetTag("workflowInstance.id", workflowInstanceId);
span.SetTag("workflowDefinition.definitionId", workflow.Identity.DefinitionId);
span.SetTag("workflowDefinition.version", workflow.Identity.Version);
span.SetTag("workflowDefinition.name", workflow.WorkflowMetadata.Name);
span.SetTag("workflowExecution.startTimeUtc", span.StartTimeUtc);
if(!string.IsNullOrWhiteSpace(context.CorrelationId))
activity?.AddTag("correlationId", context.CorrelationId);
if(context.TriggerActivityId != null)
{
var activity = context.FindActivityById(context.TriggerActivityId) ?? throw new Exception($"Trigger activity with ID {context.TriggerActivityId} not found. This should not happen.");
span.SetTag("workflowExecution.trigger.activityId", activity.Id);
span.SetTag("workflowExecution.trigger.activityName", activity.Name);
span.SetTag("workflowExecution.trigger.activityType", activity.Type);
}
activity?.AddTag("workflowInstance.id", workflowInstanceId);
activity?.AddTag("workflowDefinition.definitionId", workflow.Identity.DefinitionId);
activity?.AddTag("workflowDefinition.version", workflow.Identity.Version);
activity?.AddTag("tenantId", workflow.Identity.TenantId);
activity?.AddTag("workflowInstance.originalStatus", context.Status.ToString());
activity?.AddTag("workflowInstance.originalSubStatus", context.SubStatus.ToString());
activity?.AddEvent(new ActivityEvent("Executing"));
span.AddEvent(new ActivityEvent("Executing", tags: CreateStatusTags(context)));
await Next(context);
activity?.AddEvent(new ActivityEvent("Executed"));
activity?.AddTag("workflowInstance.newStatus", context.Status.ToString());
activity?.AddTag("workflowInstance.newSubStatus", context.SubStatus.ToString());
if (context.SubStatus == WorkflowSubStatus.Faulted)
{
span.AddEvent(new ActivityEvent("Faulted", tags: CreateStatusTags(context)));
span.SetStatus(ActivityStatusCode.Error);
span.SetTag("error", true);
}
else
{
span.AddEvent(new ActivityEvent("Executed", tags: CreateStatusTags(context)));
span.SetStatus(ActivityStatusCode.Ok);
}
if(context.Incidents.Any())
{
span.SetStatus(ActivityStatusCode.Error);
span.SetTag("workflowInstance.hasIncidents", true);
span.SetTag("error", true);
if (context.Incidents.Count > 0)
span.SetTag("error.message", JsonSerializer.Serialize(context.Incidents, _incidentSerializerOptions));
}
if (!string.IsNullOrWhiteSpace(context.CorrelationId))
span.SetTag("workflowInstance.correlationId", context.CorrelationId);
var now = systemClock.UtcNow;
span.SetTag("workflowExecution.endTimeUtc", now);
span.SetTag("workflowExecution.durationMs", (now - span.StartTimeUtc).TotalMilliseconds);
}
private ActivityTagsCollection CreateStatusTags(WorkflowExecutionContext context)
{
return new ActivityTagsCollection(new Dictionary<string, object?>
{
["workflowInstance.status"] = context.Status.ToString(),
["workflowInstance.subStatus"] = context.SubStatus.ToString()
});
}
}
/// <summary>
/// Contains extension methods for <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/>.
/// </summary>
[UsedImplicitly]
public static class OpenTelemetryWorkflowExecutionMiddlewareExtensions
{
/// Installs the <see cref="OpenTelemetryTracingWorkflowExecutionMiddleware"/> component in the workflow execution pipeline.
public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionTracing(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.Insert<OpenTelemetryTracingWorkflowExecutionMiddleware>(0);
}
}

View file

@ -14,6 +14,10 @@
<PackageReference Include="Quartz.Serialization.Json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj" />
<ProjectReference Include="..\Elsa.Quartz\Elsa.Quartz.csproj" />

View file

@ -15,6 +15,10 @@
<PackageReference Include="Quartz.Serialization.Json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj" />
<ProjectReference Include="..\Elsa.Quartz\Elsa.Quartz.csproj" />

View file

@ -21,8 +21,9 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="System.Formats.Asn1" />
<PackageReference Include="Azure.Identity" VersionOverride="1.11.4" />
<PackageReference Include="Microsoft.Identity.Client" VersionOverride="4.61.3" />
<PackageReference Include="System.Formats.Asn1" VersionOverride="8.0.1" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -13,6 +13,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Quartz.Serialization.Json" />
</ItemGroup>
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj" />

View file

@ -24,7 +24,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -17,7 +17,7 @@
<!--Overridden for vulnerability reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -15,4 +15,8 @@
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<!--Overridden for vulnaribility reasons with dependencies referencing older versions.-->
<ItemGroup>
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonVersion)" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,38 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutionSummaries.ListSummaries;
/// <summary>
/// Lists a summary view of the executions for a given activity.
/// </summary>
[PublicAPI]
internal class Endpoint(IActivityExecutionStore store) : ElsaEndpoint<Request, ListResponse<ActivityExecutionRecordSummary>>
{
/// <inheritdoc />
public override void Configure()
{
Get("/activity-execution-summaries/list");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task<ListResponse<ActivityExecutionRecordSummary>> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new ActivityExecutionRecordFilter
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityNodeId = request.ActivityNodeId,
Completed = request.Completed
};
var order = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending);
var records = (await store.FindManySummariesAsync(filter, order, cancellationToken)).ToList();
return new ListResponse<ActivityExecutionRecordSummary>(records);
}
}

View file

@ -0,0 +1,22 @@
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutionSummaries.ListSummaries;
/// <summary>
/// A request for a list of activity execution log record summaries for a given activity in a workflow instance.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The node ID of the activity to get the execution record for.
/// </summary>
public string ActivityNodeId { get; set; } = default!;
/// <summary>
/// Whether to include completed activity execution records. If not specified, all activity execution records will be included.
/// </summary>
public bool? Completed { get; set; }
}

View file

@ -0,0 +1,41 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Get;
/// <summary>
/// Gets an individual execution for a given activity.
/// </summary>
[PublicAPI]
internal class Endpoint(IActivityExecutionStore store) : ElsaEndpointWithoutRequest<ActivityExecutionRecord>
{
/// <inheritdoc />
public override void Configure()
{
Get("/activity-executions/{id}");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var id = Route<string>("id");
var filter = new ActivityExecutionRecordFilter
{
Id = id
};
var record = await store.FindAsync(filter, cancellationToken);
if (record == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(record, cancellationToken);
}
}

View file

@ -31,7 +31,7 @@ public class List : ElsaEndpointWithoutRequest<Response>
public override Task<Response> ExecuteAsync(CancellationToken ct)
{
var drivers = _registry.List();
var descriptors = drivers.Select(FromDriver).ToList();
var descriptors = drivers.Select(FromDriver).OrderByDescending(x => x.Priority).ToList();
var response = new Response(descriptors);
return Task.FromResult(response);
@ -40,7 +40,9 @@ public class List : ElsaEndpointWithoutRequest<Response>
private static StorageDriverDescriptor FromDriver(IStorageDriver driver)
{
var type = driver.GetType();
var deprecated = type.GetCustomAttribute<ObsoleteAttribute>() != null;
var displayName = type.GetCustomAttribute<DisplayAttribute>()?.Name ?? type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? type.Name.Replace("StorageDriver", "");
return new StorageDriverDescriptor(type.GetSimpleAssemblyQualifiedName(), displayName);
var priority = driver.Priority;
return new StorageDriverDescriptor(type.GetSimpleAssemblyQualifiedName(), displayName, priority, deprecated);
}
}

View file

@ -5,4 +5,4 @@ public class Response(ICollection<StorageDriverDescriptor> items)
public ICollection<StorageDriverDescriptor> Items { get; set; } = items;
}
public record StorageDriverDescriptor(string TypeName, string DisplayName);
public record StorageDriverDescriptor(string TypeName, string DisplayName, double Priority = 0, bool Deprecated = false);

View file

@ -123,8 +123,8 @@ internal class Export : ElsaEndpointWithMapper<Request, WorkflowInstanceMapper>
var bookmarks = request.IncludeBookmarks ? await LoadBookmarksAsync(workflowState.Id, cancellationToken) : null;
var workflowStateElement = _workflowStateSerializer.SerializeToElement(workflowState);
var bookmarksElement = bookmarks != null ? SerializeBookmarks(bookmarks) : default(JsonElement?);
var executionLogRecordsElement = executionLogRecords != null ? await _safeSerializer.SerializeToElementAsync(executionLogRecords, cancellationToken) : default(JsonElement?);
var activityExecutionLogRecordsElement = activityExecutionLogRecords != null ? await _safeSerializer.SerializeToElementAsync(activityExecutionLogRecords, cancellationToken) : default(JsonElement?);
var executionLogRecordsElement = executionLogRecords != null ? _safeSerializer.SerializeToElement(executionLogRecords, cancellationToken) : default(JsonElement?);
var activityExecutionLogRecordsElement = activityExecutionLogRecords != null ? _safeSerializer.SerializeToElement(activityExecutionLogRecords, cancellationToken) : default(JsonElement?);
var model = new ExportedWorkflowState(workflowStateElement, bookmarksElement, activityExecutionLogRecordsElement, executionLogRecordsElement);
return model;
}

View file

@ -148,13 +148,13 @@ internal class Import : ElsaEndpointWithoutRequest<Response>
if (model.ActivityExecutionRecords != null)
{
var activityExecutionRecords = await _safeSerializer.DeserializeAsync<ICollection<ActivityExecutionRecord>>(model.ActivityExecutionRecords.Value, cancellationToken);
var activityExecutionRecords = _safeSerializer.Deserialize<ICollection<ActivityExecutionRecord>>(model.ActivityExecutionRecords.Value, cancellationToken);
await _activityExecutionStore.SaveManyAsync(activityExecutionRecords, cancellationToken);
}
if (model.WorkflowExecutionLogRecords != null)
{
var workflowExecutionLogRecords = await _safeSerializer.DeserializeAsync<ICollection<WorkflowExecutionLogRecord>>(model.WorkflowExecutionLogRecords.Value, cancellationToken);
var workflowExecutionLogRecords = _safeSerializer.Deserialize<ICollection<WorkflowExecutionLogRecord>>(model.WorkflowExecutionLogRecords.Value, cancellationToken);
await _workflowExecutionLogStore.SaveManyAsync(workflowExecutionLogRecords, cancellationToken);
}
}

View file

@ -12,16 +12,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Journal.GetLastEntry;
/// Return the last log entry for the specified workflow instance and activity ID.
/// </summary>
[PublicAPI]
public class Get : ElsaEndpoint<Request, WorkflowExecutionLogRecord>
public class Get(IWorkflowExecutionLogStore store) : ElsaEndpoint<Request, WorkflowExecutionLogRecord>
{
private readonly IWorkflowExecutionLogStore _store;
/// <inheritdoc />
public Get(IWorkflowExecutionLogStore store)
{
_store = store;
}
/// <inheritdoc />
public override void Configure()
{
@ -36,7 +28,7 @@ public class Get : ElsaEndpoint<Request, WorkflowExecutionLogRecord>
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityId = request.ActivityId,
EventNames = new[] { "Started", "Completed", "Faulted" }
EventNames = ["Started", "Completed", "Faulted"]
};
var sort = new WorkflowExecutionLogRecordOrder<DateTimeOffset>(
@ -44,7 +36,7 @@ public class Get : ElsaEndpoint<Request, WorkflowExecutionLogRecord>
OrderDirection.Descending
);
var entry = await _store.FindAsync(filter, sort, cancellationToken);
var entry = await store.FindAsync(filter, sort, cancellationToken);
if (entry == null)
{

View file

@ -17,7 +17,7 @@ namespace Elsa.Workflows.Activities;
/// Represents a composite activity that has a single <see cref="Root"/> activity. Like a workflow, but without workflow-level properties.
/// </summary>
[PublicAPI]
public abstract class Composite : Activity, IVariableContainer
public abstract class Composite : Activity, IVariableContainer, IComposite
{
/// <inheritdoc />
protected Composite(string? source = default, int? line = default) : base(source, line)
@ -172,6 +172,10 @@ public abstract class Composite : Activity, IVariableContainer
/// Creates a new <see cref="Activities.SetVariable"/> activity.
/// </summary>
protected static SetVariable<T> SetVariable<T>(Variable<T> variable, Variable<T> value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(variable, value, source, line);
public virtual void Setup()
{
}
}
/// <summary>

View file

@ -0,0 +1,6 @@
namespace Elsa.Workflows.Contracts;
public interface IComposite
{
void Setup();
}

View file

@ -48,4 +48,9 @@ public interface IPayloadSerializer
/// <param name="serializedData">The serialized state.</param>
/// <returns>The deserialized state.</returns>
T Deserialize<T>(JsonElement serializedData);
/// <summary>
/// Gets the JSON serializer options.
/// </summary>
JsonSerializerOptions GetOptions();
}

View file

@ -11,24 +11,57 @@ public interface ISafeSerializer
/// <summary>
/// Serializes the specified state.
/// </summary>
[Obsolete("Use the non-async Serialize instead.")]
[RequiresUnreferencedCode("The type T may be trimmed.")]
ValueTask<string> SerializeAsync(object? value, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified state to a <see cref="JsonElement"/> object.
/// </summary>
[Obsolete("Use the non-async SerializeToElement instead.")]
[RequiresUnreferencedCode("The type T may be trimmed.")]
ValueTask<JsonElement> SerializeToElementAsync(object? value, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified state.
/// </summary>
[Obsolete("Use the non-async Deserialize instead.")]
[RequiresUnreferencedCode("The type T may be trimmed.")]
ValueTask<T> DeserializeAsync<T>(string json, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified state.
/// </summary>
[Obsolete("Use the non-async Deserialize instead.")]
[RequiresUnreferencedCode("The type T may be trimmed.")]
ValueTask<T> DeserializeAsync<T>(JsonElement element, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified state.
/// </summary>
[RequiresUnreferencedCode("The type T may be trimmed.")]
string Serialize(object? value, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified state to a <see cref="JsonElement"/> object.
/// </summary>
[RequiresUnreferencedCode("The type T may be trimmed.")]
JsonElement SerializeToElement(object? value, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified state.
/// </summary>
[RequiresUnreferencedCode("The type T may be trimmed.")]
T Deserialize<T>(string json, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified state.
/// </summary>
[RequiresUnreferencedCode("The type T may be trimmed.")]
T Deserialize<T>(JsonElement element, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the JSON serializer options.
/// </summary>
JsonSerializerOptions GetOptions();
}

View file

@ -5,6 +5,11 @@ namespace Elsa.Workflows.Contracts;
/// </summary>
public interface IStorageDriver
{
/// <summary>
/// The priority of the storage driver. Drivers with higher priority are used before drivers with lower priority.
/// </summary>
double Priority { get; }
/// <summary>
/// Writes a value to the storage driver.
/// </summary>

View file

@ -13,27 +13,70 @@ public interface IWorkflowStateSerializer
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version Serialize instead.")]
Task<string> SerializeAsync(WorkflowState workflowState, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
string Serialize(WorkflowState workflowState);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version SerializeToUtfBytes instead.")]
Task<byte[]> SerializeToUtfBytesAsync(WorkflowState workflowState, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
byte[] SerializeToUtfBytes(WorkflowState workflowState);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The serialized workflow state.</returns>
JsonElement SerializeToElement(WorkflowState workflowState);
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version SerializeToElement instead.")]
Task<JsonElement> SerializeToElementAsync(WorkflowState workflowState, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
JsonElement SerializeToElement(WorkflowState workflowState);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version Serialize instead.")]
Task<string> SerializeAsync(object workflowState, CancellationToken cancellationToken = default);
/// <summary>
/// Serializes the specified workflow state.
/// </summary>
/// <param name="workflowState">The workflow state to serialize.</param>
/// <returns>The serialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
string Serialize(object workflowState);
/// <summary>
@ -41,19 +84,52 @@ public interface IWorkflowStateSerializer
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <returns>The deserialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
Task<WorkflowState> DeserializeAsync(string serializedState, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified serialized state.
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <returns>The deserialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
WorkflowState Deserialize(string serializedState);
/// <summary>
/// Deserializes the specified serialized state.
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The deserialized workflow state.</returns>
WorkflowState Deserialize(JsonElement serializedState);
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
Task<WorkflowState> DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified serialized state.
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <returns>The deserialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
WorkflowState Deserialize(JsonElement serializedState);
/// <summary>
/// Deserializes the specified serialized state.
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The deserialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
Task<T> DeserializeAsync<T>(string serializedState, CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes the specified serialized state.
/// </summary>
/// <param name="serializedState">The serialized state.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The deserialized workflow state.</returns>
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
T Deserialize<T>(string serializedState);
}

View file

@ -108,6 +108,47 @@ public static partial class ActivityExecutionContextExtensions
/// </summary>
public static IDictionary<string, object> GetVariableValues(this ActivityExecutionContext activityExecutionContext) => activityExecutionContext.ExpressionExecutionContext.ReadAndFlattenMemoryBlocks();
/// <summary>
/// Evaluates each input property of the activity.
/// </summary>
public static async Task EvaluateInputPropertiesAsync(this ActivityExecutionContext context)
{
var activityDescriptor = context.ActivityDescriptor;
var inputDescriptors = activityDescriptor.Inputs.Where(x => x.AutoEvaluate).ToList();
// Evaluate inputs.
foreach (var inputDescriptor in inputDescriptors)
await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
context.SetHasEvaluatedProperties();
}
/// <summary>
/// Evaluates the specified input property of the activity.
/// </summary>
public static async Task<T?> EvaluateInputPropertyAsync<TActivity, T>(this ActivityExecutionContext context, Expression<Func<TActivity, Input<T>>> propertyExpression)
{
var inputName = propertyExpression.GetProperty()!.Name;
var input = await EvaluateInputPropertyAsync(context, inputName);
return input.ConvertTo<T>();
}
/// <summary>
/// Evaluates a specific input property of the activity.
/// </summary>
public static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, string inputName)
{
var activity = context.Activity;
var activityRegistryLookup = context.GetRequiredService<IActivityRegistryLookupService>();
var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type) ?? throw new Exception($"Activity descriptor \"{activity.Type}\" not found");
var inputDescriptor = activityDescriptor.GetWrappedInputPropertyDescriptor(activity, inputName);
if (inputDescriptor == null)
throw new Exception($"No input with name \"{inputName}\" could be found");
return await EvaluateInputPropertyAsync(context, activityDescriptor, inputDescriptor);
}
/// <summary>
/// Returns a set of tuples containing the activity and its descriptor for all activities with outputs.
/// </summary>
@ -164,6 +205,59 @@ public static partial class ActivityExecutionContextExtensions
return node?.Activity;
}
private static async Task<object?> EvaluateInputPropertyAsync(this ActivityExecutionContext context, ActivityDescriptor activityDescriptor, InputDescriptor inputDescriptor)
{
var activity = context.Activity;
var defaultValue = inputDescriptor.DefaultValue;
var value = defaultValue;
var input = inputDescriptor.ValueGetter(activity);
if (inputDescriptor.IsWrapped)
{
var wrappedInput = (Input?)input;
if (defaultValue != null && wrappedInput == null)
{
var typedInput = typeof(Input<>).MakeGenericType(inputDescriptor.Type);
var valueExpression = new Literal(defaultValue)
{
Id = Guid.NewGuid().ToString()
};
wrappedInput = (Input)Activator.CreateInstance(typedInput, valueExpression)!;
inputDescriptor.ValueSetter(activity, wrappedInput);
}
else
{
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
var expressionExecutionContext = context.ExpressionExecutionContext;
value = wrappedInput?.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue;
}
var memoryReference = wrappedInput?.MemoryBlockReference();
// When input is created from an activity provider, there may be no memory block reference.
if (memoryReference?.Id != null!)
{
// Declare the input memory block on the current context.
context.ExpressionExecutionContext.Set(memoryReference, value!);
}
}
else
{
value = input;
}
// Store the serialized input value in the activity state.
// Serializing the value ensures we store a copy of the value and not a reference to the input, which may change over time.
if (inputDescriptor.IsSerializable != false)
{
var serializedValue = context.GetRequiredService<ISafeSerializer>().SerializeToElement(value);
context.ActivityState[inputDescriptor.Name] = serializedValue;
}
return value;
}
/// <summary>
/// Returns the outcome name for the specified port property name.
/// </summary>
@ -297,7 +391,7 @@ public static partial class ActivityExecutionContextExtensions
if (outputValue == null!)
continue;
var serializedOutputValue = await serializer.SerializeAsync(outputValue, cancellationToken);
var serializedOutputValue = serializer.Serialize(outputValue, cancellationToken);
context.JournalData[outputName] = serializedOutputValue;
}

View file

@ -140,7 +140,7 @@ public static class ExpressionExecutionContextExtensions
var variable = new Variable(name, value)
{
StorageDriverType = storageDriverType ?? typeof(WorkflowStorageDriver)
StorageDriverType = storageDriverType ?? typeof(WorkflowInstanceStorageDriver)
};
// Find the first parent context that has a variable container.

View file

@ -31,14 +31,14 @@ public static class VariableExtensions
new ExpandoObjectConverterFactory());
/// <summary>
/// Configures the variable to use the <see cref="WorkflowStorageDriver"/>.
/// Configures the variable to use the <see cref="WorkflowInstanceStorageDriver"/>.
/// </summary>
public static Variable WithWorkflowStorage(this Variable variable) => variable.WithStorage<WorkflowStorageDriver>();
public static Variable WithWorkflowStorage(this Variable variable) => variable.WithStorage<WorkflowInstanceStorageDriver>();
/// <summary>
/// Configures the variable to use the <see cref="WorkflowStorageDriver"/>.
/// Configures the variable to use the <see cref="WorkflowInstanceStorageDriver"/>.
/// </summary>
public static Variable<T> WithWorkflowStorage<T>(this Variable<T> variable) => (Variable<T>)variable.WithStorage<WorkflowStorageDriver>();
public static Variable<T> WithWorkflowStorage<T>(this Variable<T> variable) => (Variable<T>)variable.WithStorage<WorkflowInstanceStorageDriver>();
/// <summary>
/// Configures the variable to use the <see cref="MemoryStorageDriver"/>.

View file

@ -21,6 +21,13 @@ public class ActivityExecutionPipelinePipelineBuilder(IServiceProvider servicePr
return this;
}
/// <inheritdoc />
public IActivityExecutionPipelineBuilder Insert(int index, Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware)
{
_components.Insert(index, middleware);
return this;
}
/// <inheritdoc />
public ActivityMiddlewareDelegate Build()
{

View file

@ -52,12 +52,14 @@ public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionPipelineBuilde
return this;
}
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Insert(int index, Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
{
_components.Insert(index, middleware);
return this;
}
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Replace(int index, Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
{
_components[index] = middleware;

View file

@ -93,9 +93,9 @@ public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegi
if (isDictionary)
{
// Remove the _type property name from the JSON, if any.
var parsedModel = (JsonObject)JsonNode.Parse(ref reader)!;
parsedModel.Remove(TypePropertyName);
return parsedModel.Deserialize(targetType, newOptions)!;
var parsedNode = JsonNode.Parse(ref reader)!;
if (parsedNode is JsonObject parsedModel) parsedModel.Remove(TypePropertyName);
return parsedNode.Deserialize(targetType, newOptions)!;
}
var isCollection = typeof(ICollection).IsAssignableFrom(targetType);
@ -291,6 +291,9 @@ public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegi
private Type? ReadType(Utf8JsonReader reader)
{
if (reader.TokenType != JsonTokenType.StartObject)
return null;
reader.Read(); // Move to the first token inside the object.
string? typeName = null;

View file

@ -9,7 +9,7 @@ namespace Elsa.Workflows.Serialization.Converters;
/// <summary>
/// Serializes <see cref="Type"/> objects to a simple alias representing the type.
/// </summary>
[PublicAPI]
[UsedImplicitly]
public class TypeJsonConverter : JsonConverter<Type>
{
private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry;

View file

@ -25,14 +25,14 @@ public class JsonPayloadSerializer : IPayloadSerializer
/// <inheritdoc />
public string Serialize(object payload)
{
var options = GetPayloadSerializerOptions();
var options = GetOptions();
return JsonSerializer.Serialize(payload, options);
}
/// <inheritdoc />
public JsonElement SerializeToElement(object payload)
{
var options = GetPayloadSerializerOptions();
var options = GetOptions();
return JsonSerializer.SerializeToElement(payload, options);
}
@ -51,18 +51,19 @@ public class JsonPayloadSerializer : IPayloadSerializer
/// <inheritdoc />
public T Deserialize<T>(string payload)
{
var options = GetPayloadSerializerOptions();
var options = GetOptions();
return JsonSerializer.Deserialize<T>(payload, options)!;
}
/// <inheritdoc />
public T Deserialize<T>(JsonElement payload)
{
var options = GetPayloadSerializerOptions();
var options = GetOptions();
return payload.Deserialize<T>(options)!;
}
private JsonSerializerOptions GetPayloadSerializerOptions()
/// <inheritdoc />
public JsonSerializerOptions GetOptions()
{
var options = new JsonSerializerOptions
{

View file

@ -31,54 +31,96 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version Serialize instead.")]
public Task<string> SerializeAsync(WorkflowState workflowState, CancellationToken cancellationToken = default)
{
return Task.FromResult(Serialize(workflowState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version SerializeToUtfBytes instead.")]
public Task<byte[]> SerializeToUtfBytesAsync(WorkflowState workflowState, CancellationToken cancellationToken = default)
{
return Task.FromResult(SerializeToUtfBytes(workflowState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version SerializeToElement instead.")]
public Task<JsonElement> SerializeToElementAsync(WorkflowState workflowState, CancellationToken cancellationToken = default)
{
return Task.FromResult(SerializeToElement(workflowState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
[Obsolete("Use the non-async version Serialize instead.")]
public Task<string> SerializeAsync(object workflowState, CancellationToken cancellationToken = default)
{
return Task.FromResult(Serialize(workflowState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
public Task<WorkflowState> DeserializeAsync(string serializedState, CancellationToken cancellationToken = default)
{
return Task.FromResult(Deserialize(serializedState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
public Task<WorkflowState> DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default)
{
return Task.FromResult(Deserialize(serializedState, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
[Obsolete("Use the non-async version Deserialize instead.")]
public Task<T> DeserializeAsync<T>(string serializedState, CancellationToken cancellationToken = default)
{
return Task.FromResult(Deserialize<T>(serializedState, cancellationToken));
}
public string Serialize(WorkflowState workflowState)
{
var options = GetOptions();
return JsonSerializer.Serialize(workflowState, options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
public byte[] SerializeToUtfBytes(WorkflowState workflowState)
{
var options = GetOptions();
return JsonSerializer.SerializeToUtf8Bytes(workflowState, options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")]
public JsonElement SerializeToElement(WorkflowState workflowState)
{
var options = GetOptions();
return JsonSerializer.SerializeToElement(workflowState, options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
public string Serialize(object workflowState)
{
var options = GetOptions();
return JsonSerializer.Serialize(workflowState, workflowState.GetType(), options);
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
public WorkflowState Deserialize(string serializedState)
{
var options = GetOptions();
return JsonSerializer.Deserialize<WorkflowState>(serializedState, options)!;
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
public WorkflowState Deserialize(JsonElement serializedState)
{
var options = GetOptions();
return serializedState.Deserialize<WorkflowState>(options)!;
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")]
public T Deserialize<T>(string serializedState)
{
var options = GetOptions();

View file

@ -22,39 +22,59 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<string> SerializeAsync(object? value, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return ValueTask.FromResult(JsonSerializer.Serialize(value, options));
return ValueTask.FromResult(Serialize(value, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<JsonElement> SerializeToElementAsync(object? value, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return new(JsonSerializer.SerializeToElement(value, options));
return new(SerializeToElement(value, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<T> DeserializeAsync<T>(string json, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return new(JsonSerializer.Deserialize<T>(json, options)!);
return new(Deserialize<T>(json, cancellationToken));
}
/// <inheritdoc />
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<T> DeserializeAsync<T>(JsonElement element, CancellationToken cancellationToken = default)
{
return new(Deserialize<T>(element, cancellationToken));
}
public string Serialize(object? value, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return new(element.Deserialize<T>(options)!);
return JsonSerializer.Serialize(value, options);
}
public JsonElement SerializeToElement(object? value, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return JsonSerializer.SerializeToElement(value, options);
}
public T Deserialize<T>(string json, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return JsonSerializer.Deserialize<T>(json, options)!;
}
public T Deserialize<T>(JsonElement element, CancellationToken cancellationToken = default)
{
var options = GetOptions();
return element.Deserialize<T>(options)!;
}
/// <inheritdoc />
protected override void AddConverters(JsonSerializerOptions options)
{
var expressionDescriptorRegistry = ServiceProvider.GetRequiredService<IExpressionDescriptorRegistry>();
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.Converters.Add(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault()));
options.Converters.Add(new SafeValueConverterFactory());

View file

@ -20,6 +20,9 @@ public class ActivityFactory : IActivityFactory
var activityElement = context.Element;
var activityDescriptor = context.ActivityDescriptor;
var activity = (IActivity)context.Element.Deserialize(type, context.SerializerOptions)!;
var composite = activity as IComposite;
composite?.Setup();
ReadSyntheticInputs(activityDescriptor, activity, activityElement, context.SerializerOptions);
ReadSyntheticOutputs(activityDescriptor, activity, activityElement);

View file

@ -3,24 +3,13 @@ using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Models;
using Humanizer;
using Microsoft.Extensions.Logging;
namespace Elsa.Workflows;
/// <inheritdoc />
public class IdentityGraphService : IIdentityGraphService
public class IdentityGraphService(IActivityVisitor activityVisitor, IActivityRegistryLookupService activityRegistryLookup, ILogger<IdentityGraphService> logger) : IIdentityGraphService
{
private readonly IActivityVisitor _activityVisitor;
private readonly IActivityRegistryLookupService _activityRegistryLookup;
/// <summary>
/// Constructor.
/// </summary>
public IdentityGraphService(IActivityVisitor activityVisitor, IActivityRegistryLookupService activityRegistryLookup)
{
_activityVisitor = activityVisitor;
_activityRegistryLookup = activityRegistryLookup;
}
/// <inheritdoc />
public async Task AssignIdentitiesAsync(Workflow workflow, CancellationToken cancellationToken = default)
{
@ -30,7 +19,7 @@ public class IdentityGraphService : IIdentityGraphService
/// <inheritdoc />
public async Task AssignIdentitiesAsync(IActivity root, CancellationToken cancellationToken = default)
{
var graph = await _activityVisitor.VisitAsync(root, cancellationToken);
var graph = await activityVisitor.VisitAsync(root, cancellationToken);
await AssignIdentitiesAsync(graph);
}
@ -56,7 +45,14 @@ public class IdentityGraphService : IIdentityGraphService
/// <inheritdoc />
public async Task AssignInputOutputsAsync(IActivity activity)
{
var activityDescriptor = await _activityRegistryLookup.FindAsync(activity.Type, activity.Version) ?? throw new Exception("Activity descriptor not found");
var activityDescriptor = await activityRegistryLookup.FindAsync(activity.Type, activity.Version);
if (activityDescriptor == null!)
{
logger.LogWarning("Activity descriptor not found for activity type {ActivityType}. Skipping identity assignment", activity.Type);
return;
}
var inputDictionary = activityDescriptor.GetWrappedInputProperties(activity);
foreach (var (inputName, input) in inputDictionary)

View file

@ -11,6 +11,8 @@ public class MemoryStorageDriver : IStorageDriver
{
private readonly IDictionary<string, object> _dictionary = new Dictionary<string, object>();
public double Priority => 0;
/// <inheritdoc />
public ValueTask WriteAsync(string id, object value, StorageDriverContext context)
{

View file

@ -71,7 +71,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
private IDictionary<string, object> GetPersistableInput(WorkflowExecutionContext workflowExecutionContext)
{
// TODO: This is a temporary solution. We need to find a better way to handle this.
var persistableInput = workflowExecutionContext.Workflow.Inputs.Where(x => x.StorageDriverType == typeof(WorkflowStorageDriver)).ToList();
var persistableInput = workflowExecutionContext.Workflow.Inputs.Where(x => x.StorageDriverType == typeof(WorkflowStorageDriver) || x.StorageDriverType == typeof(WorkflowInstanceStorageDriver)).ToList();
var input = workflowExecutionContext.Input;
var filteredInput = new Dictionary<string, object>();

View file

@ -7,8 +7,8 @@ namespace Elsa.Workflows;
/// <summary>
/// A storage driver that stores objects in the workflow state itself.
/// </summary>
[Display(Name = "Workflow (deprecated)")]
[Obsolete("This class is no longer used and will be removed in a future version.")]
[Display(Name = "Workflow")]
[Obsolete("This is no longer used and will be removed in a future version. Use the WorkflowInstanceStorageDriver instead.")]
public class WorkflowStorageDriver : IStorageDriver
{
/// <summary>
@ -16,6 +16,9 @@ public class WorkflowStorageDriver : IStorageDriver
/// </summary>
public const string VariablesDictionaryStateKey = "PersistentVariablesDictionary";
/// <inheritdoc />
public double Priority => -1;
/// <inheritdoc />
public ValueTask WriteAsync(string id, object value, StorageDriverContext context)
{

View file

@ -206,7 +206,7 @@ public class BulkDispatchWorkflows : Activity
var childInstanceId = new Variable<string>("ChildInstanceId", workflowInstanceId)
{
StorageDriverType = typeof(WorkflowStorageDriver)
StorageDriverType = typeof(WorkflowInstanceStorageDriver)
};
var variables = new List<Variable>

View file

@ -42,7 +42,7 @@ public class ActivityExecutionRecordSummary : Entity
/// <summary>
/// Gets or sets the time at which the activity execution began.
/// </summary>
public DateTimeOffset StartedAt { get; set; } = default!;
public DateTimeOffset StartedAt { get; set; }
/// <summary>
/// Gets or sets whether the activity has any bookmarks.
@ -60,7 +60,7 @@ public class ActivityExecutionRecordSummary : Entity
public DateTimeOffset? CompletedAt { get; set; }
/// <summary>
/// Returns a summary view of the specified <see cref="WorkflowInstance"/>.
/// Returns a summary view of the specified <see cref="ActivityExecutionRecord"/>.
/// </summary>
public static ActivityExecutionRecordSummary FromRecord(ActivityExecutionRecord record)
{
@ -81,7 +81,7 @@ public class ActivityExecutionRecordSummary : Entity
}
/// <summary>
/// Returns a summary view of the specified <see cref="WorkflowInstance"/>.
/// Returns a summary view of the specified <see cref="ActivityExecutionRecord"/>.
/// </summary>
public static Expression<Func<ActivityExecutionRecord, ActivityExecutionRecordSummary>> FromRecordExpression()
{

View file

@ -26,7 +26,7 @@ public class WorkflowExecutionLogRecord : Entity, ILogRecord
/// <summary>
/// The version of the workflow definition.
/// </summary>
public int WorkflowVersion { get; init; }
public int WorkflowVersion { get; set; }
/// <summary>
/// The ID of the activity instance.

View file

@ -1,10 +1,9 @@
using Elsa.Extensions;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Features;
using Elsa.Workflows.Middleware.Activities;
// ReSharper disable once CheckNamespace
namespace Elsa.Workflows.Runtime.Extensions;
namespace Elsa.Extensions;
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
public static class PipelineWorkflowsFeatureExtensions

View file

@ -0,0 +1,34 @@
using Elsa.Workflows.Middleware.Activities;
// ReSharper disable once CheckNamespace
using Elsa.Workflows.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
/// </summary>
public static class WorkflowsFeatureExtensions
{
/// <summary>
/// Installs a default workflow runtime execution pipeline.
/// </summary>
public static WorkflowsFeature WithDefaultWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature) =>
workflowsFeature.WithWorkflowExecutionPipeline(pipeline =>
pipeline.UseDefaultPipeline());
/// <summary>
/// Installs an activity invoker that can run activities asynchronously in the background.
/// </summary>
public static WorkflowsFeature WithBackgroundActivityExecutionPipeline(this WorkflowsFeature workflowsFeature) =>
workflowsFeature.WithActivityExecutionPipeline(pipeline =>
pipeline
.UseExceptionHandling()
.UseExecutionLogging()
.UseNotifications()
.UseBackgroundActivityInvoker());
}

View file

@ -0,0 +1,148 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Elsa.Common.Models;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Middleware.Activities;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Services;
using Microsoft.Extensions.Logging;
namespace Elsa.Workflows.Runtime.Services;
/// <summary>
/// An activity invoker that invokes activities detached from the workflow. This is useful for invoking activities from a background worker.
/// </summary>
public class DefaultBackgroundActivityInvoker : IBackgroundActivityInvoker
{
private readonly IWorkflowRuntime _workflowRuntime;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IVariablePersistenceManager _variablePersistenceManager;
private readonly IActivityInvoker _activityInvoker;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultBackgroundActivityInvoker"/> class.
/// </summary>
public DefaultBackgroundActivityInvoker(
IWorkflowRuntime workflowRuntime,
IWorkflowDispatcher workflowDispatcher,
IWorkflowDefinitionService workflowDefinitionService,
IVariablePersistenceManager variablePersistenceManager,
IActivityInvoker activityInvoker,
IServiceProvider serviceProvider,
ILogger<DefaultBackgroundActivityInvoker> logger)
{
_workflowRuntime = workflowRuntime;
_workflowDispatcher = workflowDispatcher;
_workflowDefinitionService = workflowDefinitionService;
_variablePersistenceManager = variablePersistenceManager;
_activityInvoker = activityInvoker;
_serviceProvider = serviceProvider;
_logger = logger;
}
/// <inheritdoc />
[RequiresUnreferencedCode("The activity type may be trimmed")]
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
{
var workflowInstanceId = scheduledBackgroundActivity.WorkflowInstanceId;
var workflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken);
if (workflowState == null)
throw new Exception("Workflow state not found");
var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(workflowState.DefinitionId, VersionOptions.SpecificVersion(workflowState.DefinitionVersion), cancellationToken);
if (workflowGraph == null)
throw new Exception("Workflow definition not found");
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_serviceProvider, workflowGraph, workflowState, cancellationTokens: cancellationToken);
var activityNodeId = scheduledBackgroundActivity.ActivityNodeId;
var activityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.NodeId == activityNodeId);
// Load persistent variables for the activity to use.
await _variablePersistenceManager.LoadVariablesAsync(workflowExecutionContext);
// Mark the activity as being invoked from a background worker.
activityExecutionContext.SetIsBackgroundExecution();
// Invoke the activity.
await _activityInvoker.InvokeAsync(activityExecutionContext);
// Capture any activity output produced by the activity (but only if the associated memory block is stored in the workflow itself).
var outputDescriptors = activityExecutionContext.ActivityDescriptor.Outputs;
var outputValues = new Dictionary<string, object>();
foreach (var outputDescriptor in outputDescriptors)
{
var output = (Output?)outputDescriptor.ValueGetter(activityExecutionContext.Activity);
if (output == null)
continue;
var memoryBlockReference = output.MemoryBlockReference();
if (!activityExecutionContext.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out var memoryBlock))
continue;
var variableMetadata = memoryBlock.Metadata as VariableBlockMetadata;
var driver = variableMetadata?.StorageDriverType;
// We only capture output written to the workflow itself. Other drivers like blob storage, etc. will be ignored since the foreground context will be loading those.
if (driver != typeof(WorkflowStorageDriver) && driver != typeof(WorkflowInstanceStorageDriver))
continue;
var outputValue = activityExecutionContext.Get(memoryBlockReference);
if (outputValue != null)
outputValues[outputDescriptor.Name] = outputValue;
}
// Resume the workflow, passing along activity output, outcomes and scheduled activities.
var bookmarkId = scheduledBackgroundActivity.BookmarkId;
var inputKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityOutputKey(activityNodeId);
var outcomesKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityOutcomesKey(activityNodeId);
var completedKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityCompletedKey(activityNodeId);
var journalDataKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityJournalDataKey(activityNodeId);
var bookmarksKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityBookmarksKey(activityNodeId);
var scheduledActivitiesKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityScheduledActivitiesKey(activityNodeId);
var outcomes = activityExecutionContext.GetBackgroundOutcomes()?.ToList();
var completed = activityExecutionContext.GetBackgroundCompleted();
var scheduledActivities = activityExecutionContext.GetBackgroundScheduledActivities().ToList();
var dispatchRequest = new DispatchWorkflowInstanceRequest
{
InstanceId = workflowInstanceId,
BookmarkId = bookmarkId,
Properties = new Dictionary<string, object>
{
[scheduledActivitiesKey] = JsonSerializer.Serialize(scheduledActivities),
[inputKey] = outputValues,
[journalDataKey] = activityExecutionContext.JournalData,
[bookmarksKey] = activityExecutionContext.Bookmarks.ToList()
}
};
if (outcomes != null)
dispatchRequest.Properties[outcomesKey] = outcomes;
if (completed != null)
dispatchRequest.Properties[completedKey] = completed;
if (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Background execution for activity {ActivityNodeId} was canceled", activityNodeId);
return;
}
await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken);
}
}

View file

@ -34,7 +34,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
var client = CreateClient();
client.BaseAddress = new Uri(client.BaseAddress!, "/elsa/api");
client.Timeout = TimeSpan.FromMinutes(1);
return RestService.For<TClient>(client, CreateRefitSettings());
return RestService.For<TClient>(client, CreateRefitSettings(Services));
}
public HttpClient CreateHttpWorkflowClient()

View file

@ -13,7 +13,7 @@ public class HelloWorldTests(App app) : AppComponentTest(app)
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync("1590068018aa4f0a");
var model = await response.ReadAsJsonAsync<Response>();
var model = await response.ReadAsJsonAsync<Response>(WorkflowServer.Services);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(WorkflowSubStatus.Finished, model.WorkflowState.SubStatus);
}

View file

@ -1,4 +1,4 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
using Elsa.Api.Client.Resources.WorkflowInstances.Models;
using Elsa.Common.Entities;
@ -65,7 +65,7 @@ public class InputOutputLoggingTests(App app) : AppComponentTest(app)
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync(workflowDefinitionId);
var model = await response.ReadAsJsonAsync<ExecuteResponse>();
var model = await response.ReadAsJsonAsync<Response>(WorkflowServer.Services);
return model.WorkflowState;
}

View file

@ -62,8 +62,8 @@ public class CountdownWorkflowTests(App app) : AppComponentTest(app)
}
}
private IDictionary<string, object> GetVariablesDictionary(ActivityExecutionContextState context)
private VariablesDictionary GetVariablesDictionary(ActivityExecutionContextState context)
{
return context.Properties.GetOrAdd(WorkflowStorageDriver.VariablesDictionaryStateKey, () => new Dictionary<string, object>());
return context.Properties.GetOrAdd(WorkflowInstanceStorageDriver.VariablesDictionaryStateKey, () => new VariablesDictionary());
}
}

View file

@ -15,7 +15,7 @@ public class WorkflowCompletionTests(App app) : AppComponentTest(app)
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync(workflowDefinitionId);
var model = await response.ReadAsJsonAsync<Response>();
var model = await response.ReadAsJsonAsync<Response>(WorkflowServer.Services);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(WorkflowSubStatus.Finished, model.WorkflowState.SubStatus);
}

View file

@ -32,14 +32,14 @@ public class SerializerUnicodeEncodingTests(ITestOutputHelper testOutputHelper)
}
[Fact]
public async Task TestSafeSerializer()
public void TestSafeSerializer()
{
var serializer = _serviceProvider.GetRequiredService<ISafeSerializer>();
await TestSerializerAsync(input => serializer.SerializeAsync(input).AsTask());
TestSerializer(input => serializer.Serialize(input));
}
[Fact]
public async Task TestWorkflowStateSerializer()
public void TestWorkflowStateSerializer()
{
var serializer = _serviceProvider.GetRequiredService<IWorkflowStateSerializer>();
TestSerializer(input => serializer.Serialize(input));
@ -56,18 +56,6 @@ public class SerializerUnicodeEncodingTests(ITestOutputHelper testOutputHelper)
var serializedStringValue = GetSerializedTextValue(serializedJson);
Assert.Equal(unicodeString, serializedStringValue);
}
private async Task TestSerializerAsync(Func<object, Task<string>> serialize)
{
var unicodeString = UnicodeRangeGenerator.GenerateUnicodeString();
var anonymousObject = new
{
Text = unicodeString
};
var serializedJson = await serialize(anonymousObject);
var serializedStringValue = GetSerializedTextValue(serializedJson);
Assert.Equal(unicodeString, serializedStringValue);
}
private string GetSerializedTextValue(string serializedJson)
{