Merge remote-tracking branch 'origin/blueberry' into enh/multi-tenancy-refactorings
This commit is contained in:
commit
ff487fad80
13
.github/workflows/packages.yml
vendored
13
.github/workflows/packages.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ElsaStudioVersion>3.3.0-preview.470</ElsaStudioVersion>
|
||||
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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>
|
||||
51
Elsa.sln
51
Elsa.sln
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
BIN
design/custom/2x/elsa-logo.png
Normal file
BIN
design/custom/2x/elsa-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -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"]
|
||||
|
|
@ -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:
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
command: -c 'max_connections=2000'
|
||||
|
|
|
|||
49
docker/otel-collector-config.yaml
Normal file
49
docker/otel-collector-config.yaml
Normal 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 ]
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
56
src/clients/Elsa.Api.Client/Converters/TypeJsonConverter.cs
Normal file
56
src/clients/Elsa.Api.Client/Converters/TypeJsonConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -11,8 +11,4 @@
|
|||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Extensions\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
6
src/modules/Elsa.Workflows.Core/Contracts/IComposite.cs
Normal file
6
src/modules/Elsa.Workflows.Core/Contracts/IComposite.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Workflows.Contracts;
|
||||
|
||||
public interface IComposite
|
||||
{
|
||||
void Setup();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"/>.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue