From 642e700c3ded4c9573e3f649b0ebbefd8bd8e9b4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 11 Jan 2024 21:00:35 +0100 Subject: [PATCH 01/21] Refactor MassTransitActivityTypeProvider (#4792) Fixed the Message input descriptor for dynamically provided MassTransit PublishMessage activities. --- .../Activities/PublishMessage.cs | 6 +- .../MassTransitActivityTypeProvider.cs | 68 ++++++++----------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs b/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs index e5972aa46..1802f3bd8 100644 --- a/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs +++ b/src/modules/Elsa.MassTransit/Activities/PublishMessage.cs @@ -6,6 +6,7 @@ using Elsa.MassTransit.Implementations; using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; using MassTransit; namespace Elsa.MassTransit.Activities; @@ -29,7 +30,10 @@ public class PublishMessage : CodeActivity /// /// The message to send. Must be a concrete implementation of the configured . /// - [Input(Description = "The message to send. Must be a concrete implementation of the configured message type.")] + [Input( + Description = "The message to send. Must be a concrete implementation of the configured message type.", + UIHint = InputUIHints.MultiLine + )] public Input Message { get; set; } = default!; /// diff --git a/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs b/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs index 75d9b32a6..e2dabe143 100644 --- a/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs +++ b/src/modules/Elsa.MassTransit/Implementations/MassTransitActivityTypeProvider.cs @@ -6,8 +6,8 @@ using Elsa.MassTransit.Options; using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.Contracts; -using Elsa.Workflows.UIHints; using Elsa.Workflows.Helpers; +using Elsa.Workflows.Management; using Elsa.Workflows.Models; using Humanizer; using Microsoft.Extensions.Options; @@ -17,40 +17,31 @@ namespace Elsa.MassTransit.Implementations; /// /// Provides activities to the system from the configured MassTransit message types. /// -public class MassTransitActivityTypeProvider : IActivityProvider +public class MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions options, IActivityDescriber activityDescriber) : IActivityProvider { - private readonly IActivityFactory _activityFactory; - private readonly MassTransitActivityOptions _options; - - /// - /// Constructor. - /// - public MassTransitActivityTypeProvider(IActivityFactory activityFactory, IOptions options) - { - _activityFactory = activityFactory; - _options = options.Value; - } - /// - public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) + public async ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) { - var messageTypes = _options.MessageTypes; - var descriptors = CreateDescriptors(messageTypes).ToList(); - return new(descriptors); + var messageTypes = options.Value.MessageTypes; + var descriptors = await CreateDescriptorsAsync(messageTypes, cancellationToken); + return descriptors.ToList(); } - private IEnumerable CreateDescriptors(IEnumerable messageTypes) + private async Task> CreateDescriptorsAsync(IEnumerable messageTypes, CancellationToken cancellationToken = default) { + var descriptors = new List(); foreach (var messageType in messageTypes) { - yield return CreateMessageReceivedDescriptor(messageType); + descriptors.Add(await CreateMessageReceivedDescriptor(messageType, cancellationToken)); if(messageType.IsClass) - yield return CreatePublishMessageDescriptor(messageType); + descriptors.Add(await CreatePublishMessageDescriptor(messageType, cancellationToken)); } + + return descriptors; } - private ActivityDescriptor CreateMessageReceivedDescriptor(Type messageType) + private async Task CreateMessageReceivedDescriptor(Type messageType, CancellationToken cancellationToken = default) { var activityAttr = messageType.GetCustomAttribute(); var typeName = activityAttr?.Type ?? messageType.Name; @@ -61,6 +52,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider var category = categoryAttr?.Category ?? activityAttr?.Category ?? "MassTransit"; var descriptionAttr = messageType.GetCustomAttribute(); var description = descriptionAttr?.Description ?? activityAttr?.Description; + + var outputDescriptor = await activityDescriber.DescribeOutputProperty(x => x.Result!, cancellationToken); + var openOutputType = typeof(Output<>); + var outputType = openOutputType.MakeGenericType(messageType); + outputDescriptor.Type = outputType; return new() { @@ -73,17 +69,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider IsBrowsable = true, Outputs = { - new OutputDescriptor - { - Description = "The received message", - DisplayName = "Received Message", - Name = nameof(MessageReceived.Result), - Type = typeof(object) - } + outputDescriptor }, Constructor = context => { - var activity = _activityFactory.Create(context); + var activity = activityFactory.Create(context); activity.Type = fullTypeName; activity.MessageType = messageType; return activity; @@ -91,7 +81,7 @@ public class MassTransitActivityTypeProvider : IActivityProvider }; } - private ActivityDescriptor CreatePublishMessageDescriptor(Type messageType) + private async Task CreatePublishMessageDescriptor(Type messageType, CancellationToken cancellationToken = default) { var activityAttr = messageType.GetCustomAttribute(); var typeName = activityAttr?.Type ?? messageType.Name; @@ -104,6 +94,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider var descriptionAttr = messageType.GetCustomAttribute(); var description = descriptionAttr?.Description ?? activityAttr?.Description; + var messageInputDescriptor = await activityDescriber.DescribeInputPropertyAsync(x => x.Message, cancellationToken: cancellationToken); + var openInputType = typeof(Input<>); + var inputType = openInputType.MakeGenericType(messageType); + messageInputDescriptor.Type = inputType; + return new() { TypeName = fullTypeName, @@ -115,18 +110,11 @@ public class MassTransitActivityTypeProvider : IActivityProvider IsBrowsable = true, Inputs = { - new InputDescriptor - { - Description = "The message to publish.", - UIHint = InputUIHints.MultiLine, - DisplayName = "Message", - Type = typeof(Input), - Name = nameof(PublishMessage.Message) - } + messageInputDescriptor }, Constructor = context => { - var activity = _activityFactory.Create(context); + var activity = activityFactory.Create(context); activity.Type = fullTypeName; activity.MessageType = messageType; return activity; From faa90fddf615b138cf789803ce3cb2204a2d329d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 11 Jan 2024 21:02:29 +0100 Subject: [PATCH 02/21] Update GitHub workflow for v3.0.3 The GitHub Actions workflow has been updated to accommodate for the new version v3.0.3. Changes include adjusting the branch references and VERSION variable in the packages.yml file. --- .github/workflows/packages.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index c0c28fcfe..29f163d5c 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - v3.0.2 + - v3.0.3 release: types: [ prereleased, published ] env: @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Verify commit exists in origin/v3.0.2 + - name: Verify commit exists in origin/v3.0.3 run: | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/v3.0.2 + git branch --remote --contains | grep origin/v3.0.3 - name: Set VERSION variable run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then @@ -29,7 +29,7 @@ jobs: TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV else - echo "VERSION=3.0.2-preview.${{github.run_number}}" >> $GITHUB_ENV + echo "VERSION=3.0.3-preview.${{github.run_number}}" >> $GITHUB_ENV fi - name: Build run: dotnet build --configuration Release /p:Version=${VERSION} From 66d8e4b20fda9575a5736475a4752c9ad1f68ea2 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 12 Jan 2024 08:47:41 +0100 Subject: [PATCH 03/21] Updated MassTransit sample project This commit updates the Elsa.Samples.AspNet.MassTransitActivities.csproj --- Elsa.sln | 36 +++++------ src/bundles/Elsa.Server.Web/Program.cs | 3 +- src/bundles/Elsa.Server.Web/appsettings.json | 2 +- .../Elsa.ServerAndStudio.Web.csproj | 1 + .../Elsa.ServerAndStudio.Web/Program.cs | 59 +++++++++++-------- .../Elsa.ServerAndStudio.Web/appsettings.json | 3 +- ...amples.AspNet.MassTransitActivities.csproj | 2 + .../Program.cs | 22 +++++-- 8 files changed, 77 insertions(+), 51 deletions(-) diff --git a/Elsa.sln b/Elsa.sln index 458554229..493d4da73 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -20,7 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D packages.props = packages.props README.md = README.md update-migrations.sh = update-migrations.sh - frameworks.props = frameworks.props + frameworks.props = frameworks.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}" @@ -253,47 +253,47 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.My EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj", "{84AB9951-3DBF-43A4-8F5D-AEED1102CE88}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.ServiceBus.IntegrationTests", "test\integration\Elsa.ServiceBus.IntegrationTests\Elsa.ServiceBus.IntegrationTests.csproj", "{9504E3F3-F77F-437A-8644-D23D7F5FCF8B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{B818988E-639C-4E6E-85C1-B231BCAD9DAB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{B789B743-0FBE-4BBC-9F45-84C20C8D4814}" ProjectSection(SolutionItems) = preProject .github\workflows\elsa-server-and-studio.yml = .github\workflows\elsa-server-and-studio.yml .github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml - .github\workflows\packages.yml = .github\workflows\packages.yml + .github\workflows\packages.yml = .github\workflows\packages.yml .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}" EndProject @@ -864,7 +864,7 @@ Global {05F3B23E-CF28-467B-AD3C-595EA4ED6B96} = {AB797AF0-C12C-46DE-A157-7E25625C6200} {74ACD9D0-8B4C-42FA-A582-E93BF0075023} = {AB797AF0-C12C-46DE-A157-7E25625C6200} {CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D} = {AB797AF0-C12C-46DE-A157-7E25625C6200} - {B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {732BF088-6AD7-4C4D-9A48-8074253596D4} = {B818988E-639C-4E6E-85C1-B231BCAD9DAB} {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {790E94F2-5393-47DF-AC52-D9247F5B243A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} diff --git a/src/bundles/Elsa.Server.Web/Program.cs b/src/bundles/Elsa.Server.Web/Program.cs index 7df6fea32..89ee7b6b3 100644 --- a/src/bundles/Elsa.Server.Web/Program.cs +++ b/src/bundles/Elsa.Server.Web/Program.cs @@ -10,7 +10,6 @@ using Elsa.EntityFrameworkCore.Modules.Identity; using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; using Elsa.Extensions; -using Elsa.Http.Handlers; using Elsa.Http.Options; using Elsa.MongoDb.Extensions; using Elsa.MongoDb.Modules.Identity; @@ -29,7 +28,7 @@ const bool useHangfire = false; const bool useQuartz = true; const bool useMassTransit = true; const bool useMassTransitAzureServiceBus = false; -const bool useMassTransitRabbitMq = false; +const bool useMassTransitRabbitMq = true; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; diff --git a/src/bundles/Elsa.Server.Web/appsettings.json b/src/bundles/Elsa.Server.Web/appsettings.json index 9859fc4f8..131ccbe59 100644 --- a/src/bundles/Elsa.Server.Web/appsettings.json +++ b/src/bundles/Elsa.Server.Web/appsettings.json @@ -17,7 +17,7 @@ "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", "MongoDb": "mongodb://localhost:27017/elsa-workflows", "AzureServiceBus": "", - "RabbitMq": "amqp://guest:guest@localhost:5672/elsa" + "RabbitMq": "amqp://guest:guest@localhost:5672" }, "Smtp": { "Host": "localhost", diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj index 40a7047bc..659bc16ef 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj +++ b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj @@ -18,6 +18,7 @@ + diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs index eb19d40ac..3726828da 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs @@ -6,6 +6,8 @@ using Elsa.Extensions; using Elsa.Webhooks.Extensions; using Microsoft.AspNetCore.Mvc; +const bool useMassTransit = true; + var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseStaticWebAssets(); var services = builder.Services; @@ -16,32 +18,39 @@ var identityTokenSection = identitySection.GetSection("Tokens"); // Add Elsa services. services - .AddElsa(elsa => elsa - .UseSasTokens() - .UseIdentity(identity => + .AddElsa(elsa => + { + elsa + .UseSasTokens() + .UseIdentity(identity => + { + identity.IdentityOptions = options => identitySection.Bind(options); + identity.TokenOptions = options => identityTokenSection.Bind(options); + identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options)); + identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options)); + identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options)); + }) + .UseDefaultAuthentication() + .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) + .UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) + .UseScheduling() + .UseJavaScript(options => options.AllowClrAccess = true) + .UseLiquid() + .UseCSharp() + .UsePython() + .UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options)) + .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) + .UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options)) + .UseWorkflowsApi() + .UseRealTimeWorkflows() + .AddActivitiesFrom() + .AddWorkflowsFrom(); + + if (useMassTransit) { - identity.IdentityOptions = options => identitySection.Bind(options); - identity.TokenOptions = options => identityTokenSection.Bind(options); - identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options)); - identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options)); - identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options)); - }) - .UseDefaultAuthentication() - .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) - .UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))) - .UseScheduling() - .UseJavaScript(options => options.AllowClrAccess = true) - .UseLiquid() - .UseCSharp() - .UsePython() - .UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options)) - .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) - .UseWebhooks(webhooks => webhooks.WebhookOptions = options => builder.Configuration.GetSection("Webhooks").Bind(options)) - .UseWorkflowsApi() - .UseRealTimeWorkflows() - .AddActivitiesFrom() - .AddWorkflowsFrom() - ); + elsa.UseMassTransit(); + } + }); services.AddHealthChecks(); diff --git a/src/bundles/Elsa.ServerAndStudio.Web/appsettings.json b/src/bundles/Elsa.ServerAndStudio.Web/appsettings.json index 6a427b4da..4f0c9d1bc 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/bundles/Elsa.ServerAndStudio.Web/appsettings.json @@ -13,7 +13,8 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;" + "Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;", + "RabbitMq": "rabbitmq://guest:guest@localhost:5672" }, "Hosting": { "BasePath": "" diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Elsa.Samples.AspNet.MassTransitActivities.csproj b/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Elsa.Samples.AspNet.MassTransitActivities.csproj index 6406236ea..00da2f741 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Elsa.Samples.AspNet.MassTransitActivities.csproj +++ b/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Elsa.Samples.AspNet.MassTransitActivities.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Program.cs b/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Program.cs index b68fa11ab..efc6b8bc6 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.AspNet.MassTransitActivities/Program.cs @@ -28,9 +28,23 @@ builder.Services.AddElsa(elsa => // Add services for HTTP activities and workflow middleware. elsa.UseHttp(); + + // Use C#. + elsa.UseCSharp(options => + { + options.Assemblies.Add(typeof(OrderCreated).Assembly); + options.Namespaces.Add(typeof(OrderCreated).Namespace!); + }); - // Use JavaScript and Liquid. - elsa.UseJavaScript(); + // Use JavaScript. + elsa.UseJavaScript(options => + { + options.AllowClrAccess = true; + options.RegisterType(); + options.RegisterType(); + }); + + // Use Liquid. elsa.UseLiquid(); // Configure identity so that we can create a default admin user. @@ -39,7 +53,7 @@ builder.Services.AddElsa(elsa => identity.UseAdminUserProvider(); identity.TokenOptions = options => { - options.SigningKey = "secret-token-signing-key"; + options.SigningKey = "super-secret-and-securely-stored-token-signing-key"; options.AccessTokenLifetime = TimeSpan.FromDays(1); }; }); @@ -50,7 +64,7 @@ builder.Services.AddElsa(elsa => // Configure MassTransit. elsa.UseMassTransit(massTransit => { - massTransit.UseRabbitMq(rabbitMqConnectionString); + //massTransit.UseRabbitMq(rabbitMqConnectionString); massTransit.AddMessageType(); massTransit.AddMessageType(); }); From ac9428a6e0ed6fbdc63e29514f05cb924fc165a9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 15 Jan 2024 21:15:05 +0100 Subject: [PATCH 04/21] Allow creation of variables dynamically via `setVariable` (#4800) * Disable MassTransitRabbitMq in Elsa.Server.Web. * Create variable if not exists in Workflow.Core The update introduced checks if a variable exists before attempting to set its value in the ExpressionExecutionContextExtensions. If the variable doesn't exist, it now creates one automatically. Fixes #4789 * Remove unnecessary comment The comment indicating to create a variable if it doesn't exist was deemed superfluous and removed from the ExpressionExecutionContextExtensions.cs file. This improves code cleanliness as the functionality is self-explanatory through the existing code structure. --- src/bundles/Elsa.Server.Web/Program.cs | 2 +- .../Extensions/ExpressionExecutionContextExtensions.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/bundles/Elsa.Server.Web/Program.cs b/src/bundles/Elsa.Server.Web/Program.cs index 89ee7b6b3..1c35e86c8 100644 --- a/src/bundles/Elsa.Server.Web/Program.cs +++ b/src/bundles/Elsa.Server.Web/Program.cs @@ -28,7 +28,7 @@ const bool useHangfire = false; const bool useQuartz = true; const bool useMassTransit = true; const bool useMassTransitAzureServiceBus = false; -const bool useMassTransitRabbitMq = true; +const bool useMassTransitRabbitMq = false; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index cb9dd853c..b93272793 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -292,7 +292,12 @@ public static class ExpressionExecutionContextExtensions select v; var variable = q.FirstOrDefault(); - variable?.Set(context, value); + + if(variable != null) + variable.Set(context, value); + + if (variable == null) + CreateVariable(context, variableName, value); } /// From 4912726ea08d4466f1d9e40fcdc3418f9bc52fb9 Mon Sep 17 00:00:00 2001 From: Night Wu Date: Fri, 19 Jan 2024 19:50:30 +0000 Subject: [PATCH 05/21] Fix InvalidOperation Exception for SaveVariablesAsync function (#4808) Co-authored-by: admin --- .../Services/VariablePersistenceManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs index 3ce3ed598..0bd89f183 100644 --- a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs +++ b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs @@ -61,7 +61,7 @@ public class VariablePersistenceManager : IVariablePersistenceManager foreach (var context in contexts) { - var variables = GetLocalVariables(context); + var variables = GetLocalVariables(context).ToList(); var storageDriverContext = new StorageDriverContext(context, cancellationToken); foreach (var variable in variables) @@ -89,7 +89,7 @@ public class VariablePersistenceManager : IVariablePersistenceManager public async Task DeleteVariablesAsync(ActivityExecutionContext context) { var register = context.ExpressionExecutionContext.Memory; - var variableList = GetLocalVariables(context); + var variableList = GetLocalVariables(context).ToList(); var cancellationToken = context.CancellationToken; var storageDriverContext = new StorageDriverContext(context, cancellationToken); From c5e6213fbfca6a71590f5b8b7912ec0795f610d6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 19 Jan 2024 23:27:27 +0100 Subject: [PATCH 06/21] Remove Elsa.Studio.Web from solution A duplicate Elsa.Studio.Web project reference has been removed from the Elsa solution. --- Elsa.sln | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Elsa.sln b/Elsa.sln index 5af513753..f90044f8e 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -304,8 +304,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}" EndProject Global @@ -742,10 +740,6 @@ Global {26888832-DBEA-4B23-8DC2-84478A702CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {26888832-DBEA-4B23-8DC2-84478A702CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {26888832-DBEA-4B23-8DC2-84478A702CC4}.Release|Any CPU.Build.0 = Release|Any CPU - {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Release|Any CPU.Build.0 = Release|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -882,7 +876,6 @@ Global {169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6} = {DD089B8B-DA73-492A-9010-F772D1C178DA} {5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A} = {DD089B8B-DA73-492A-9010-F772D1C178DA} {26888832-DBEA-4B23-8DC2-84478A702CC4} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} - {D5C149EE-276C-4C59-98F9-37F0EA2A7866} = {F06B9573-DF68-4606-866C-A7546A10A05A} {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From 5edda57c5b52235ccf27f28e1e27b744bddb2a23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jan 2024 10:45:34 +0100 Subject: [PATCH 07/21] Bump System.Data.SqlClient in /src/modules/Elsa.Dapper (#4805) Bumps [System.Data.SqlClient](https://github.com/dotnet/corefx) from 4.8.5 to 4.8.6. - [Release notes](https://github.com/dotnet/corefx/releases) - [Commits](https://github.com/dotnet/corefx/commits) --- updated-dependencies: - dependency-name: System.Data.SqlClient dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/modules/Elsa.Dapper/Elsa.Dapper.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj index 6f25c445b..714945f04 100644 --- a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj +++ b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj @@ -32,7 +32,7 @@ - + From 00331f7497feb08ad39cb730f53e6a8085b08406 Mon Sep 17 00:00:00 2001 From: Mohamed Ali Date: Sun, 21 Jan 2024 12:52:46 +0300 Subject: [PATCH 08/21] Allow multiple value processing for Form and QueryString values. (#4736) * Allow multiple value processing for Form and QueryString values. * refactor HTTPEndpoint * add multi-part form handling * cleanup * reoder form handling code --- .../Elsa.Http/Activities/HttpEndpoint.cs | 88 +++++++++++-------- .../Extensions/StringValueExtensions.cs | 28 ++++++ src/modules/Elsa.Http/Features/HttpFeature.cs | 10 +-- .../Parsers/FormHttpContentParser.cs | 28 ------ .../Parsers/StringHttpContentParser.cs | 24 ----- .../JsonWorkflowStateSerializer.cs | 16 ++-- 6 files changed, 90 insertions(+), 104 deletions(-) create mode 100644 src/modules/Elsa.Http/Extensions/StringValueExtensions.cs delete mode 100644 src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs delete mode 100644 src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index 1fdd7b0a1..6630d693b 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -211,8 +211,8 @@ public class HttpEndpoint : Trigger var routeData = GetRouteData(httpContext, path); var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!); - var queryStringDictionary = httpContext.Request.Query.ToDictionary, string, object>(queryString => queryString.Key, queryString => queryString.Value[0]!); - var headersDictionary = httpContext.Request.Headers.ToDictionary, string, object>(header => header.Key, header => header.Value[0]!); + var queryStringDictionary = httpContext.Request.Query.ToObjectDictionary(); + var headersDictionary = httpContext.Request.Headers.ToObjectDictionary(); context.Set(RouteData, routeDictionary); context.Set(QueryStringData, queryStringDictionary); @@ -225,48 +225,60 @@ public class HttpEndpoint : Trigger return; } - // Read content, if any. - try + // Handle Form Fields + if (request.HasFormContentType) { - var content = await ParseContentAsync(context, request); - ParsedContent.Set(context, content); + + var formFields = request.Form.ToObjectDictionary(); + + ParsedContent.Set(context, formFields); + + // Read files, if any. + var files = ReadFilesAsync(context, request); + + if (files.Any()) + { + if (!ValidateFileSizes(context, httpContext, files)) + { + await HandleFileSizeTooLargeAsync(context, httpContext); + return; + } + + if (!ValidateFileExtensionWhitelist(context, httpContext, files)) + { + await HandleInvalidFileExtensionWhitelistAsync(context, httpContext); + return; + } + + if (!ValidateFileExtensionBlacklist(context, httpContext, files)) + { + await HandleInvalidFileExtensionBlacklistAsync(context, httpContext); + return; + } + + if (!ValidateFileMimeTypes(context, httpContext, files)) + { + await HandleInvalidFileMimeTypesAsync(context, httpContext); + return; + } + + Files.Set(context, files.ToArray()); + } } - catch (JsonException e) + else { - await HandleInvalidJsonPayloadAsync(context, httpContext, e); - throw; - } - - // Read files, if any. - var files = ReadFilesAsync(context, request); - - if (files.Any()) - { - if (!ValidateFileSizes(context, httpContext, files)) + // Parse Non-Form content. + try { - await HandleFileSizeTooLargeAsync(context, httpContext); - return; + var content = await ParseContentAsync(context, request); + ParsedContent.Set(context, content); + } + catch (JsonException e) + { + await HandleInvalidJsonPayloadAsync(context, httpContext, e); + throw; } - if (!ValidateFileExtensionWhitelist(context, httpContext, files)) - { - await HandleInvalidFileExtensionWhitelistAsync(context, httpContext); - return; - } - - if (!ValidateFileExtensionBlacklist(context, httpContext, files)) - { - await HandleInvalidFileExtensionBlacklistAsync(context, httpContext); - return; - } - - if (!ValidateFileMimeTypes(context, httpContext, files)) - { - await HandleInvalidFileMimeTypesAsync(context, httpContext); - return; - } - - Files.Set(context, files.ToArray()); } // Complete. diff --git a/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs b/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs new file mode 100644 index 000000000..3b7948c1e --- /dev/null +++ b/src/modules/Elsa.Http/Extensions/StringValueExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Extensions; + +/// +/// Contains extension methods for the Enumerable dictonaries. +/// +public static class StringValueExtensions +{ + /// + /// Convert the collection to the desired dictionary type. + /// + /// + /// + public static Dictionary ToObjectDictionary(this IEnumerable> collection) + { + return collection.ToDictionary, string, object>( + item => item.Key, + item => item.Value.Count <= 1 ? + item.Value[0]! + : item.Value.ToArray()); + } +} diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 796eddc66..ece6c375c 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -158,12 +158,10 @@ public class HttpFeature : FeatureBase .AddRequestHandler() .AddNotificationHandler() - // Content parsers. - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - + // Content parsers. + .AddSingleton() + .AddSingleton() + // HTTP content factories. .AddScoped() .AddScoped() diff --git a/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs b/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs deleted file mode 100644 index 315ccc01b..000000000 --- a/src/modules/Elsa.Http/Parsers/FormHttpContentParser.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Xml.Serialization; -using Elsa.Extensions; -using Elsa.Http.Contracts; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Primitives; - -namespace Elsa.Http.Parsers; - -/// -/// Reads application/xml and text/xml content type streams. -/// -public class FormHttpContentParser : IHttpContentParser -{ - /// - public int Priority => 0; - - /// - public bool GetSupportsContentType(string contentType) => contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.InvariantCultureIgnoreCase); - - /// - public async Task ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken) - { - using var reader = new FormReader(content); - - return (await reader.ReadFormAsync(cancellationToken)).ToDictionary, string, object>(formField => formField.Key, formField => formField.Value[0]!); ; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs b/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs deleted file mode 100644 index 23fb9caf6..000000000 --- a/src/modules/Elsa.Http/Parsers/StringHttpContentParser.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Elsa.Expressions.Helpers; -using Elsa.Http.Contracts; - -namespace Elsa.Http.Parsers; - -/// -/// Reads any content type streams as a string and attempts to convert the string to the specified return type. -/// -public class StringHttpContentParser : IHttpContentParser -{ - /// - public int Priority => -10; - - /// - public bool GetSupportsContentType(string contentType) => !contentType.StartsWith("multipart/form-data"); - - /// - public async Task ReadAsync(Stream content, Type? returnType, CancellationToken cancellationToken) - { - using var reader = new StreamReader(content, leaveOpen: true); - var text = await reader.ReadToEndAsync(); - return returnType == null ? text : text.ConvertTo(returnType)!; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index 8145e7a98..c09642d94 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -36,8 +36,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.Serialize(workflowState, options); } @@ -46,8 +46,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.SerializeToUtf8Bytes(workflowState, options); } @@ -56,8 +56,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer { var options = GetSerializerOptions(); var serializingWorkflowState = new SerializingWorkflowState(options); - await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); - + await _notificationSender.SendAsync(serializingWorkflowState, cancellationToken); + return JsonSerializer.SerializeToElement(workflowState, options); } @@ -110,8 +110,8 @@ public class JsonWorkflowStateSerializer : IWorkflowStateSerializer options.Converters.Add(JsonMetadataServices.TimeSpanConverter); options.Converters.Add(new PolymorphicObjectConverterFactory()); options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry)); - options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); - + options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); + return options; } } \ No newline at end of file From 832fac4db17f86d37b05e7ba129c5a969a84b10a Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 21 Jan 2024 11:53:07 +0200 Subject: [PATCH 09/21] Add NUKE build support (#4815) --- .github/workflows/packages.yml | 8 +-- .github/workflows/pr.yml | 44 ++++++++++++ .gitignore | 6 +- .nuke/build.schema.json | 120 ++++++++++++++++++++++++++++++++ .nuke/parameters.json | 4 ++ Elsa.sln | 53 +++++++------- build.cmd | 7 ++ build.ps1 | 74 ++++++++++++++++++++ build.sh | 67 ++++++++++++++++++ build/.editorconfig | 11 +++ build/Build.CI.GitHubActions.cs | 56 +++++++++++++++ build/Build.cs | 69 ++++++++++++++++++ build/Configuration.cs | 16 +++++ build/Directory.Build.props | 7 ++ build/Directory.Build.targets | 8 +++ build/_build.csproj | 22 ++++++ build/_build.csproj.DotSettings | 28 ++++++++ 17 files changed, 566 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/pr.yml create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json create mode 100755 build.cmd create mode 100644 build.ps1 create mode 100755 build.sh create mode 100644 build/.editorconfig create mode 100644 build/Build.CI.GitHubActions.cs create mode 100644 build/Build.cs create mode 100644 build/Configuration.cs create mode 100644 build/Directory.Build.props create mode 100644 build/Directory.Build.targets create mode 100644 build/_build.csproj create mode 100644 build/_build.csproj.DotSettings diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 1a24c9b44..1cab3083a 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -31,12 +31,8 @@ jobs: else echo "VERSION=3.1.0-preview.${{github.run_number}}" >> $GITHUB_ENV fi - - name: Build - run: dotnet build --configuration Release /p:Version=${VERSION} - - name: Test - run: dotnet test --configuration Release /p:Version=${VERSION} --no-build - - name: Pack - run: dotnet pack --configuration Release /p:Version=${VERSION} /p:PackageOutputPath=$(pwd)/packages + - name: Compile+Test+Pack + run: ./build.sh Compile+Test+Pack --version ${VERSION} - name: Upload artifact uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 000000000..6d7547a43 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [CustomGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_pr --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: pr + +on: + pull_request: + branches: + - main + - 'v3*' + paths: + - '**/*' + - '!docs/**/*' + - '!package.json' + - '!package-lock.json' + - '!readme.md' + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - if: ${{ runner.os == 'Windows' }} + name: 'Use GNU tar' + shell: cmd + run: | + echo "Adding GNU tar to PATH" + echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + - uses: actions/checkout@v3 + - name: 'Run: Compile, Test, Pack' + run: ./build.cmd Compile Test Pack diff --git a/.gitignore b/.gitignore index 68b95e154..f772d8ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -51,9 +51,6 @@ node_modules/ .stylelintrc.yml .eslintrc.yml -#github -.github/ - #volatile App_Data/ *.db @@ -76,3 +73,6 @@ src/designer/elsa-workflows-designer/loader src/modules/Elsa.Workflows.Designer/package-lock.json unlist.sh + +# build artifacts +/artifacts diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 000000000..3f9224ea7 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "IgnoreFailedSources": { + "type": "boolean", + "description": "Ignore unreachable sources during Restore" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Pack", + "Restore", + "Test" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Pack", + "Restore", + "Test" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + }, + "Version": { + "type": "string" + } + } + } + } +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 000000000..8535901f9 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Elsa.sln" +} diff --git a/Elsa.sln b/Elsa.sln index f90044f8e..501605779 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34003.232 @@ -16,12 +17,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D common.props = common.props configureawait.props = configureawait.props docker-compose.yml = docker-compose.yml + frameworks.props = frameworks.props icon.png = icon.png NuGet.Config = NuGet.Config packages.props = packages.props README.md = README.md update-migrations.sh = update-migrations.sh - frameworks.props = frameworks.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}" @@ -254,57 +255,52 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.My EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj", "{84AB9951-3DBF-43A4-8F5D-AEED1102CE88}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Alterations.MassTransit", "src\modules\Elsa.Alterations.MassTransit\Elsa.Alterations.MassTransit.csproj", "{1F621382-08FB-45EA-B94C-96A7FECB9489}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.UnitTests", "test\unit\Elsa.Activities.UnitTests\Elsa.Activities.UnitTests.csproj", "{E6562B0F-AF64-472A-B009-BF6D40DDE99B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.ServiceBus.IntegrationTests", "test\integration\Elsa.ServiceBus.IntegrationTests\Elsa.ServiceBus.IntegrationTests.csproj", "{9504E3F3-F77F-437A-8644-D23D7F5FCF8B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaStudioWebAssembly", "src\bundles\ElsaStudioWebAssembly\ElsaStudioWebAssembly.csproj", "{E12A1BDF-5D65-493B-835D-AFBC480A5650}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.EntityFrameworkCore.Common", "src\modules\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj", "{FBA14F8A-9F14-4364-ABDB-B8AE81A0D755}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Quartz.EntityFrameworkCore.Sqlite\Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj", "{A71557F2-D5FB-4772-9391-F7D18A3B6C44}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.SqlServer", "src\modules\Elsa.Quartz.EntityFrameworkCore.SqlServer\Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj", "{05F3B23E-CF28-467B-AD3C-595EA4ED6B96}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.MySql", "src\modules\Elsa.Quartz.EntityFrameworkCore.MySql\Elsa.Quartz.EntityFrameworkCore.MySql.csproj", "{74ACD9D0-8B4C-42FA-A582-E93BF0075023}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Quartz.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.Quartz.EntityFrameworkCore.PostgreSql\Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj", "{CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.FileStorage", "src\modules\Elsa.FileStorage\Elsa.FileStorage.csproj", "{732BF088-6AD7-4C4D-9A48-8074253596D4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{B818988E-639C-4E6E-85C1-B231BCAD9DAB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.CSharp", "src\modules\Elsa.CSharp\Elsa.CSharp.csproj", "{24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Python", "src\modules\Elsa.Python\Elsa.Python.csproj", "{790E94F2-5393-47DF-AC52-D9247F5B243A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{B789B743-0FBE-4BBC-9F45-84C20C8D4814}" ProjectSection(SolutionItems) = preProject .github\workflows\elsa-server-and-studio.yml = .github\workflows\elsa-server-and-studio.yml .github\workflows\elsa-server.yml = .github\workflows\elsa-server.yml - .github\workflows\packages.yml = .github\workflows\packages.yml .github\workflows\elsa-studio.yml = .github\workflows\elsa-studio.yml + .github\workflows\packages.yml = .github\workflows\packages.yml + .github\workflows\pr.yml = .github\workflows\pr.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Studio.Web", "src\bundles\Elsa.Studio.Web\Elsa.Studio.Web.csproj", "{D5C149EE-276C-4C59-98F9-37F0EA2A7866}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.RabbitMq", "src\modules\Elsa.MassTransit.RabbitMq\Elsa.MassTransit.RabbitMq.csproj", "{169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.MassTransit.AzureServiceBus", "src\modules\Elsa.MassTransit.AzureServiceBus\Elsa.MassTransit.AzureServiceBus.csproj", "{5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.AspNet.Heartbeats", "src\samples\aspnet\Elsa.Samples.AspNet.Heartbeats\Elsa.Samples.AspNet.Heartbeats.csproj", "{26888832-DBEA-4B23-8DC2-84478A702CC4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.AspNet.CustomUIHandler", "src\samples\aspnet\Elsa.Samples.AspNet.CustomUIHandler\Elsa.Samples.AspNet.CustomUIHandler.csproj", "{832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -740,10 +736,16 @@ Global {26888832-DBEA-4B23-8DC2-84478A702CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {26888832-DBEA-4B23-8DC2-84478A702CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {26888832-DBEA-4B23-8DC2-84478A702CC4}.Release|Any CPU.Build.0 = Release|Any CPU + {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5C149EE-276C-4C59-98F9-37F0EA2A7866}.Release|Any CPU.Build.0 = Release|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A}.Release|Any CPU.Build.0 = Release|Any CPU + {99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -869,13 +871,14 @@ Global {05F3B23E-CF28-467B-AD3C-595EA4ED6B96} = {AB797AF0-C12C-46DE-A157-7E25625C6200} {74ACD9D0-8B4C-42FA-A582-E93BF0075023} = {AB797AF0-C12C-46DE-A157-7E25625C6200} {CCCCEF8C-7D96-4BEA-B9D0-E91EDF08E65D} = {AB797AF0-C12C-46DE-A157-7E25625C6200} - {B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {732BF088-6AD7-4C4D-9A48-8074253596D4} = {B818988E-639C-4E6E-85C1-B231BCAD9DAB} + {B818988E-639C-4E6E-85C1-B231BCAD9DAB} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {24331E82-D7AF-45B1-ACF0-CA6C3B0B77DC} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {790E94F2-5393-47DF-AC52-D9247F5B243A} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {169BEA3D-2A81-47EE-A6C1-3F8719EEC1F6} = {DD089B8B-DA73-492A-9010-F772D1C178DA} {5B1AF00E-1030-4BBA-9C1A-1DC5AEFDD44A} = {DD089B8B-DA73-492A-9010-F772D1C178DA} {26888832-DBEA-4B23-8DC2-84478A702CC4} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {D5C149EE-276C-4C59-98F9-37F0EA2A7866} = {F06B9573-DF68-4606-866C-A7546A10A05A} {832D6FF5-16D3-41D1-8FDD-D9BEC26BDA6A} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/build.cmd b/build.cmd new file mode 100755 index 000000000..b08cc590f --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..4634dc03e --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_NOLOGO = 1 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..fdff0c623 --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 000000000..31e43dcd8 --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs new file mode 100644 index 000000000..e317e2222 --- /dev/null +++ b/build/Build.CI.GitHubActions.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.CI.GitHubActions.Configuration; +using Nuke.Common.Execution; +using Nuke.Common.Utilities; +using Nuke.Components; + +[CustomGitHubActions( + "pr", + GitHubActionsImage.UbuntuLatest, + OnPullRequestBranches = ["main", "v3*"], + OnPullRequestIncludePaths = ["**/*"], + OnPullRequestExcludePaths = ["docs/**/*", "package.json", "package-lock.json", "readme.md"], + PublishArtifacts = false, + InvokedTargets = [nameof(ICompile.Compile), nameof(ITest.Test), nameof(IPack.Pack)], + CacheKeyFiles = [] + ) +] +public partial class Build; + +class CustomGitHubActionsAttribute : GitHubActionsAttribute +{ + public CustomGitHubActionsAttribute(string name, GitHubActionsImage image, params GitHubActionsImage[] images) : base(name, image, images) + { + } + + protected override GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCollection relevantTargets) + { + var job = base.GetJobs(image, relevantTargets); + + var newSteps = new List(job.Steps); + newSteps.Insert(0, new GitHubActionsUseGnuTarStep()); + + job.Steps = newSteps.ToArray(); + return job; + } +} + +class GitHubActionsUseGnuTarStep : GitHubActionsStep +{ + public override void Write(CustomFileWriter writer) + { + writer.WriteLine("- if: ${{ runner.os == 'Windows' }}"); + using (writer.Indent()) + { + writer.WriteLine("name: 'Use GNU tar'"); + writer.WriteLine("shell: cmd"); + writer.WriteLine("run: |"); + using (writer.Indent()) + { + writer.WriteLine("echo \"Adding GNU tar to PATH\""); + writer.WriteLine("echo C:\\Program Files\\Git\\usr\\bin>>\"%GITHUB_PATH%\""); + } + } + } +} \ No newline at end of file diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 000000000..c3f644925 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nuke.Common; +using Nuke.Common.CI; +using Nuke.Common.Git; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Utilities.Collections; +using Nuke.Components; +using Serilog; + +[ShutdownDotNetAfterServerBuild] +partial class Build : NukeBuild, ITest, IPack +{ + public static int Main() => Execute(x => ((ICompile) x).Compile); + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [GitRepository] readonly GitRepository GitRepository; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + + public AbsolutePath PackagesDirectory => RootDirectory / "packages"; + + string TagVersion => GitRepository.Tags.SingleOrDefault(x => "v".StartsWith(x))?[1..]; + bool IsTaggedBuild => !string.IsNullOrWhiteSpace(TagVersion); + + string VersionSuffix; + + [Parameter] + string Version; + + protected override void OnBuildInitialized() + { + VersionSuffix = !IsTaggedBuild + ? $"preview-{DateTime.UtcNow:yyyyMMdd-HHmm}" + : ""; + + if (IsLocalBuild) + { + VersionSuffix = $"dev-{DateTime.UtcNow:yyyyMMdd-HHmm}"; + } + + Log.Information("BUILD SETUP"); + Log.Information("Configuration:\t{Configuration}", Configuration); + Log.Information("Version suffix:\t{VersionSuffix}", VersionSuffix); + Log.Information("Version:\t\t{Version}", Version); + Log.Information("Tagged build:\t{IsTaggedBuild}", IsTaggedBuild); + } + + Target Clean => _ => _ + .Before(x => x.Restore) + .Executes(() => + { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(x => x.DeleteDirectory()); + ((IHazArtifacts) this).ArtifactsDirectory.CreateOrCleanDirectory(); + }); + + public Configure CompileSettings => _ => _ + // ensure we don't generate too much output in CI run + // 0 Turns off emission of all warning messages + // 1 Displays severe warning messages + .SetWarningLevel(IsServerBuild ? 0 : 1); + + public IEnumerable TestProjects => ((IHazSolution) this).Solution.AllProjects.Where(x => x.Name.EndsWith("Tests")); +} \ No newline at end of file diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 000000000..9c08b1ae9 --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/Directory.Build.props b/build/Directory.Build.props new file mode 100644 index 000000000..61fd9f44b --- /dev/null +++ b/build/Directory.Build.props @@ -0,0 +1,7 @@ + + + + false + + + \ No newline at end of file diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets new file mode 100644 index 000000000..253260956 --- /dev/null +++ b/build/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 000000000..cdd7e0336 --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + + CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 + .. + .. + 1 + true + true + false + true + false + + + + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 000000000..eb3f4c27c --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,28 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True From c409414152e478d7580c30071c64edbb2a885ca6 Mon Sep 17 00:00:00 2001 From: raymonddenhaan <155616759+raymonddenhaan@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:05:03 +0100 Subject: [PATCH 10/21] Workflow cancellation (#4813) * Removed duplicate entries * Prevented workflows and activities from starting when the parent workflow is being cancelled * Added cancellation to execution contexts * Added store for workflow execution contexts * Added cancellation to workflowRuntime * Removed calling BookmarkPersistedHandler when persisting bookmarks. * Added endpoint for bulk cancelling tasks * Added tests for cancelling workflows * Prevented cancelling the cancellation process since it could have unwanted effects --------- Co-authored-by: Sipke Schoorstra --- Elsa.sln | 2 +- .../ScheduleActivityHandler.cs | 2 +- .../Grains/WorkflowInstance.cs | 60 ++++++++- .../Proto/WorkflowInstance.proto | 1 + .../Services/ProtoActorWorkflowRuntime.cs | 10 +- .../BulkDelete/Endpoint.cs | 2 +- .../WorkflowInstances/BulkCancel/Endpoint.cs | 21 +-- .../ActivityExecutionContext.Cancel.cs | 44 +++++++ ...ivityExecutionContext.ExecutionLogEntry.cs | 40 ++++++ .../Contexts/ActivityExecutionContext.cs | 22 +++- .../WorkflowExecutionContext.Cancel.cs | 37 ++++++ ...kflowExecutionContext.ExecutionLogEntry.cs | 36 +++++ .../Contexts/WorkflowExecutionContext.cs | 31 ++++- .../ActivityExecutionContextExtensions.cs | 44 +------ .../WorkflowExecutionContextExtensions.cs | 32 ----- .../DefaultActivityInvokerMiddleware.cs | 14 +- .../Activities/ExceptionHandlingMiddleware.cs | 2 +- ... => DefaultActivitySchedulerMiddleware.cs} | 4 + .../Options/RunWorkflowOptions.cs | 1 + .../Services/WorkflowRunner.cs | 4 + .../Services/WorkflowStateExtractor.cs | 2 +- .../Activities/BulkDispatchWorkflows.cs | 4 + .../Contracts/IBookmarkPersister.cs | 5 +- .../IWorkflowExecutionContextStore.cs | 24 ++++ .../Contracts/IWorkflowRuntime.cs | 7 + .../Features/WorkflowRuntimeFeature.cs | 10 +- ...lowExecutionContextNotificationsHandler.cs | 36 +++++ .../Workflows/PersistBookmarkMiddleware.cs | 4 +- .../Notifications/WorkflowBookmarksIndexed.cs | 2 +- .../WorkflowBookmarksPersisted.cs | 2 +- .../Options/StartWorkflowHostOptions.cs | 5 + .../Services/BookmarkPersister.cs | 24 +--- .../Services/DefaultWorkflowRuntime.cs | 76 ++++++++++- .../MemoryWorkflowExecutionContextStore.cs | 42 ++++++ .../Services/WorkflowHost.cs | 1 + .../Elsa.IntegrationTests.csproj | 2 + .../DefaultRuntimeTests.cs | 102 ++++++++++++++ .../WorkflowCancellation/ProtoActorTests.cs | 124 ++++++++++++++++++ .../Workflows/BulkSuspendedWorkflow.cs | 33 +++++ .../Workflows/ResumeDispatchWorkflow.cs | 21 +++ .../Workflows/SimpleChildWorkflow.cs | 19 +++ .../Workflows/SimpleSuspendedWorkflow.cs | 22 ++++ 42 files changed, 854 insertions(+), 122 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs create mode 100644 src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs create mode 100644 src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.Cancel.cs create mode 100644 src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.ExecutionLogEntry.cs rename src/modules/Elsa.Workflows.Core/Middleware/Workflows/{DefaultWorkSchedulerMiddleware.cs => DefaultActivitySchedulerMiddleware.cs} (91%) create mode 100644 src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowExecutionContextStore.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Handlers/WorkflowExecutionContextNotificationsHandler.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Services/MemoryWorkflowExecutionContextStore.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/ResumeDispatchWorkflow.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleChildWorkflow.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleSuspendedWorkflow.cs diff --git a/Elsa.sln b/Elsa.sln index 501605779..cee039477 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34003.232 diff --git a/src/modules/Elsa.Alterations/AlterationHandlers/ScheduleActivityHandler.cs b/src/modules/Elsa.Alterations/AlterationHandlers/ScheduleActivityHandler.cs index 43a05acb1..727267988 100644 --- a/src/modules/Elsa.Alterations/AlterationHandlers/ScheduleActivityHandler.cs +++ b/src/modules/Elsa.Alterations/AlterationHandlers/ScheduleActivityHandler.cs @@ -28,7 +28,7 @@ public class ScheduleActivityHandler : AlterationHandlerBase { // If the activity is in a faulted state, reset it to Running. if (existingActivityExecutionContext.Status == ActivityStatus.Faulted) - existingActivityExecutionContext.Status = ActivityStatus.Running; + existingActivityExecutionContext.TransitionTo(ActivityStatus.Running); // Schedule the activity execution context. var parentContext = existingActivityExecutionContext.ParentActivityExecutionContext; diff --git a/src/modules/Elsa.ProtoActor/Grains/WorkflowInstance.cs b/src/modules/Elsa.ProtoActor/Grains/WorkflowInstance.cs index 0dd27ad33..21f680608 100644 --- a/src/modules/Elsa.ProtoActor/Grains/WorkflowInstance.cs +++ b/src/modules/Elsa.ProtoActor/Grains/WorkflowInstance.cs @@ -3,16 +3,22 @@ using Elsa.ProtoActor.Extensions; using Elsa.ProtoActor.Mappers; using Elsa.ProtoActor.ProtoBuf; using Elsa.ProtoActor.Snapshots; +using Elsa.Workflows; using Elsa.Workflows.Contracts; +using Elsa.Workflows.Helpers; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Runtime.Contracts; using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.State; using Microsoft.Extensions.DependencyInjection; using Proto; using Proto.Cluster; using Proto.Persistence; +using CancellationTokens = Elsa.Workflows.Models.CancellationTokens; +using WorkflowStatus = Elsa.Workflows.WorkflowStatus; +using WorkflowSubStatus = Elsa.Workflows.WorkflowSubStatus; namespace Elsa.ProtoActor.Grains; @@ -38,6 +44,8 @@ internal class WorkflowInstance : WorkflowInstanceBase private IWorkflowHost _workflowHost = default!; private WorkflowState _workflowState = default!; + private readonly ICollection _cancellationTokenSources = new List(); + /// public WorkflowInstance( IServiceScopeFactory scopeFactory, @@ -139,6 +147,10 @@ internal class WorkflowInstance : WorkflowInstanceBase var versionOptions = VersionOptions.FromString(request.VersionOptions); var cancellationToken = Context.CancellationToken; + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _cancellationTokenSources.Add(cancellationTokenSource); + cancellationToken = cancellationTokenSource.Token; + // Only need to reconstruct a workflow host if not already done so during CanStart. if (_workflowHost == null!) { @@ -155,7 +167,9 @@ internal class WorkflowInstance : WorkflowInstanceBase CorrelationId = correlationId, Input = input, Properties = properties, - TriggerActivityId = request.TriggerActivityId + TriggerActivityId = request.TriggerActivityId, + StatusUpdatedCallback = StatusUpdated, + CancellationTokens = new CancellationTokens(cancellationToken) }; var task = _workflowHost.StartWorkflowAsync(startWorkflowOptions, cancellationToken); @@ -187,6 +201,31 @@ internal class WorkflowInstance : WorkflowInstanceBase }); } + private void StatusUpdated(WorkflowExecutionContext context) + { + _ = Task.Run(async () => await Update(context)); + } + + private async Task Update(WorkflowExecutionContext context) + { + using var scope = _scopeFactory.CreateScope(); + var extractor = scope.ServiceProvider.GetRequiredService(); + var bookmarkPersistor = scope.ServiceProvider.GetRequiredService(); + var workflowState = extractor.Extract(context); + var originalBookmarks = _workflowHost.WorkflowState.Bookmarks; + + _workflowState = workflowState; + + await SaveSnapshotAsync(); + SaveWorkflowInstance(workflowState); + var newBookmarks = workflowState.Bookmarks; + + var diff = Diff.For(originalBookmarks, newBookmarks); + + var bookmarkRequest = new UpdateBookmarksRequest(workflowState.DefinitionId, diff, workflowState.CorrelationId); + await bookmarkPersistor.PersistBookmarksAsync(bookmarkRequest); + } + /// public override Task Stop() { @@ -208,6 +247,10 @@ internal class WorkflowInstance : WorkflowInstanceBase var activityInstanceId = request.ActivityInstanceId.NullIfEmpty(); var activityHash = request.ActivityHash.NullIfEmpty(); var cancellationToken = Context.CancellationToken; + + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _cancellationTokenSources.Add(cancellationTokenSource); + cancellationToken = cancellationTokenSource.Token; var resumeWorkflowHostOptions = new ResumeWorkflowHostOptions { @@ -218,7 +261,8 @@ internal class WorkflowInstance : WorkflowInstanceBase ActivityInstanceId = activityInstanceId, ActivityHash = activityHash, Input = _input, - Properties = _properties + Properties = _properties, + CancellationTokens = cancellationToken }; var definitionId = _definitionId; @@ -260,6 +304,18 @@ internal class WorkflowInstance : WorkflowInstanceBase /// public override Task Resume(ResumeWorkflowRequest request) => Task.FromResult(new WorkflowExecutionResponse()); + public override async Task Cancel() + { + if (_workflowState.Status != WorkflowStatus.Finished) + { + _workflowState.SubStatus = WorkflowSubStatus.Cancelled; + _workflowState.Status = WorkflowStatus.Finished; + } + + foreach(var source in _cancellationTokenSources) + source.Cancel(); + } + /// public override async Task ExportState(ExportWorkflowStateRequest request) { diff --git a/src/modules/Elsa.ProtoActor/Proto/WorkflowInstance.proto b/src/modules/Elsa.ProtoActor/Proto/WorkflowInstance.proto index 1e5b533ac..c6570ac8d 100644 --- a/src/modules/Elsa.ProtoActor/Proto/WorkflowInstance.proto +++ b/src/modules/Elsa.ProtoActor/Proto/WorkflowInstance.proto @@ -12,6 +12,7 @@ service WorkflowInstance { rpc Start (StartWorkflowRequest) returns (WorkflowExecutionResponse); rpc Stop (Empty) returns (Empty); rpc Resume (ResumeWorkflowRequest) returns (WorkflowExecutionResponse); + rpc Cancel (Empty) returns (Empty); rpc ExportState(ExportWorkflowStateRequest) returns (ExportWorkflowStateResponse); rpc ImportState(ImportWorkflowStateRequest) returns (ImportWorkflowStateResponse); } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index c10b9f788..0d47a9625 100644 --- a/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -16,6 +16,7 @@ using Elsa.Workflows.State; using Proto.Cluster; using Bookmark = Elsa.Workflows.Models.Bookmark; using CountRunningWorkflowsRequest = Elsa.Workflows.Runtime.Requests.CountRunningWorkflowsRequest; +using WorkflowStatus = Elsa.Workflows.WorkflowStatus; namespace Elsa.ProtoActor.Services; @@ -246,6 +247,13 @@ internal class ProtoActorWorkflowRuntime : IWorkflowRuntime return result!; } + /// + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken) + { + var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); + await client.Cancel(cancellationToken); + } + /// public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) { @@ -296,7 +304,7 @@ internal class ProtoActorWorkflowRuntime : IWorkflowRuntime DefinitionId = request.DefinitionId, Version = request.Version, CorrelationId = request.CorrelationId, - WorkflowStatus = Workflows.WorkflowStatus.Running + WorkflowStatus = WorkflowStatus.Running }; return await _workflowInstanceStore.CountAsync(filter, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs index 60edf36b7..c7756c25e 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs @@ -22,7 +22,7 @@ internal class BulkDelete : ElsaEndpoint public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request!.DefinitionIds, cancellationToken); + var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken); return new Response(count); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Endpoint.cs index 19a2bb69d..1f25f003d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/BulkCancel/Endpoint.cs @@ -1,25 +1,30 @@ -using System.Text.Json.Serialization; using Elsa.Abstractions; +using Elsa.Workflows.Runtime.Contracts; namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkCancel; public class BulkCancel : ElsaEndpoint { + private readonly IWorkflowRuntime _workflowRuntime; + + public BulkCancel(IWorkflowRuntime workflowRuntime) + { + _workflowRuntime = workflowRuntime; + } + public override void Configure() { Post("/bulk-actions/cancel/workflow-instances/by-id"); ConfigurePermissions("cancel:workflow-instances"); } - + public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - // TODO: Implement workflow cancellation. - var count = -1; + var tasks = request.Ids.Select(id => _workflowRuntime.CancelWorkflowAsync(id, cancellationToken)).ToList(); + await Task.WhenAll(tasks); + + var count = tasks.Count(t => t.IsCompletedSuccessfully); return new(count); } - - public record BulkCancelWorkflowInstancesRequest(ICollection Ids); - - public record BulkCancelWorkflowInstancesResponse([property: JsonPropertyName("cancelled")] int CancelledCount); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs new file mode 100644 index 000000000..3732bfeea --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs @@ -0,0 +1,44 @@ +using Elsa.Extensions; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Notifications; +using Elsa.Workflows.Signals; + +namespace Elsa.Workflows; + +public partial class ActivityExecutionContext +{ + private readonly CancellationTokenRegistration _cancellationRegistration; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly INotificationSender _publisher; + + private void CancelActivity() + { + // If the activity is not running, do nothing. + if (Status != ActivityStatus.Running && Status != ActivityStatus.Faulted) + return; + + _ = Task.Run(async () => await CancelActivityAsync()); + } + + private async Task CancelActivityAsync() + { + // Select all child contexts. + var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this).ToList(); + + foreach (var childContext in childContexts) + childContext._cancellationTokenSource.Cancel(); + + TransitionTo(ActivityStatus.Canceled); + ClearBookmarks(); + ClearCompletionCallbacks(); + WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == NodeId); + + // Add an execution log entry. + AddExecutionLogEntry("Canceled", payload: JournalData, includeActivityState: true); + + _cancellationRegistration.Dispose(); + + await this.SendSignalAsync(new CancelSignal()); + await _publisher.SendAsync(new ActivityCancelled(this)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs new file mode 100644 index 000000000..a7ce2cffb --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs @@ -0,0 +1,40 @@ +using Elsa.Extensions; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public partial class ActivityExecutionContext +{ + /// + /// Adds a new to the execution log of the current . + /// + /// The name of the event. + /// The message of the event. + /// The source of the activity. For example, the source file name and line number in case of composite activities. + /// Any contextual data related to this event. + /// True to include activity state with this event, false otherwise. + /// Returns the created . + public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = default, string? source = default, object? payload = default, bool includeActivityState = false) + { + var activityState = includeActivityState ? ActivityState : default; + + var logEntry = new WorkflowExecutionLogEntry( + Id, + ParentActivityExecutionContext?.Id, + Activity.Id, + Activity.Type, + Activity.Version, + Activity.Name, + NodeId, + activityState, + _systemClock.UtcNow, + WorkflowExecutionContext.ExecutionLogSequence++, + eventName, + message, + source ?? Activity.GetSource(), + payload); + + WorkflowExecutionContext.ExecutionLog.Add(logEntry); + return logEntry; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index b53b7958a..9583fbc20 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -4,6 +4,7 @@ using Elsa.Common.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Extensions; +using Elsa.Mediator.Contracts; using Elsa.Workflows.Contracts; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; @@ -15,7 +16,7 @@ namespace Elsa.Workflows; /// /// Represents the context of an activity execution. /// -public class ActivityExecutionContext : IExecutionContext +public partial class ActivityExecutionContext : IExecutionContext { private readonly ISystemClock _systemClock; private readonly List _bookmarks = new(); @@ -47,6 +48,10 @@ public class ActivityExecutionContext : IExecutionContext Tag = tag; CancellationToken = cancellationToken; Id = id; + _publisher = GetRequiredService(); + + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _cancellationRegistration = _cancellationTokenSource.Token.Register(CancelActivity); } /// @@ -123,8 +128,21 @@ public class ActivityExecutionContext : IExecutionContext /// /// The current status of the activity. /// - public ActivityStatus Status { get; set; } + public ActivityStatus Status { get; private set; } + /// + /// Sets the current status of the activity. + /// + public void TransitionTo(ActivityStatus status) + { + Status = status; + + if (Status is ActivityStatus.Completed + or ActivityStatus.Canceled + or ActivityStatus.Faulted) + _cancellationRegistration.Dispose(); + } + /// /// Gets or sets the exception that occurred during the activity execution, if any. /// diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.Cancel.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.Cancel.cs new file mode 100644 index 000000000..74a21f7f6 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.Cancel.cs @@ -0,0 +1,37 @@ +namespace Elsa.Workflows; + +/// +/// Provides context to the currently executing workflow. +/// +public partial class WorkflowExecutionContext +{ + private ICollection _cancellationTokenSources = new List(); + private ICollection _cancellationRegistrations = new List(); + + /// + /// Cancels the workflow and all it's children. + /// + public void Cancel() + { + foreach (var source in _cancellationTokenSources) + source.Cancel(); + + _cancellationTokenSources.Clear(); + } + + private void CancelWorkflow() + { + Bookmarks.Clear(); + _completionCallbackEntries.Clear(); + + if (Status != WorkflowStatus.Running && SubStatus != WorkflowSubStatus.Faulted) + return; + + AddExecutionLogEntry("Workflow cancelled"); + + TransitionTo(WorkflowSubStatus.Cancelled); + + foreach (var registration in _cancellationRegistrations) + registration.Dispose(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.ExecutionLogEntry.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.ExecutionLogEntry.cs new file mode 100644 index 000000000..137f372fd --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.ExecutionLogEntry.cs @@ -0,0 +1,36 @@ +using Elsa.Extensions; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public partial class WorkflowExecutionContext +{ + /// + /// Adds a new to the execution log of the current . + /// + /// The name of the event. + /// The message of the event. + /// Any contextual data related to this event. + /// Returns the created . + public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = default, object? payload = default) + { + var logEntry = new WorkflowExecutionLogEntry( + Id, + default, + Workflow.Id, + Workflow.Type, + Workflow.Identity.Version, + Workflow.Name, + Workflow.Identity.Id, + default, + SystemClock.UtcNow, + ExecutionLogSequence++, + eventName, + message, + Workflow.GetSource(), + payload); + + ExecutionLog.Add(logEntry); + return logEntry; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 51bcc4ae3..30df7271b 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -28,7 +28,7 @@ public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, Ac /// Provides context to the currently executing workflow. /// [PublicAPI] -public class WorkflowExecutionContext : IExecutionContext +public partial class WorkflowExecutionContext : IExecutionContext { private static readonly object ActivityOutputRegistryKey = new(); private static readonly object LastActivityResultKey = new(); @@ -37,6 +37,7 @@ public class WorkflowExecutionContext : IExecutionContext private readonly IList _completionCallbackEntries = new List(); private IList _activityExecutionContexts; private readonly IHasher _hasher; + private readonly Action? _statusUpdatedCallback; /// /// Initializes a new instance of . @@ -51,6 +52,7 @@ public class WorkflowExecutionContext : IExecutionContext string? triggerActivityId, IEnumerable incidents, DateTimeOffset createdAt, + Action? statusUpdatedCallback, CancellationTokens cancellationTokens) { ServiceProvider = serviceProvider; @@ -70,6 +72,13 @@ public class WorkflowExecutionContext : IExecutionContext CreatedAt = createdAt; CancellationTokens = cancellationTokens; Incidents = incidents.ToList(); + + var appSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationTokens.ApplicationCancellationToken); + _cancellationTokenSources.Add(appSource); + var sysSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationTokens.SystemCancellationToken); + _cancellationTokenSources.Add(sysSource); + _cancellationRegistrations.Add(appSource.Token.Register(CancelWorkflow)); + _cancellationRegistrations.Add(sysSource.Token.Register(CancelWorkflow)); } /// @@ -84,6 +93,7 @@ public class WorkflowExecutionContext : IExecutionContext IDictionary? properties = default, ExecuteActivityDelegate? executeDelegate = default, string? triggerActivityId = default, + Action? statusUpdatedCallback = default, CancellationTokens cancellationTokens = default) { var systemClock = serviceProvider.GetRequiredService(); @@ -99,6 +109,7 @@ public class WorkflowExecutionContext : IExecutionContext properties, executeDelegate, triggerActivityId, + statusUpdatedCallback, cancellationTokens ); } @@ -115,6 +126,7 @@ public class WorkflowExecutionContext : IExecutionContext IDictionary? properties = default, ExecuteActivityDelegate? executeDelegate = default, string? triggerActivityId = default, + Action? statusUpdatedCallback = default, CancellationTokens cancellationTokens = default) { var workflowExecutionContext = await CreateAsync( @@ -128,6 +140,7 @@ public class WorkflowExecutionContext : IExecutionContext properties, executeDelegate, triggerActivityId, + statusUpdatedCallback, cancellationTokens); var workflowStateExtractor = serviceProvider.GetRequiredService(); @@ -150,6 +163,7 @@ public class WorkflowExecutionContext : IExecutionContext IDictionary? properties = default, ExecuteActivityDelegate? executeDelegate = default, string? triggerActivityId = default, + Action? statusUpdatedCallback = default, CancellationTokens cancellationTokens = default) { // Setup a workflow execution context. @@ -163,6 +177,7 @@ public class WorkflowExecutionContext : IExecutionContext triggerActivityId, incidents, createdAt, + statusUpdatedCallback, cancellationTokens) { MemoryRegister = workflow.CreateRegister() @@ -521,6 +536,20 @@ public class WorkflowExecutionContext : IExecutionContext throw new Exception($"Cannot transition from {SubStatus} to {subStatus}"); SubStatus = subStatus; + + //For now only trigger on Cancelled, since the other statuses are handling via the host/runner + if (SubStatus == WorkflowSubStatus.Cancelled + && _statusUpdatedCallback is not null) + _statusUpdatedCallback(this); + + if (Status == WorkflowStatus.Finished + || SubStatus == WorkflowSubStatus.Suspended) + { + foreach (var registration in _cancellationRegistrations) + { + registration.Dispose(); + } + } } /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 937934592..631ba1fa6 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -1,7 +1,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Text.Json; -using Elsa.Common.Contracts; using Elsa.Expressions.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; @@ -74,45 +73,6 @@ public static class ActivityExecutionContextExtensions /// public static bool IsTriggerOfWorkflow(this ActivityExecutionContext context) => context.WorkflowExecutionContext.TriggerActivityId == context.Activity.Id; - /// - /// Adds a new to the execution log of the current . - /// - /// The being extended. - /// The name of the event. - /// The message of the event. - /// The source of the activity. For example, the source file name and line number in case of composite activities. - /// Any contextual data related to this event. - /// True to include activity state with this event, false otherwise. - /// Returns the created . - public static WorkflowExecutionLogEntry AddExecutionLogEntry(this ActivityExecutionContext context, string eventName, string? message = default, string? source = default, object? payload = default, bool includeActivityState = false) - { - var activity = context.Activity; - var activityInstanceId = context.Id; - var parentActivityInstanceId = context.ParentActivityExecutionContext?.Id; - var workflowExecutionContext = context.WorkflowExecutionContext; - var now = context.GetRequiredService().UtcNow; - var activityState = includeActivityState ? context.ActivityState : default; - - var logEntry = new WorkflowExecutionLogEntry( - activityInstanceId, - parentActivityInstanceId, - activity.Id, - activity.Type, - activity.Version, - activity.Name, - context.NodeId, - activityState, - now, - workflowExecutionContext.ExecutionLogSequence++, - eventName, - message, - source ?? activity.GetSource(), - payload); - - workflowExecutionContext.ExecutionLog.Add(logEntry); - return logEntry; - } - /// /// Creates a workflow variable by name and optionally sets the value. /// @@ -477,7 +437,7 @@ public static class ActivityExecutionContextExtensions await childContext.CancelActivityAsync(); // Mark the activity as complete. - context.Status = ActivityStatus.Completed; + context.TransitionTo(ActivityStatus.Completed); // Record the outcomes, if any. if (outcomes != null) @@ -595,7 +555,7 @@ public static class ActivityExecutionContextExtensions await CancelActivityAsync(childContext); var publisher = context.GetRequiredService(); - context.Status = ActivityStatus.Canceled; + context.TransitionTo(ActivityStatus.Canceled); context.ClearBookmarks(); context.ClearCompletionCallbacks(); context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == context.NodeId); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 8e1ac8d32..e3cb8db38 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -125,36 +125,4 @@ public static class WorkflowExecutionContextExtensions /// Returns true if all activities have completed or canceled, false otherwise. /// public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.ActivityExecutionContexts.All(x => x.IsCompleted); - - /// - /// Adds a new to the execution log of the current . - /// - /// The being extended. - /// The name of the event. - /// The message of the event. - /// Any contextual data related to this event. - /// Returns the created . - public static WorkflowExecutionLogEntry AddExecutionLogEntry(this WorkflowExecutionContext context, string eventName, string? message = default, object? payload = default) - { - var now = context.GetRequiredService().UtcNow; - - var logEntry = new WorkflowExecutionLogEntry( - context.Id, - default, - context.Workflow.Id, - context.Workflow.Type, - context.Workflow.Identity.Version, - context.Workflow.Name, - context.Workflow.Identity.Id, - default, - now, - context.ExecutionLogSequence++, - eventName, - message, - context.Workflow.GetSource(), - payload); - - context.ExecutionLog.Add(logEntry); - return logEntry; - } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 32d02f4b6..fe25241c6 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -41,16 +41,24 @@ public class DefaultActivityInvokerMiddleware : IActivityExecutionMiddleware // Evaluate input properties. await EvaluateInputPropertiesAsync(context); - + + // Prevent the activity from being started if cancellation is requested. + if (context.CancellationToken.IsCancellationRequested) + { + context.TransitionTo(ActivityStatus.Canceled); + context.AddExecutionLogEntry("Activity cancelled"); + return; + } + // Check if the activity can be executed. if (!await context.Activity.CanExecuteAsync(context)) { - context.Status = ActivityStatus.Pending; + context.TransitionTo(ActivityStatus.Pending); context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time"); return; } - context.Status = ActivityStatus.Running; + context.TransitionTo(ActivityStatus.Running); // Execute activity. await ExecuteActivityAsync(context); diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs index 6a8e851ca..5d47eebc8 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs @@ -50,7 +50,7 @@ public class ExceptionHandlingMiddleware : IActivityExecutionMiddleware { _logger.LogWarning(e, "An exception was caught from a downstream middleware component"); context.Exception = e; - context.Status = ActivityStatus.Faulted; + context.TransitionTo(ActivityStatus.Faulted); var activity = context.Activity; var exceptionState = ExceptionState.FromException(e); diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs similarity index 91% rename from src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs rename to src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs index 5d16919b4..b29e3d08e 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultWorkSchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs @@ -39,6 +39,10 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware while (scheduler.HasAny) { + // Do not start a workflow if cancellation has been requested. + if (context.CancellationTokens.ApplicationCancellationToken.IsCancellationRequested) + break; + var currentWorkItem = scheduler.Take(); await ExecuteWorkItemAsync(context, currentWorkItem); } diff --git a/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs b/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs index c3eb7201e..fc4a9d20c 100644 --- a/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs @@ -18,4 +18,5 @@ public class RunWorkflowOptions public IDictionary? Properties { get; set; } public string? TriggerActivityId { get; set; } public CancellationTokens CancellationTokens { get; set; } + public Action? StatusUpdatedCallback { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index 7f3ccd4e5..01f07bdda 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -94,6 +94,7 @@ public class WorkflowRunner : IWorkflowRunner var properties = options?.Properties; var correlationId = options?.CorrelationId; var triggerActivityId = options?.TriggerActivityId; + var statusUpdatedCallback = options?.StatusUpdatedCallback; var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync( scope.ServiceProvider, workflow, @@ -103,6 +104,7 @@ public class WorkflowRunner : IWorkflowRunner properties, default, triggerActivityId, + statusUpdatedCallback, options?.CancellationTokens ?? cancellationToken); // Schedule the first activity. @@ -122,6 +124,7 @@ public class WorkflowRunner : IWorkflowRunner var properties = options?.Properties; var correlationId = options?.CorrelationId ?? workflowState.CorrelationId; var triggerActivityId = options?.TriggerActivityId; + var statusUpdatedCallback = options?.StatusUpdatedCallback; var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync( scope.ServiceProvider, workflow, @@ -130,6 +133,7 @@ public class WorkflowRunner : IWorkflowRunner input, properties, default, triggerActivityId, + statusUpdatedCallback, options?.CancellationTokens ?? cancellationToken); var bookmarkId = options?.BookmarkId; diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index aa8ef62da..15bfb6322 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -127,7 +127,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor activityExecutionContext.Id = activityExecutionContextState.Id; activityExecutionContext.Properties = properties; activityExecutionContext.ActivityState = activityExecutionContextState.ActivityState ?? new Dictionary(); - activityExecutionContext.Status = activityExecutionContextState.Status; + activityExecutionContext.TransitionTo(activityExecutionContextState.Status); activityExecutionContext.StartedAt = activityExecutionContextState.StartedAt; activityExecutionContext.CompletedAt = activityExecutionContextState.CompletedAt; activityExecutionContext.Tag = activityExecutionContextState.Tag; diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index 25578afd2..75b97539c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -102,6 +102,10 @@ public class BulkDispatchWorkflows : Activity await foreach (var item in items) { + if (context.WorkflowExecutionContext.CancellationTokens.ApplicationCancellationToken + .IsCancellationRequested) + break; + batch.Add(item); if (batch.Count < batchSize) diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkPersister.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkPersister.cs index f46df1e7c..c826d66c5 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkPersister.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkPersister.cs @@ -1,5 +1,4 @@ -using Elsa.Workflows.Helpers; -using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Requests; namespace Elsa.Workflows.Runtime.Contracts; @@ -11,5 +10,5 @@ public interface IBookmarksPersister /// /// Persists bookmarks and raises events. /// - Task PersistBookmarksAsync(WorkflowExecutionContext context, Diff diff); + Task PersistBookmarksAsync(UpdateBookmarksRequest updateBookmarksRequest); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowExecutionContextStore.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowExecutionContextStore.cs new file mode 100644 index 000000000..ba90ce747 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowExecutionContextStore.cs @@ -0,0 +1,24 @@ +namespace Elsa.Workflows.Runtime.Contracts; + +/// +/// Stores records. +/// +public interface IWorkflowExecutionContextStore +{ + /// + /// Saves a record of the . + /// + /// The to save. + Task SaveAsync(WorkflowExecutionContext context); + + /// + /// Finds a with the specified ID. + /// + /// The matching entity or null if no match was found. + Task FindAsync(string workflowExecutionContextId); + + /// + /// Deletes the record of the with the specified ID if it exists. + /// + Task DeleteAsync(string workflowExecutionContextId); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs index 746481379..d55af768b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs @@ -60,6 +60,13 @@ public interface IWorkflowRuntime /// Options for executing the workflow. Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowOptions? options = default); + /// + /// Cancels the execution of a workflow. + /// + /// The ID of the workflow instance to cancel. + /// + Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default); + /// /// Finds all the workflows that can be started or resumed based on a query model. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 1f2ac45b3..23919dcde 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -73,6 +73,11 @@ public class WorkflowRuntimeFeature : FeatureBase /// public Func WorkflowInboxStore { get; set; } = sp => sp.GetRequiredService(); + /// + /// A factory that instantiates an . + /// + public Func WorkflowExecutionContextStore { get; set; } = sp => sp.GetRequiredService(); + /// /// A factory that instantiates an . /// @@ -157,6 +162,7 @@ public class WorkflowRuntimeFeature : FeatureBase .AddScoped(WorkflowExecutionLogStore) .AddScoped(ActivityExecutionLogStore) .AddScoped(WorkflowInboxStore) + .AddScoped(WorkflowExecutionContextStore) .AddSingleton(RunTaskDispatcher) .AddSingleton(BackgroundActivityScheduler) .AddScoped() @@ -188,6 +194,7 @@ public class WorkflowRuntimeFeature : FeatureBase .AddMemoryStore() .AddMemoryStore() .AddMemoryStore() + .AddMemoryStore() // Distributed locking. .AddScoped(DistributedLockProvider) @@ -207,7 +214,8 @@ public class WorkflowRuntimeFeature : FeatureBase .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() // Workflow activation strategies. .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/WorkflowExecutionContextNotificationsHandler.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/WorkflowExecutionContextNotificationsHandler.cs new file mode 100644 index 000000000..992de6ede --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/WorkflowExecutionContextNotificationsHandler.cs @@ -0,0 +1,36 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Notifications; +using Elsa.Workflows.Notifications; +using Elsa.Workflows.Runtime.Contracts; + +namespace Elsa.Workflows.Runtime.Handlers; + +/// +/// Deletes workflow execution log records in response to the notification. +/// +internal class WorkflowExecutionContextNotificationsHandler : + INotificationHandler, + INotificationHandler +{ + private readonly IWorkflowExecutionContextStore _store; + + /// + /// Initializes a new instance of the class. + /// + public WorkflowExecutionContextNotificationsHandler(IWorkflowExecutionContextStore store) + { + _store = store; + } + + /// + public Task HandleAsync(WorkflowExecuting notification, CancellationToken cancellationToken) + { + return _store.SaveAsync(notification.WorkflowExecutionContext); + } + + /// + public Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken) + { + return _store.DeleteAsync(notification.WorkflowExecutionContext.Id); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs index d712632a0..12126f734 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs @@ -1,6 +1,7 @@ using Elsa.Workflows.Helpers; using Elsa.Workflows.Pipelines.WorkflowExecution; using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Requests; namespace Elsa.Workflows.Runtime.Middleware.Workflows; @@ -24,6 +25,7 @@ public class PersistBookmarkMiddleware : WorkflowExecutionMiddleware await Next(context); var updatedBookmarks = context.Bookmarks.ToList(); var diff = Diff.For(originalBookmarks, updatedBookmarks); - await _bookmarksPersister.PersistBookmarksAsync(context, diff); + var bookmarkRequest = new UpdateBookmarksRequest(context.Id, diff, context.CorrelationId); + await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksIndexed.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksIndexed.cs index 11cda2d6f..6303effd1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksIndexed.cs +++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksIndexed.cs @@ -7,5 +7,5 @@ namespace Elsa.Workflows.Runtime.Notifications; /// /// The workflow execution context. /// The bookmarks that were added, removed, or unchanged. -public record WorkflowBookmarksIndexed(WorkflowExecutionContext WorkflowExecutionContext, IndexedWorkflowBookmarks IndexedWorkflowBookmarks) : INotification; +public record WorkflowBookmarksIndexed(IndexedWorkflowBookmarks IndexedWorkflowBookmarks) : INotification; diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksPersisted.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksPersisted.cs index 66768f625..b5b5f2fb2 100644 --- a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksPersisted.cs +++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowBookmarksPersisted.cs @@ -9,4 +9,4 @@ namespace Elsa.Workflows.Runtime.Notifications; /// /// The workflow execution context. /// The bookmarks that were added, removed, or unchanged. -public record WorkflowBookmarksPersisted(WorkflowExecutionContext Context, Diff Diff) : INotification; \ No newline at end of file +public record WorkflowBookmarksPersisted(Diff Diff) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Options/StartWorkflowHostOptions.cs b/src/modules/Elsa.Workflows.Runtime/Options/StartWorkflowHostOptions.cs index 361dfc2d9..1a4697c81 100644 --- a/src/modules/Elsa.Workflows.Runtime/Options/StartWorkflowHostOptions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Options/StartWorkflowHostOptions.cs @@ -24,4 +24,9 @@ public class StartWorkflowHostOptions /// Cancellation tokens that can be used to cancel the workflow instance without cancelling system-level operations. public CancellationTokens CancellationTokens { get; set; } + + /// + /// Callback method that will be called when the status of the workflow has been updated + /// + public Action? StatusUpdatedCallback { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkPersister.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkPersister.cs index f2cc05b53..775ade777 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkPersister.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkPersister.cs @@ -1,7 +1,5 @@ using Elsa.Mediator; using Elsa.Mediator.Contracts; -using Elsa.Workflows.Helpers; -using Elsa.Workflows.Models; using Elsa.Workflows.Runtime.Contracts; using Elsa.Workflows.Runtime.Notifications; using Elsa.Workflows.Runtime.Requests; @@ -11,25 +9,15 @@ namespace Elsa.Workflows.Runtime.Services; /// public class BookmarksPersister(IBookmarkUpdater bookmarkUpdater, INotificationSender notificationSender) : IBookmarksPersister { - - /// - public async Task PersistBookmarksAsync(WorkflowExecutionContext context, Diff diff) + public async Task PersistBookmarksAsync(UpdateBookmarksRequest updateBookmarksRequest) { - var cancellationToken = context.CancellationTokens.SystemCancellationToken; - var updateBookmarksContext = new UpdateBookmarksRequest(context.Id, diff, context.CorrelationId); - await bookmarkUpdater.UpdateBookmarksAsync(updateBookmarksContext, cancellationToken); - + await bookmarkUpdater.UpdateBookmarksAsync(updateBookmarksRequest); + // Publish domain event. - await notificationSender.SendAsync(new WorkflowBookmarksIndexed(context, new IndexedWorkflowBookmarks(context.Id, diff.Added, diff.Removed, diff.Unchanged)), cancellationToken); - - // Notify all interested activities that the bookmarks have been persisted. - var activityExecutionContexts = context.ActivityExecutionContexts.Where(x => x.Activity is IBookmarksPersistedHandler && x.Bookmarks.Any()).ToList(); - - foreach (var activityExecutionContext in activityExecutionContexts) - await ((IBookmarksPersistedHandler)activityExecutionContext.Activity).BookmarksPersistedAsync(activityExecutionContext); - + await notificationSender.SendAsync(new WorkflowBookmarksIndexed(new IndexedWorkflowBookmarks(updateBookmarksRequest.WorkflowInstanceId, updateBookmarksRequest.Diff.Added, updateBookmarksRequest.Diff.Removed, updateBookmarksRequest.Diff.Unchanged))); + // Publish domain event. - await notificationSender.SendAsync(new WorkflowBookmarksPersisted(context, diff), NotificationStrategy.Background, cancellationToken); + await notificationSender.SendAsync(new WorkflowBookmarksPersisted(updateBookmarksRequest.Diff), NotificationStrategy.Background); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs index 11730d0af..8b5bf4ad8 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs @@ -1,6 +1,7 @@ using Elsa.Common.Models; using Elsa.Extensions; using Elsa.Workflows.Contracts; +using Elsa.Workflows.Helpers; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Management.Mappers; @@ -32,6 +33,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime private readonly IWorkflowInstanceFactory _workflowInstanceFactory; private readonly WorkflowStateMapper _workflowStateMapper; private readonly IIdentityGenerator _identityGenerator; + private readonly IWorkflowExecutionContextStore _workflowExecutionContextStore; + private readonly IWorkflowStateExtractor _workflowStateExtractor; + private readonly IServiceProvider _serviceProvider; + private readonly IBookmarksPersister _bookmarksPersister; /// /// Constructor. @@ -47,7 +52,11 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime IDistributedLockProvider distributedLockProvider, IWorkflowInstanceFactory workflowInstanceFactory, WorkflowStateMapper workflowStateMapper, - IIdentityGenerator identityGenerator) + IIdentityGenerator identityGenerator, + IWorkflowExecutionContextStore workflowExecutionContextStore, + IWorkflowStateExtractor workflowStateExtractor, + IServiceProvider serviceProvider, + IBookmarksPersister bookmarksPersister) { _workflowHostFactory = workflowHostFactory; _workflowDefinitionService = workflowDefinitionService; @@ -60,6 +69,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime _workflowInstanceFactory = workflowInstanceFactory; _workflowStateMapper = workflowStateMapper; _identityGenerator = identityGenerator; + _workflowExecutionContextStore = workflowExecutionContextStore; + _workflowStateExtractor = workflowStateExtractor; + _serviceProvider = serviceProvider; + _bookmarksPersister = bookmarksPersister; } /// @@ -90,6 +103,67 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime { return await StartWorkflowAsync(definitionId, options); } + + /// + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken) + { + var workflowExecutionContext = await _workflowExecutionContextStore.FindAsync(workflowInstanceId); + + if (workflowExecutionContext is null) + { + // The execution context is not running on this instance. + // It might not be running on any instance, so check the db and update the record. + // Use lock to prevent race conditions and other instances from updating the workflow context + await using var cancelLock = await _distributedLockProvider.TryAcquireLockAsync($"{workflowInstanceId}-cancel"); + if (cancelLock == null) + return; + + var workflowInstance = await _workflowInstanceStore.FindAsync(workflowInstanceId, cancellationToken); + if (workflowInstance is null + || workflowInstance.SubStatus == WorkflowSubStatus.Cancelled + || workflowInstance.SubStatus == WorkflowSubStatus.Faulted) + return; + + var workflowState = await ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + + if (workflowState == null) + throw new Exception("Workflow state not found"); + + var workflowDefinition = await _workflowDefinitionService.FindAsync(workflowState.DefinitionId, VersionOptions.SpecificVersion(workflowState.DefinitionVersion), cancellationToken); + + if (workflowDefinition == null) + throw new Exception("Workflow definition not found"); + + var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken); + workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(_serviceProvider, workflow, workflowState, cancellationTokens: cancellationToken); + + if (!cancellationToken.IsCancellationRequested) + await CancelWorkflowExecutionContextAsync(); + + return; + } + + await using var mainCancelLock = await _distributedLockProvider.AcquireLockAsync($"{workflowInstanceId}-cancel", TimeSpan.FromMinutes(1)); + + await CancelWorkflowExecutionContextAsync(); + + async Task CancelWorkflowExecutionContextAsync() + { + var originalBookmarks = workflowExecutionContext.Bookmarks.ToList(); + + workflowExecutionContext.Cancel(); + + var newBookmarks = workflowExecutionContext.Bookmarks.ToList(); + var diff = Diff.For(originalBookmarks, newBookmarks); + var bookmarkRequest = new UpdateBookmarksRequest(workflowExecutionContext.Id, + diff, + workflowExecutionContext.CorrelationId); + await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest); + + var instance = await _workflowInstanceManager.SaveAsync(workflowExecutionContext); + await _workflowInstanceStore.SaveAsync(instance); + } + } /// public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions options) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/MemoryWorkflowExecutionContextStore.cs b/src/modules/Elsa.Workflows.Runtime/Services/MemoryWorkflowExecutionContextStore.cs new file mode 100644 index 000000000..7e262cd4c --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Services/MemoryWorkflowExecutionContextStore.cs @@ -0,0 +1,42 @@ +using Elsa.Common.Services; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Stores; + +namespace Elsa.Workflows.Runtime.Services; + +/// +/// Stores in memory. +/// +public class MemoryWorkflowExecutionContextStore : IWorkflowExecutionContextStore +{ + private readonly MemoryStore _store; + + /// + /// Initializes a new instance of the class. + /// + public MemoryWorkflowExecutionContextStore(MemoryStore store) + { + _store = store; + } + + /// + public Task SaveAsync(WorkflowExecutionContext context) + { + _store.Save(context, x => x.Id); + return Task.CompletedTask; + } + + /// + public Task FindAsync(string workflowExecutionContextId) + { + var result = _store.Find((context) => context.Id == workflowExecutionContextId); + return Task.FromResult(result); + } + + /// + public Task DeleteAsync(string workflowExecutionContextId) + { + _store.Delete(workflowExecutionContextId); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs index 9980c8639..07eb48939 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowHost.cs @@ -80,6 +80,7 @@ public class WorkflowHost : IWorkflowHost Input = input, Properties = properties, TriggerActivityId = options?.TriggerActivityId, + StatusUpdatedCallback = options?.StatusUpdatedCallback, CancellationTokens = options?.CancellationTokens ?? cancellationToken }; diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj index 29f7310ed..43f1f116e 100644 --- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj +++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,6 +24,7 @@ + diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs new file mode 100644 index 000000000..7222bdece --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; +using Elsa.Mediator.HostedServices; +using Elsa.Mediator.Options; +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation; + +public class DefaultRuntimeTests +{ + private readonly IServiceProvider _services; + private readonly CapturingTextWriter _capturingTextWriter = new(); + + private readonly IWorkflowRuntime _workflowRuntime; + private readonly BackgroundCommandSenderHostedService _backgroundCommandSenderHostedService; + private readonly BackgroundEventPublisherHostedService _backgroundEventPublisherHostedService; + + public DefaultRuntimeTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .AddWorkflow() + .AddWorkflow() + .AddWorkflow() + .AddWorkflow() + .ConfigureServices(services => + { + services + .AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return ActivatorUtilities.CreateInstance(sp, options.CommandWorkerCount); + }) + .AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return ActivatorUtilities.CreateInstance(sp, options.NotificationWorkerCount); + }) + ; + }) + .Build(); + + _backgroundCommandSenderHostedService = _services.GetRequiredService(); + _backgroundEventPublisherHostedService = _services.GetRequiredService(); + _workflowRuntime = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Cancelling a suspended workflow")] + public async Task SuspendedCancelTest() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + const string workflowDefinitionId = nameof(SimpleSuspendedWorkflow); + var workflowState = await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions()); + + Assert.Equal(WorkflowStatus.Running, workflowState.Status); + Assert.Equal(WorkflowSubStatus.Suspended, workflowState.SubStatus); + + await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId); + var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId); + + Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status); + Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus); + Assert.Empty(_capturingTextWriter.Lines); + } + + [Fact(DisplayName = "Cancelling a running workflow")] + public async Task RunningCancelTest() + { + await _backgroundCommandSenderHostedService.StartAsync(CancellationToken.None); + await _backgroundEventPublisherHostedService.StartAsync(CancellationToken.None); + + // Populate registries. + await _services.PopulateRegistriesAsync(); + + const string workflowDefinitionId = nameof(BulkSuspendedWorkflow); + var workflowState = await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions()); + + var bookmarks = new Stack(workflowState.Bookmarks); + var resumeOptions = new ResumeWorkflowRuntimeOptions { BookmarkId = bookmarks.Pop().Id }; + var state = await _workflowRuntime.ResumeWorkflowAsync(workflowState.WorkflowInstanceId,resumeOptions); + + await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId); + var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId); + + Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status); + Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus); + Assert.NotEmpty(_capturingTextWriter.Lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs new file mode 100644 index 000000000..1a987f62a --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Extensions; +using Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; +using Elsa.Mediator.HostedServices; +using Elsa.Mediator.Options; +using Elsa.ProtoActor.HostedServices; +using Elsa.Testing.Shared; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Options; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Proto.Persistence.Sqlite; +using Xunit; +using Xunit.Abstractions; +using WorkflowStatus = Elsa.Workflows.WorkflowStatus; +using WorkflowSubStatus = Elsa.Workflows.WorkflowSubStatus; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation; + +public class ProtoActorTests +{ + private readonly IServiceProvider _services; + private readonly CapturingTextWriter _capturingTextWriter = new(); + + private readonly IWorkflowRuntime _workflowRuntime; + private readonly WorkflowServerHost _workflowServerHost; + private readonly BackgroundCommandSenderHostedService _backgroundCommandSenderHostedService; + private readonly BackgroundEventPublisherHostedService _backgroundEventPublisherHostedService; + + public ProtoActorTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .AddWorkflow() + .AddWorkflow() + .AddWorkflow() + .AddWorkflow() + .ConfigureServices(services => + { + services + .AddSingleton(sp => ActivatorUtilities.CreateInstance(sp)); + + services + .AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return ActivatorUtilities.CreateInstance(sp, + options.CommandWorkerCount); + }) + .AddSingleton(sp => + { + var options = sp.GetRequiredService>().Value; + return ActivatorUtilities.CreateInstance(sp, + options.NotificationWorkerCount); + }); + }).ConfigureElsa(elsa => elsa.UseWorkflowRuntime(runtime => runtime.UseProtoActor(protoActor => + { + protoActor.PersistenceProvider = _ => + new SqliteProvider( + new SqliteConnectionStringBuilder("Data Source=elsa.sqlite.db;Cache=Shared;")); + } + ))) + .Build(); + + _backgroundCommandSenderHostedService = _services.GetRequiredService(); + _backgroundEventPublisherHostedService = _services.GetRequiredService(); + _workflowServerHost = _services.GetRequiredService(); + _workflowRuntime = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Cancelling a suspended workflow")] + public async Task SuspendedCancelTest() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + await _workflowServerHost.StartAsync(default); + const string workflowDefinitionId = nameof(SimpleSuspendedWorkflow); + var workflowState = + await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions()); + + Assert.Equal(WorkflowStatus.Running, workflowState.Status); + Assert.Equal(WorkflowSubStatus.Suspended, workflowState.SubStatus); + + await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId); + + await Task.Delay(2000); + var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId); + + Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status); + Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus); + Assert.Empty(_capturingTextWriter.Lines); + } + + [Fact(DisplayName = "Cancelling a running workflow")] + public async Task RunningCancelTest() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + await _workflowServerHost.StartAsync(default); + await _backgroundCommandSenderHostedService.StartAsync(CancellationToken.None); + await _backgroundEventPublisherHostedService.StartAsync(CancellationToken.None); + + const string workflowDefinitionId = nameof(BulkSuspendedWorkflow); + var workflowState = + await _workflowRuntime.StartWorkflowAsync(workflowDefinitionId, new StartWorkflowRuntimeOptions()); + + var bookmarks = new Stack(workflowState.Bookmarks); + var resumeOptions = new ResumeWorkflowRuntimeOptions { BookmarkId = bookmarks.Pop().Id }; + await _workflowRuntime.ResumeWorkflowAsync(workflowState.WorkflowInstanceId, resumeOptions); + + await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId); + var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId); + + Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status); + Assert.Equal(WorkflowSubStatus.Cancelled, lastWorkflowState.SubStatus); + + Assert.NotEmpty(_capturingTextWriter.Lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs new file mode 100644 index 000000000..afb88519b --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elsa.Scheduling.Activities; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; + +public class BulkSuspendedWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + object[] items = Enumerable.Range(0,10000).Select(x => (object) x).ToArray(); + + builder.Root = new Sequence + { + Activities = + { + new Start(), + new Delay(TimeSpan.FromSeconds(10)), + new BulkDispatchWorkflows + { + WorkflowDefinitionId = new Input(nameof(SimpleChildWorkflow)), + Items = new Input>(items) + } + }, + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/ResumeDispatchWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/ResumeDispatchWorkflow.cs new file mode 100644 index 000000000..8292b7026 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/ResumeDispatchWorkflow.cs @@ -0,0 +1,21 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; + +public class ResumeDispatchWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new Sequence + { + Activities = + { + new PublishEvent { EventName = new Input("ResumeBlockDispatch") } + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleChildWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleChildWorkflow.cs new file mode 100644 index 000000000..b8217afaf --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleChildWorkflow.cs @@ -0,0 +1,19 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; + +public class SimpleChildWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new Sequence + { + Activities = + { + new WriteLine("Executed SimpleChildWorkflow") + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleSuspendedWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleSuspendedWorkflow.cs new file mode 100644 index 000000000..7b0912b6d --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/SimpleSuspendedWorkflow.cs @@ -0,0 +1,22 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowCancellation.Workflows; + +public class SimpleSuspendedWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new Sequence + { + Activities = + { + new Start(), + new Event("BlockingEvent"), + new WriteLine("Workflow was not properly blocked") + }, + }; + } +} \ No newline at end of file From e45ec3e6b4221e6dc8c44d61610d4d24987ff777 Mon Sep 17 00:00:00 2001 From: raymonddenhaan <155616759+raymonddenhaan@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:34:45 +0100 Subject: [PATCH 11/21] Disabled tests that had an unreliable result (#4827) --- .../Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs | 3 ++- .../Scenarios/WorkflowCancellation/ProtoActorTests.cs | 4 ++-- .../WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs index 7222bdece..47e8b025a 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs @@ -76,7 +76,8 @@ public class DefaultRuntimeTests Assert.Empty(_capturingTextWriter.Lines); } - [Fact(DisplayName = "Cancelling a running workflow")] + [Fact(DisplayName = "Cancelling a running workflow", + Skip= "Unpredictable result, need to create a dispatcher for tests that will run outside of the unit test")] public async Task RunningCancelTest() { await _backgroundCommandSenderHostedService.StartAsync(CancellationToken.None); diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs index 1a987f62a..dcc19d259 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/ProtoActorTests.cs @@ -88,7 +88,6 @@ public class ProtoActorTests await _workflowRuntime.CancelWorkflowAsync(workflowState.WorkflowInstanceId); - await Task.Delay(2000); var lastWorkflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowState.WorkflowInstanceId); Assert.Equal(WorkflowStatus.Finished, lastWorkflowState!.Status); @@ -96,7 +95,8 @@ public class ProtoActorTests Assert.Empty(_capturingTextWriter.Lines); } - [Fact(DisplayName = "Cancelling a running workflow")] + [Fact(DisplayName = "Cancelling a running workflow", + Skip= "Unpredictable result, need to create a dispatcher for tests that will run outside of the unit test")] public async Task RunningCancelTest() { // Populate registries. diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs index afb88519b..fa763bced 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowCancellation/Workflows/BulkSuspendedWorkflow.cs @@ -14,7 +14,7 @@ public class BulkSuspendedWorkflow : WorkflowBase { protected override void Build(IWorkflowBuilder builder) { - object[] items = Enumerable.Range(0,10000).Select(x => (object) x).ToArray(); + object[] items = Enumerable.Range(0,1500).Select(x => (object) x).ToArray(); builder.Root = new Sequence { From a7851175b81d75ba48046a3f756fc07a85c9f440 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 23 Jan 2024 21:58:38 +0100 Subject: [PATCH 12/21] Add input application to workflow state extractor A new method ApplyInput has been added to prevent overwriting any existing input in the WorkflowExecutionContext. Now, input items are added to the workflow execution context only if they don't exist there already. The Apply method has been updated as well to add workflow state inputs appropriately. Fixes #4705 --- .../Services/WorkflowStateExtractor.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index aa8ef62da..347f7f703 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -35,6 +35,13 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor return state; } + + private void ApplyInput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + { + // Only add input from state if the input doesn't already exist on the workflow execution context. + foreach (var inputItem in state.Input) + if (!workflowExecutionContext.Input.ContainsKey(inputItem.Key)) workflowExecutionContext.Input.Add(inputItem.Key, inputItem.Value); + } private IDictionary GetPersistableInput(WorkflowExecutionContext workflowExecutionContext) { @@ -55,7 +62,6 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor /// public WorkflowExecutionContext Apply(WorkflowExecutionContext workflowExecutionContext, WorkflowState state) { - // Do not map input. We don't want to overwrite the input that was passed to the workflow. workflowExecutionContext.Id = state.Id; workflowExecutionContext.CorrelationId = state.CorrelationId; workflowExecutionContext.SubStatus = state.SubStatus; @@ -63,6 +69,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor workflowExecutionContext.Output = state.Output; workflowExecutionContext.ExecutionLogSequence = state.ExecutionLogSequence; workflowExecutionContext.CreatedAt = state.CreatedAt; + ApplyInput(state, workflowExecutionContext); ApplyProperties(state, workflowExecutionContext); ApplyActivityExecutionContexts(state, workflowExecutionContext); ApplyCompletionCallbacks(state, workflowExecutionContext); From a1b7afdf4fed9df4bd9461d96f1df2545f3181da Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 23 Jan 2024 22:01:13 +0100 Subject: [PATCH 13/21] Reorder methods in WorkflowStateExtractor service The private methods within the WorkflowStateExtractor service have been reordered for readability and maintainability. The `ApplyInput` and `GetPersistableInput` methods have been moved to a position just before the `ExtractProperties` method. This ensures that similar or related methods are grouped together, improving the logical structure of the code. --- .../Services/WorkflowStateExtractor.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index 347f7f703..3a6ce6851 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -35,6 +35,24 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor return state; } + + /// + public WorkflowExecutionContext Apply(WorkflowExecutionContext workflowExecutionContext, WorkflowState state) + { + workflowExecutionContext.Id = state.Id; + workflowExecutionContext.CorrelationId = state.CorrelationId; + workflowExecutionContext.SubStatus = state.SubStatus; + workflowExecutionContext.Bookmarks = state.Bookmarks; + workflowExecutionContext.Output = state.Output; + workflowExecutionContext.ExecutionLogSequence = state.ExecutionLogSequence; + workflowExecutionContext.CreatedAt = state.CreatedAt; + ApplyInput(state, workflowExecutionContext); + ApplyProperties(state, workflowExecutionContext); + ApplyActivityExecutionContexts(state, workflowExecutionContext); + ApplyCompletionCallbacks(state, workflowExecutionContext); + ApplyScheduledActivities(state, workflowExecutionContext); + return workflowExecutionContext; + } private void ApplyInput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { @@ -59,24 +77,6 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor return filteredInput; } - /// - public WorkflowExecutionContext Apply(WorkflowExecutionContext workflowExecutionContext, WorkflowState state) - { - workflowExecutionContext.Id = state.Id; - workflowExecutionContext.CorrelationId = state.CorrelationId; - workflowExecutionContext.SubStatus = state.SubStatus; - workflowExecutionContext.Bookmarks = state.Bookmarks; - workflowExecutionContext.Output = state.Output; - workflowExecutionContext.ExecutionLogSequence = state.ExecutionLogSequence; - workflowExecutionContext.CreatedAt = state.CreatedAt; - ApplyInput(state, workflowExecutionContext); - ApplyProperties(state, workflowExecutionContext); - ApplyActivityExecutionContexts(state, workflowExecutionContext); - ApplyCompletionCallbacks(state, workflowExecutionContext); - ApplyScheduledActivities(state, workflowExecutionContext); - return workflowExecutionContext; - } - private void ExtractProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { state.Properties = workflowExecutionContext.Properties; From 792c06ee04a5558456bec8cba5e17fb528f37962 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 24 Jan 2024 01:18:00 +0100 Subject: [PATCH 14/21] Update GitHub actions workflow to target v3.0.4 The GitHub actions workflow has been updated to target the version branch v3.0.4. The necessary checks and commands have been modified accordingly to ensure proper compatibility with the new branch. This update is necessary for the correct functioning of the automatic deployment activities. --- .github/workflows/packages.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 29f163d5c..72d1388c2 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - v3.0.3 + - v3.0.4 release: types: [ prereleased, published ] env: @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Verify commit exists in origin/v3.0.3 + - name: Verify commit exists in origin/v3.0.4 run: | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/v3.0.3 + git branch --remote --contains | grep origin/v3.0.4 - name: Set VERSION variable run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "published" ]]; then From 03743d6da131fd1c4d1e5c72c07a8f1416868aea Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 24 Jan 2024 01:21:52 +0100 Subject: [PATCH 15/21] Update version number in GitHub workflow The version number has been updated in the GitHub actions workflow file. Previously, it was hardcoded to "3.0.3-preview", but now it's switched to "3.0.4-preview". This change keeps our software versioning consistent and up-to-date. --- .github/workflows/packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 72d1388c2..bb54e11bd 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -29,7 +29,7 @@ jobs: TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV else - echo "VERSION=3.0.3-preview.${{github.run_number}}" >> $GITHUB_ENV + echo "VERSION=3.0.4-preview.${{github.run_number}}" >> $GITHUB_ENV fi - name: Build run: dotnet build --configuration Release /p:Version=${VERSION} From e7edb1d3ea8d36b54d71d7631a5ac3122be04205 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 24 Jan 2024 20:32:33 +0100 Subject: [PATCH 16/21] Update Elsa.Studio and Elsa.Studio.Login.BlazorWasm package versions The package versions for Elsa.Studio and Elsa.Studio.Login.BlazorWasm have been updated to 3.0.4 across multiple projects. This update allows projects to utilize the latest features and improvements from these packages. --- .../Elsa.ServerAndStudio.Web.csproj | 4 ++-- src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj | 4 ++-- .../ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj index 659bc16ef..d58697184 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj +++ b/src/bundles/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj @@ -35,8 +35,8 @@ - - + + \ No newline at end of file diff --git a/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj b/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj index 184fe20fe..3964f9121 100644 --- a/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj +++ b/src/bundles/Elsa.Studio.Web/Elsa.Studio.Web.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj b/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj index 5c8e0361d..2da3d44eb 100644 --- a/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj +++ b/src/bundles/ElsaStudioWebAssembly/ElsaStudioWebAssembly.csproj @@ -19,9 +19,9 @@ - - - + + + From 64c4d897393c34a1ead1084609f9bbdad3876466 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 26 Jan 2024 11:25:25 +0100 Subject: [PATCH 17/21] Update namespace in ModuleExtensions.cs The namespace in the ModuleExtensions.cs file within Elsa.Webhooks has been changed. The change moves the namespace from "Elsa.Webhooks.Extensions" to "Elsa.Extensions". A ReSharper directive has been added to disable warnings related to the namespace check. --- src/modules/Elsa.Webhooks/Extensions/ModuleExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Webhooks/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Webhooks/Extensions/ModuleExtensions.cs index 6b8a30599..fb1cb8845 100644 --- a/src/modules/Elsa.Webhooks/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Webhooks/Extensions/ModuleExtensions.cs @@ -1,7 +1,8 @@ using Elsa.Features.Services; using Elsa.Webhooks.Features; -namespace Elsa.Webhooks.Extensions; +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; /// /// Adds extensions to that enables the feature. From a644d59c737d4a1430e2fc26ccac5cfe9facf57a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 26 Jan 2024 14:49:30 +0100 Subject: [PATCH 18/21] Update target branches for GitHub Actions The target branches for GitHub Action on pull requests have been updated. Specifically, the wildcard 'v3*' is removed from the 'OnPullRequestBranches' attribute. Now, actions will only be triggered for pull requests on the 'main' branch. --- build/Build.CI.GitHubActions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index e317e2222..a6e559699 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -8,7 +8,7 @@ using Nuke.Components; [CustomGitHubActions( "pr", GitHubActionsImage.UbuntuLatest, - OnPullRequestBranches = ["main", "v3*"], + OnPullRequestBranches = ["main"], OnPullRequestIncludePaths = ["**/*"], OnPullRequestExcludePaths = ["docs/**/*", "package.json", "package-lock.json", "readme.md"], PublishArtifacts = false, From 4a3ae96ed94845099369fed7931276c03b9da7d5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 26 Jan 2024 14:49:42 +0100 Subject: [PATCH 19/21] Remove hardcoded version from Elsa.Workflows.Api.csproj The hardcoded version specification has been removed from the Elsa.Workflows.Api project file. This change will allow the versioning system to more effectively manage and track the versioning of the project. --- src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj index c85487ae3..43fc92b01 100644 --- a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj +++ b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj @@ -5,7 +5,6 @@ - 3.0.0 Provides API endpoints for client applications such as the Elsa Designer web component. From d693cb7f8f0ed2cb89840efc1633cf4df0ca9310 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 26 Jan 2024 14:52:31 +0100 Subject: [PATCH 20/21] Remove unused Elsa.Webhooks.Extensions import The Elsa.Webhooks.Extensions import was removed from various Program.cs files because it doesn't exist anymore. --- src/bundles/Elsa.ServerAndStudio.Web/Program.cs | 1 - .../Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs | 1 - .../Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs index 3726828da..a83d2cf6a 100644 --- a/src/bundles/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/bundles/Elsa.ServerAndStudio.Web/Program.cs @@ -3,7 +3,6 @@ using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; using Elsa.Extensions; -using Elsa.Webhooks.Extensions; using Microsoft.AspNetCore.Mvc; const bool useMassTransit = true; diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs b/src/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs index 1a5649b71..42cf941ac 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.AspNet.Onboarding.WorkflowServer/Program.cs @@ -2,7 +2,6 @@ using Elsa.EntityFrameworkCore.Modules.Management; using Elsa.EntityFrameworkCore.Modules.Runtime; using Elsa.Extensions; using Elsa.Samples.AspNet.Onboarding.WorkflowServer.Workflows; -using Elsa.Webhooks.Extensions; var builder = WebApplication.CreateBuilder(args); diff --git a/src/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs b/src/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs index 857640421..26335242e 100644 --- a/src/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs +++ b/src/samples/aspnet/Elsa.Samples.AspNet.Webhooks.WorkflowServer/Program.cs @@ -1,7 +1,6 @@ using System; using Elsa.Extensions; using Elsa.Samples.AspNet.Webhooks.WorkflowServer.Workflows; -using Elsa.Webhooks.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; From 15e5e58de2c6ecf74df0b4538bd1ddf6bde63bf5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 26 Jan 2024 19:20:23 +0100 Subject: [PATCH 21/21] Refactor GetWorkflowDefinitionId in ActivityExtensions Expanded the GetWorkflowDefinitionId method in ActivityExtensions.cs to check for multiple properties before determining whether an activity is a workflow definition activity or not. This ensures a more rigorous validation process. --- .../Extensions/ActivityExtensions.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs index 2663488ad..6ae48eab8 100644 --- a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs @@ -145,11 +145,26 @@ public static class ActivityExtensions /// Sets the root activity in the specified activity. /// public static void SetRoot(this JsonObject container, JsonObject root) => container.SetProperty(root, "root"); - + /// /// Gets the root activity in the specified activity. /// - public static string? GetWorkflowDefinitionId(this JsonObject activity) => activity.GetProperty("workflowDefinitionId"); + public static string? GetWorkflowDefinitionId(this JsonObject activity) + { + // Determine if this activity represents a workflow definition activity based on the presence of the following properties. + var workflowDefinitionIdNode = activity.GetProperty("workflowDefinitionId"); + var workflowDefinitionVersionId = activity.GetProperty("workflowDefinitionVersionId"); + var latestAvailablePublishedVersion = activity.GetProperty("latestAvailablePublishedVersion"); + var latestAvailablePublishedVersionId = activity.GetProperty("latestAvailablePublishedVersionId"); + + if (workflowDefinitionIdNode == null || workflowDefinitionVersionId == null || latestAvailablePublishedVersion == null || latestAvailablePublishedVersionId == null) + return null; // Not a workflow definition activity. + + if (workflowDefinitionIdNode is JsonValue value) + return value.ToString(); + + return null; // Not a workflow definition activity. + } /// /// Determines whether the given activity is a workflow definition activity. @@ -157,7 +172,7 @@ public static class ActivityExtensions /// The JsonObject representing the activity. /// Returns true if the activity is a workflow definition activity; otherwise, false. public static bool GetIsWorkflowDefinitionActivity(this JsonObject activity) => activity.ContainsKey("workflowDefinitionId") && activity.ContainsKey("workflowDefinitionVersionId"); - + /// /// Gets a value indicating whether the specified activity can trigger the workflow. ///