From 311b792f9bc16804ee111119bbe4b189baf3552e Mon Sep 17 00:00:00 2001 From: sergergood Date: Mon, 16 Dec 2024 22:57:11 +0300 Subject: [PATCH 001/166] Call async methods when in an async method --- .../Elsa.Testing.Shared.Component/Services/SignalManager.cs | 2 +- .../Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs | 4 ++-- .../WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Delete/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Import/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Post/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Publish/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Retract/Endpoint.cs | 4 ++-- .../WorkflowDefinitions/UpdateReferences/Endpoint.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Version/Delete.cs | 4 ++-- .../Endpoints/WorkflowDefinitions/Version/Revert.cs | 4 ++-- 15 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs b/src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs index 94ce581af..b9b585f66 100644 --- a/src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs +++ b/src/common/Elsa.Testing.Shared.Component/Services/SignalManager.cs @@ -25,7 +25,7 @@ public class SignalManager await Task.WhenAny(taskCompletionSource.Task, Task.Delay(millisecondsTimeout, cancellationTokenSource.Token)); cancellationTokenSource.Token.ThrowIfCancellationRequested(); _signals.TryRemove(signal, out _); - return taskCompletionSource.Task.Result; + return await taskCompletionSource.Task; } catch (OperationCanceledException) { 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 bce5264cf..1da5ff771 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDelete/Endpoint.cs @@ -19,9 +19,9 @@ internal class BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager, public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return null!; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs index a56f49821..11648b4ef 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkDeleteVersions/Endpoint.cs @@ -19,9 +19,9 @@ internal class BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionM public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return null!; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs index 3893a32d1..1c36f7fb1 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs @@ -21,9 +21,9 @@ internal class BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPu public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return null!; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs index 0b9129147..e57162059 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkRetract/Endpoint.cs @@ -21,9 +21,9 @@ internal class BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPu public override async Task ExecuteAsync(Request request, CancellationToken cancellationToken) { - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return null!; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs index 33ddca4ba..a3c5bfde3 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Delete/Endpoint.cs @@ -33,9 +33,9 @@ internal class Delete(IWorkflowDefinitionManager workflowDefinitionManager, IAut return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs index 932bbe04e..c29dc96f7 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/DeleteVersion/Endpoint.cs @@ -19,9 +19,9 @@ internal class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManage public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs index 23517a900..72e792c17 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs @@ -45,9 +45,9 @@ internal class Import : ElsaEndpoint var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken); var definition = result.WorkflowDefinition; - var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs index b02a123e0..8b4c7622f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs @@ -49,9 +49,9 @@ internal class ImportFiles : ElsaEndpoint /// public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken) { - var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index 0746c037d..b8ff04d48 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -56,9 +56,9 @@ internal class Post( draft.DefinitionId = definitionId; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(draft), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(draft), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs index 0a28f964d..55d63a06b 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Publish/Endpoint.cs @@ -35,9 +35,9 @@ internal class Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublis return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs index 9c2b1484f..7a52e7506 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Retract/Endpoint.cs @@ -36,9 +36,9 @@ internal class Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublis return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs index ca94da972..a510186c7 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/UpdateReferences/Endpoint.cs @@ -36,9 +36,9 @@ internal class UpdateReferences(IWorkflowReferenceUpdater workflowReferenceUpdat return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs index a1cda6181..b6a4d0930 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Delete.cs @@ -42,9 +42,9 @@ public class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs index 0d7aef8df..4dcd78dc9 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Version/Revert.cs @@ -39,9 +39,9 @@ internal class RevertVersion(IWorkflowDefinitionManager workflowDefinitionManage return; } - var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); + var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy); - if (!authorizationResult.Result.Succeeded) + if (!authorizationResult.Succeeded) { await SendForbiddenAsync(cancellationToken); return; From 76d685744d3149bcaf9acee117e0b18401cbd769 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 22 Dec 2024 10:00:56 +0100 Subject: [PATCH 002/166] Update target frameworks and package versions to latest Dropped .NET 6 support and upgraded projects to .NET 8 and .NET 9 frameworks. Updated several package dependencies to their latest stable or preview versions to ensure compatibility and leverage improvements. --- Directory.Packages.props | 141 +++++++----------- src/Directory.Build.props | 2 +- ...rsistence.EntityFrameworkCore.MySql.csproj | 2 +- src/modules/Elsa.Common/Elsa.Common.csproj | 4 - .../Elsa.EntityFrameworkCore.MySql.csproj | 2 +- ...sa.Quartz.EntityFrameworkCore.MySql.csproj | 2 +- src/modules/Elsa.Quartz/Elsa.Quartz.csproj | 2 +- 7 files changed, 56 insertions(+), 99 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a69ea2529..2457b4ded 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,35 +16,35 @@ - + - + - + - - - + + + - - - - - - - - + + + + + + + + - + - + @@ -55,17 +55,17 @@ - - - - + + + + - + - + @@ -86,84 +86,45 @@ - + - + - + - + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -174,16 +135,16 @@ - + - - + + - + @@ -221,11 +182,11 @@ - - + + - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cb5db9709..84212213e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 diff --git a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj index 04d22bca5..4ef508dba 100644 --- a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj +++ b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore.MySql/Elsa.Agents.Persistence.EntityFrameworkCore.MySql.csproj @@ -1,7 +1,7 @@  - net6.0;net8.0 + net8.0;net9.0 Provides an EF Core migrations for MySQL for the Agents Persistence module. elsa module agents semantic kernel llm ai persistence efcore entity framework core mysql diff --git a/src/modules/Elsa.Common/Elsa.Common.csproj b/src/modules/Elsa.Common/Elsa.Common.csproj index 7899635c9..654e988e5 100644 --- a/src/modules/Elsa.Common/Elsa.Common.csproj +++ b/src/modules/Elsa.Common/Elsa.Common.csproj @@ -21,8 +21,4 @@ - - - - diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj b/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj index c8ad4abb8..97efa0a73 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 Provides MySQL EF Core migrations for various modules. diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj index e9362407a..422ff83a5 100644 --- a/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj +++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0 + net8.0;net9.0 Provides EF Core migrations for Quartz.NET. diff --git a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj index d58faa3c6..1d9418298 100644 --- a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj +++ b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj @@ -1,7 +1,7 @@ - net6.0;net8.0;net9.0 + net8.0;net9.0 Provides integration with the Quartz.NET library and provide am implementation of Elsa's IJobScheduler using Quartz.NET. From ba983e042c3e9fa365f0ea03608039126549dc9a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 22 Dec 2024 10:06:55 +0100 Subject: [PATCH 003/166] Refactor `Directory.Packages.props` for consistent formatting Reformatted the `Directory.Packages.props` file to improve readability and ensure consistent indentation. This change has no impact on functionality but enhances maintainability and code clarity. --- Directory.Packages.props | 343 +++++++++++++++++---------------------- 1 file changed, 150 insertions(+), 193 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2457b4ded..5a14c8b2a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,195 +1,152 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 0992404d960c68de011ff89d992cc4834f79ee64 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 26 Dec 2024 11:17:54 +0100 Subject: [PATCH 004/166] Add integration test for workflow serialization Introduce a new integration test to verify workflow serialization functionality, ensuring that newly created workflow definitions can be serialized and deserialized correctly. Adjust default nullability for certain fields in `WorkflowDefinition` to improve consistency and prevent null reference issues. Minor argument update in `New` method of `WorkflowDefinitionPublisher`. --- .../Entities/WorkflowDefinition.cs | 8 ++--- .../Services/WorkflowDefinitionPublisher.cs | 2 +- .../Scenarios/Serialization/Tests.cs | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Serialization/Tests.cs diff --git a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs index 5f15fbdc1..d465f0765 100644 --- a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs +++ b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs @@ -12,7 +12,7 @@ public class WorkflowDefinition : VersionedEntity /// /// The logical ID of the workflow. This ID is the same across versions. /// - public string DefinitionId { get; set; } = default!; + public string DefinitionId { get; set; } = null!; /// /// The name of the workflow. @@ -67,17 +67,17 @@ public class WorkflowDefinition : VersionedEntity /// /// The name of the workflow materializer to interpret the or . /// - public string MaterializerName { get; set; } = default!; + public string MaterializerName { get; set; } = null!; /// /// Provider-specific data. /// public string? MaterializerContext { get; set; } - + /// /// A textual representation of the workflow. The data is to be interpreted by the configured materializer. /// - public string? StringData { get; set; } + public string StringData { get; set; } = null!; /// /// A binary representation of the workflow. The data is to be interpreted by the configured materializer. diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs index 75a20e2d4..0cdc9491e 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs @@ -46,7 +46,7 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher } /// - public WorkflowDefinition New(IActivity? root = default) + public WorkflowDefinition New(IActivity? root = null) { root ??= new Sequence(); var id = _identityGenerator.GenerateId(); diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Serialization/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Serialization/Tests.cs new file mode 100644 index 000000000..786c7a8ba --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Serialization/Tests.cs @@ -0,0 +1,31 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.Serialization; + +public class Tests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + private readonly IWorkflowDefinitionPublisher _publisher; + private readonly IActivitySerializer _serializer; + + public Tests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); + _services.GetRequiredService(); + _publisher = _services.GetRequiredService(); + _serializer = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Can serialize newly created workflow definition")] + public async Task Test1() + { + await _services.PopulateRegistriesAsync(); + var workflowDefinition = _publisher.New(); + var root = _serializer.Deserialize(workflowDefinition.StringData); + Assert.NotNull(root); + } +} \ No newline at end of file From 62df21cbaf172c8f91ac8c7798b43f0417d02642 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 26 Dec 2024 11:52:36 +0100 Subject: [PATCH 005/166] Make StringData property nullable in WorkflowDefinition Updated the `StringData` property to allow null values, improving flexibility and aligning with usage scenarios where the property may not always contain a value. This change ensures better handling of optional data. --- .../Elsa.Workflows.Management/Entities/WorkflowDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs index d465f0765..5eb87a812 100644 --- a/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs +++ b/src/modules/Elsa.Workflows.Management/Entities/WorkflowDefinition.cs @@ -77,7 +77,7 @@ public class WorkflowDefinition : VersionedEntity /// /// A textual representation of the workflow. The data is to be interpreted by the configured materializer. /// - public string StringData { get; set; } = null!; + public string? StringData { get; set; } /// /// A binary representation of the workflow. The data is to be interpreted by the configured materializer. From 59e131f1a3c41fd382cd48fe5836850e23507cff Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 26 Dec 2024 12:02:54 +0100 Subject: [PATCH 006/166] Set DatabaseProvider to SQLite in appsettings.json Switched the default database provider from PostgreSQL to SQLite in the appsettings.json configuration file. This change enables the application to use SQLite for its database operations. --- src/apps/Elsa.ServerAndStudio.Web/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json index e95f0771c..0e85580b3 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json @@ -15,7 +15,7 @@ "SqlServer": "Server=localhost;Database=elsa;User Id=sa;Password=Password12!", "PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60" }, - "DatabaseProvider": "PostgreSql", + "DatabaseProvider": "Sqlite", "Hosting": { "BaseUrl": "https://localhost:8080", "BasePath": "" From 3e76e3efb01a769016a1805e33b10c608fbbe37a Mon Sep 17 00:00:00 2001 From: sergergood Date: Fri, 27 Dec 2024 18:57:22 +0300 Subject: [PATCH 007/166] Added example of perfomance test --- test/performance/Directory.Build.props | 2 -- .../Elsa.Workflows.PerformanceTests/Config.cs | 14 ++++++++ .../ConsoleActivitiesBenchmark.cs | 32 +++++++++++++++++++ .../Elsa.Workflows.PerformanceTests.csproj | 4 +-- .../Program.cs | 4 +++ .../UnitTest1.cs | 9 ------ 6 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 test/performance/Elsa.Workflows.PerformanceTests/Config.cs create mode 100644 test/performance/Elsa.Workflows.PerformanceTests/ConsoleActivitiesBenchmark.cs create mode 100644 test/performance/Elsa.Workflows.PerformanceTests/Program.cs delete mode 100644 test/performance/Elsa.Workflows.PerformanceTests/UnitTest1.cs diff --git a/test/performance/Directory.Build.props b/test/performance/Directory.Build.props index 0e0af60ea..674d5ca9b 100644 --- a/test/performance/Directory.Build.props +++ b/test/performance/Directory.Build.props @@ -1,6 +1,4 @@ - - diff --git a/test/performance/Elsa.Workflows.PerformanceTests/Config.cs b/test/performance/Elsa.Workflows.PerformanceTests/Config.cs new file mode 100644 index 000000000..307deecce --- /dev/null +++ b/test/performance/Elsa.Workflows.PerformanceTests/Config.cs @@ -0,0 +1,14 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; + +namespace Elsa.Workflows.PerformanceTests; + +public class Config : ManualConfig +{ + public Config() + { + AddExporter(MarkdownExporter.GitHub); + AddDiagnoser(MemoryDiagnoser.Default); + } +} \ No newline at end of file diff --git a/test/performance/Elsa.Workflows.PerformanceTests/ConsoleActivitiesBenchmark.cs b/test/performance/Elsa.Workflows.PerformanceTests/ConsoleActivitiesBenchmark.cs new file mode 100644 index 000000000..9f214cf1b --- /dev/null +++ b/test/performance/Elsa.Workflows.PerformanceTests/ConsoleActivitiesBenchmark.cs @@ -0,0 +1,32 @@ +using BenchmarkDotNet.Attributes; +using Elsa.Extensions; +using Elsa.Workflows.Activities; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.PerformanceTests; + +[Config(typeof(Config))] +public class ConsoleActivitiesBenchmark +{ + private WriteLine _writeLineWorkflow; + private IWorkflowRunner _workflowRunner; + private ServiceProvider _serviceProvider; + + [GlobalSetup] + public void GlobalSetup() + { + var services = new ServiceCollection(); + services.AddElsa(); + + _serviceProvider = services.BuildServiceProvider(); + _workflowRunner = _serviceProvider.GetRequiredService(); + + _writeLineWorkflow = new WriteLine("Hello, World!"); + } + + [Benchmark] + public async Task WriteLine() => await _workflowRunner.RunAsync(_writeLineWorkflow); + + [GlobalCleanup] + public void GlobalCleanup() => _serviceProvider.Dispose(); +} \ No newline at end of file diff --git a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj index fc1b99f93..ee58b4574 100644 --- a/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj +++ b/test/performance/Elsa.Workflows.PerformanceTests/Elsa.Workflows.PerformanceTests.csproj @@ -6,11 +6,11 @@ enable false - true + Exe - + diff --git a/test/performance/Elsa.Workflows.PerformanceTests/Program.cs b/test/performance/Elsa.Workflows.PerformanceTests/Program.cs new file mode 100644 index 000000000..af752d90e --- /dev/null +++ b/test/performance/Elsa.Workflows.PerformanceTests/Program.cs @@ -0,0 +1,4 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher switcher = new(typeof(Program).Assembly); +switcher.Run(args); \ No newline at end of file diff --git a/test/performance/Elsa.Workflows.PerformanceTests/UnitTest1.cs b/test/performance/Elsa.Workflows.PerformanceTests/UnitTest1.cs deleted file mode 100644 index 5eecbccb1..000000000 --- a/test/performance/Elsa.Workflows.PerformanceTests/UnitTest1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Workflows.PerformanceTests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file From 710fe871171852bf00542d7329f360d4886d3bd7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 10:17:15 +0100 Subject: [PATCH 008/166] Update Elsa Studio package reference --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5911ae4e2..eb59aca7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -37,6 +37,6 @@ $(NoWarn);IL2026;IL2046;IL2057;IL2067;IL2070;IL2072;IL2075;IL2087;IL2091 - 3.3.0-preview.666 + 3.3.0-rc4 \ No newline at end of file From 07c46ae5a798da79e96b79ef135019e42834c31e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 11:04:38 +0100 Subject: [PATCH 009/166] Refactor configuration to use distinct ApiUrl and BaseUrl Updated to separate ApiUrl from BaseUrl in appsettings for clarity. Modified references in _Host.cshtml and Program.cs accordingly to streamline configuration and reduce potential for errors. --- .../Pages/_Host.cshtml | 35 +++++++++---------- src/apps/Elsa.ServerAndStudio.Web/Program.cs | 6 +++- .../Elsa.ServerAndStudio.Web/appsettings.json | 3 +- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml b/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml index c2217d30d..a335e4c85 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml +++ b/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml @@ -3,8 +3,7 @@ @inject IConfiguration Configuration; @{ var baseUrl = Configuration["Hosting:BaseUrl"]!; - var apiUrl = baseUrl + Url.Content("~/elsa/api"); - var basePath = Configuration["Hosting:BasePath"]!; + var apiUrl = Configuration["Hosting:ApiUrl"]!; } @@ -15,20 +14,20 @@ Elsa Studio 3.0 - - - - + + + + - - - - + + + + @@ -44,19 +43,19 @@ Reload 🗙 - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/apps/Elsa.ServerAndStudio.Web/Program.cs b/src/apps/Elsa.ServerAndStudio.Web/Program.cs index 581f25ec5..fb43aa4cd 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/apps/Elsa.ServerAndStudio.Web/Program.cs @@ -130,7 +130,11 @@ services if (useCaching) http.UseCache(); - http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options); + http.ConfigureHttpOptions = options => + { + options.BaseUrl = new Uri(configuration["Hosting:BaseUrl"]!); + options.BasePath = configuration["Http:BasePath"]; + }; }) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) .UseWebhooks(webhooks => webhooks.ConfigureSinks = options => builder.Configuration.GetSection("Webhooks:Sinks").Bind(options)) diff --git a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json index 0e85580b3..56cfb0e9c 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json @@ -18,7 +18,7 @@ "DatabaseProvider": "Sqlite", "Hosting": { "BaseUrl": "https://localhost:8080", - "BasePath": "" + "ApiUrl": "https://localhost:8080/elsa/api" }, "Identity": { "Tokens": { @@ -81,7 +81,6 @@ "DefaultSender": "noreply@crmservices.com" }, "Http": { - "BaseUrl": "https://localhost:5001", "BasePath": "/api/workflows" }, "Webhooks": { From 406975f64eef22ab6f389a035c13d87deacf7365 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 11:15:50 +0100 Subject: [PATCH 010/166] Refactor BaseUrl and ApiUrl configuration handling. Updated to use `Http:BaseUrl` for backward compatibility with `Hosting:BaseUrl`. Simplified ApiUrl construction by removing redundant configuration keys and standardizing the behavior using `ApiPrefix`. --- src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml | 7 +++---- src/apps/Elsa.ServerAndStudio.Web/Program.cs | 2 +- src/apps/Elsa.ServerAndStudio.Web/appsettings.json | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml b/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml index a335e4c85..81e0c82bc 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml +++ b/src/apps/Elsa.ServerAndStudio.Web/Pages/_Host.cshtml @@ -2,8 +2,8 @@ @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject IConfiguration Configuration; @{ - var baseUrl = Configuration["Hosting:BaseUrl"]!; - var apiUrl = Configuration["Hosting:ApiUrl"]!; + var baseUrl = Configuration["Hosting:BaseUrl"] ?? Configuration["Http:BaseUrl"]!; // HttpBaseUrl is for backward compatibility. + var apiUrl = $"{baseUrl.TrimEnd('/')}/{(Configuration["Hosting:ApiPrefix"] ?? "elsa/api").TrimStart('/')}"; } @@ -51,8 +51,7 @@ diff --git a/src/apps/Elsa.ServerAndStudio.Web/Program.cs b/src/apps/Elsa.ServerAndStudio.Web/Program.cs index fb43aa4cd..6d4aa8895 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/apps/Elsa.ServerAndStudio.Web/Program.cs @@ -132,7 +132,7 @@ services http.ConfigureHttpOptions = options => { - options.BaseUrl = new Uri(configuration["Hosting:BaseUrl"]!); + options.BaseUrl = new Uri(configuration["Hosting:BaseUrl"] ?? configuration["Http:BaseUrl"]!); // HttpBaseUrl is for backward compatibility. options.BasePath = configuration["Http:BasePath"]; }; }) diff --git a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json index 56cfb0e9c..4a3ccd5db 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/appsettings.json +++ b/src/apps/Elsa.ServerAndStudio.Web/appsettings.json @@ -18,7 +18,7 @@ "DatabaseProvider": "Sqlite", "Hosting": { "BaseUrl": "https://localhost:8080", - "ApiUrl": "https://localhost:8080/elsa/api" + "ApiPrefix": "/elsa/api" }, "Identity": { "Tokens": { From afa51f85faa493c60603b948935e4be06747511c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 14:13:32 +0100 Subject: [PATCH 011/166] Add support for ExpandoObject deserialization in ObjectConverter This update enables deserialization of JSON nodes into ExpandoObject via ObjectConverter. It improves type handling and interoperability with dynamic structures. --- src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 85238b016..93a0b2a77 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -104,6 +104,7 @@ public static class ObjectConverter return underlyingTargetType switch { { } t when t == typeof(string) => jsonNode.ToString(), + { } t when t == typeof(ExpandoObject) => JsonSerializer.Deserialize(jsonNode.ToJsonString()), { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), _ => jsonNode }; From 470399ed219f714651b173696bafd12e48a04c30 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 18:55:28 +0100 Subject: [PATCH 012/166] Refine trigger replacement logic with empty check. Added a check to avoid unnecessary operations when the removed list is empty in the ReplaceAsync method. This prevents potential redundant calls and ensures more efficient execution. --- .../Modules/Runtime/TriggerStore.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/TriggerStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/TriggerStore.cs index 2b2e26913..05ab1d03b 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/TriggerStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/TriggerStore.cs @@ -37,8 +37,14 @@ public class EFCoreTriggerStore(EntityStore /// public async ValueTask ReplaceAsync(IEnumerable removed, IEnumerable added, CancellationToken cancellationToken = default) { - var filter = new TriggerFilter { Ids = removed.Select(r => r.Id).ToList() }; - await DeleteManyAsync(filter, cancellationToken); + var removedList = removed.ToList(); + + if(removedList.Count > 0) + { + var filter = new TriggerFilter { Ids = removedList.Select(r => r.Id).ToList() }; + await DeleteManyAsync(filter, cancellationToken); + } + await store.SaveManyAsync(added, OnSaveAsync, cancellationToken); } From 021795f53a8a49b1dc1a641b8d5b7c4da6199e5d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 18:56:29 +0100 Subject: [PATCH 013/166] Enable MySQL primitive collections support and update configs Add support for EF Core primitive collections in MySQL by specifying `EnablePrimitiveCollectionsSupport()` in the DbContext configuration. Updated the Docker Compose file to rename the MySQL volume and adjusted appsettings.json to include a MySQL connection string and set MySQL as the default database provider. --- docker/docker-compose.yml | 4 ++-- src/apps/Elsa.Server.Web/Program.cs | 11 ++++++----- src/apps/Elsa.Server.Web/appsettings.json | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d8e421916..8f107f6ae 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,7 +23,7 @@ ports: - "3306:3306" volumes: - - mysql_data:/var/lib/mysql + - mysql_data1:/var/lib/mysql mongodb: image: mongo:latest @@ -104,6 +104,6 @@ volumes: postgres-data: - mysql_data: + mysql_data1: cockroachdb-data: mongodb_data: diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index b1427d8cd..44048d8e8 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -151,7 +151,7 @@ services ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString); + ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -187,7 +187,7 @@ services ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString); + ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -233,7 +233,7 @@ services ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString); + ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -361,7 +361,7 @@ services ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString); + ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -549,7 +549,8 @@ services if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 - if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) + ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); #endif if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); }); diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 1f6fd76cd..e1f95cb2f 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -18,6 +18,7 @@ "AllowedHosts": "*", "ConnectionStrings": { "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", + "MySql": "Server=localhost;Database=elsa;Uid=admin;Pwd=password;", "PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", "CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", "MongoDb": "mongodb://localhost:27017/elsa-workflows", @@ -25,7 +26,7 @@ "RabbitMq": "amqp://guest:guest@localhost:5672", "Redis": "localhost:6379,abortConnect=false" }, - "DatabaseProvider": "Sqlite", + "DatabaseProvider": "MySql", "Multitenancy": { "Tenants": [ { From 4ed58c67e6bcbe68dd087b34d11102112dfef879 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 20:06:33 +0100 Subject: [PATCH 014/166] Enable primitive collections and refactor MySQL EF options. Added support for primitive collections and parameterized collection translation in MySQL EF configurations. Removed redundant `EnablePrimitiveCollectionsSupport` calls in program configuration to centralize the behavior. --- src/apps/Elsa.Server.Web/Program.cs | 12 ++++++------ .../DbContextOptionsBuilder.cs | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 44048d8e8..fc36b8e1c 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -150,8 +150,8 @@ services else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 - else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); + else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) + ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -187,7 +187,7 @@ services ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); + ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -233,7 +233,7 @@ services ef.UsePostgreSql(postgresConnectionString!); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); + ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -361,7 +361,7 @@ services ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 else if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); + ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); @@ -550,7 +550,7 @@ services if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); #if !NET9_0 if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) - ef.UseMySql(mySqlConnectionString, null, opt => opt.EnablePrimitiveCollectionsSupport()); + ef.UseMySql(mySqlConnectionString); #endif if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); }); diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs index 8e64723ff..29af18c12 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs @@ -23,7 +23,9 @@ public static class DbContextOptionsBuilderExtensions db .MigrationsAssembly(options.GetMigrationsAssemblyName(migrationsAssembly)) .MigrationsHistoryTable(options.GetMigrationsHistoryTableName(), options.GetSchemaName()) - .SchemaBehavior(MySqlSchemaBehavior.Ignore); + .SchemaBehavior(MySqlSchemaBehavior.Ignore) + .EnablePrimitiveCollectionsSupport() + .TranslateParameterizedCollectionsToConstants(); configure?.Invoke(db); }); From c5bf6fd0e368a877422263c1e26ea72b56cb9ad6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 28 Dec 2024 20:07:50 +0100 Subject: [PATCH 015/166] Switch database provider to SQLite Updated the `DatabaseProvider` setting in `appsettings.json` from MySQL to SQLite. This change likely simplifies development or testing by using a lightweight, file-based database. --- src/apps/Elsa.Server.Web/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index e1f95cb2f..66598c7b9 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -26,7 +26,7 @@ "RabbitMq": "amqp://guest:guest@localhost:5672", "Redis": "localhost:6379,abortConnect=false" }, - "DatabaseProvider": "MySql", + "DatabaseProvider": "Sqlite", "Multitenancy": { "Tenants": [ { From 97a0b3612285c29809a8286ffb0bd904f0a4f09f Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Tue, 31 Dec 2024 10:22:28 +0000 Subject: [PATCH 016/166] Correct JsonEditor namesapce --- Elsa.sln | 4 ++-- src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs | 1 - .../UIHints/JsonEditor/JsonCodeOptionsProvider.cs | 2 +- .../UIHints/JsonEditor/JsonEditorUIHintHandler.cs | 1 - 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Elsa.sln b/Elsa.sln index 619e260be..5f0731b5a 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -84,6 +84,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 ProjectSection(SolutionItems) = preProject docker\.dockerignore = docker\.dockerignore docker\docker-compose-datadog.yml = docker\docker-compose-datadog.yml + docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml docker\docker-compose.yml = docker\docker-compose.yml docker\ElsaServer-Datadog.Dockerfile = docker\ElsaServer-Datadog.Dockerfile docker\ElsaServer.Dockerfile = docker\ElsaServer.Dockerfile @@ -91,7 +92,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile docker\init-db.sh = docker\init-db.sh docker\otel-collector-config.yaml = docker\otel-collector-config.yaml - docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Elasticsearch", "src\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj", "{3246883E-2FA7-4B4A-BDC5-99039A2869BC}" @@ -929,6 +929,7 @@ Global {690B0274-291F-4D9E-BA76-54EFF7D3E4BC} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {060FD0BA-BD78-48E1-A8A7-4906A5AD5E39} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} {169A82A5-2DB3-40EA-801E-14C08D743DF7} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1} + {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {01B96BB9-35E8-4364-ACB8-6D12A14D8DBA} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {47FBCB04-0C2D-453C-BE2F-7052CAC22524} = {EB3A7401-0DE3-476F-9E6F-057F1F4590FB} {B32DB9B2-AD6C-48A5-8682-4373CB045185} = {C80C8231-D35C-4ACC-9ED6-9F3DB221535E} @@ -966,7 +967,6 @@ Global {2F3E1026-5054-4E1F-899B-F1A7F70F9912} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37} - {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {BF934627-F531-44FB-BEC2-ECA801FF31E7} = {DD089B8B-DA73-492A-9010-F772D1C178DA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 67e5cb4a6..6720e3e7a 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -1,7 +1,6 @@ using Elsa.Common; using Elsa.Common.Features; using Elsa.Common.Serialization; -using Elsa.CSharp.Activities; using Elsa.Expressions.Features; using Elsa.Extensions; using Elsa.Features.Abstractions; diff --git a/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonCodeOptionsProvider.cs b/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonCodeOptionsProvider.cs index efebacc1c..c847644b3 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonCodeOptionsProvider.cs +++ b/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonCodeOptionsProvider.cs @@ -2,7 +2,7 @@ using System.Reflection; using Elsa.Workflows.UIHints.CodeEditor; // ReSharper disable once CheckNamespace -namespace Elsa.CSharp.Activities; +namespace Elsa.Workflows.UIHints.JsonEditor; internal class JsonCodeOptionsProvider : CodeEditorOptionsProviderBase { diff --git a/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonEditorUIHintHandler.cs b/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonEditorUIHintHandler.cs index d8033383a..2d4e6e403 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonEditorUIHintHandler.cs +++ b/src/modules/Elsa.Workflows.Core/UIHints/JsonEditor/JsonEditorUIHintHandler.cs @@ -1,5 +1,4 @@ using System.Reflection; -using Elsa.CSharp.Activities; namespace Elsa.Workflows.UIHints.JsonEditor; From e07ece5875116a8ca09fc139a61f2e7d742791fa Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 31 Dec 2024 13:54:19 +0100 Subject: [PATCH 017/166] Regenerate EF Core migrations for Runtime --- docker/docker-compose.yml | 6 +- scripts/migrations/efcore-3.1.sh | 2 +- scripts/migrations/efcore-3.2.sh | 2 +- src/apps/Directory.Build.props | 2 +- .../Runtime/20231024160940_Initial.cs | 2 +- .../Migrations/Runtime/20240329200651_V3_1.cs | 4 +- .../Migrations/Runtime/20240610184652_V3_2.cs | 4 +- ...ner.cs => 20241231124504_V3_3.Designer.cs} | 11 ++-- ...2211632_V3_3.cs => 20241231124504_V3_3.cs} | 66 +++---------------- .../RuntimeElsaDbContextModelSnapshot.cs | 9 ++- ...ner.cs => 20241231124811_V3_3.Designer.cs} | 10 +-- ...2211722_V3_3.cs => 20241231124811_V3_3.cs} | 3 +- .../RuntimeElsaDbContextModelSnapshot.cs | 8 +-- .../Runtime/20231024160952_Initial.cs | 2 +- .../Migrations/Runtime/20240329200711_V3_1.cs | 4 +- .../Migrations/Runtime/20240610184748_V3_2.cs | 4 +- ...ner.cs => 20241231124728_V3_3.Designer.cs} | 8 +-- ...2211710_V3_3.cs => 20241231124728_V3_3.cs} | 43 +++--------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- .../Runtime/20231024160944_Initial.cs | 2 +- .../Migrations/Runtime/20240329200657_V3_1.cs | 4 +- .../Migrations/Runtime/20240610184719_V3_2.cs | 4 +- ...ner.cs => 20241231124643_V3_3.Designer.cs} | 8 +-- ...2211646_V3_3.cs => 20241231124643_V3_3.cs} | 61 +++-------------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- .../Migrations/Runtime/20240329200705_V3_1.cs | 4 +- .../Migrations/Runtime/20240610184731_V3_2.cs | 4 +- ...ner.cs => 20241231122946_V3_3.Designer.cs} | 8 +-- ...2211658_V3_3.cs => 20241231122946_V3_3.cs} | 37 +++-------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- .../efcore-3.3.sh | 2 +- .../efcore-3.3.sh | 2 +- .../efcore-3.3.sh | 2 +- .../Entities/StoredBookmark.cs | 6 -- 34 files changed, 91 insertions(+), 261 deletions(-) rename src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/{20241212211632_V3_3.Designer.cs => 20241231124504_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/{20241212211632_V3_3.cs => 20241231124504_V3_3.cs} (86%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20241212211722_V3_3.Designer.cs => 20241231124811_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20241212211722_V3_3.cs => 20241231124811_V3_3.cs} (99%) rename src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/{20241212211710_V3_3.Designer.cs => 20241231124728_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/{20241212211710_V3_3.cs => 20241231124728_V3_3.cs} (91%) rename src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/{20241212211646_V3_3.Designer.cs => 20241231124643_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/{20241212211646_V3_3.cs => 20241231124643_V3_3.cs} (87%) rename src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/{20241212211658_V3_3.Designer.cs => 20241231122946_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/{20241212211658_V3_3.cs => 20241231122946_V3_3.cs} (91%) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8f107f6ae..76d7d7b40 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,7 +13,7 @@ - "5432:5432" mysql: - image: mysql:9.1.0 + image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: password @@ -23,7 +23,7 @@ ports: - "3306:3306" volumes: - - mysql_data1:/var/lib/mysql + - mysql_data2:/var/lib/mysql mongodb: image: mongo:latest @@ -104,6 +104,6 @@ volumes: postgres-data: - mysql_data1: + mysql_data2: cockroachdb-data: mongodb_data: diff --git a/scripts/migrations/efcore-3.1.sh b/scripts/migrations/efcore-3.1.sh index 158880741..b9357f071 100755 --- a/scripts/migrations/efcore-3.1.sh +++ b/scripts/migrations/efcore-3.1.sh @@ -16,6 +16,6 @@ for module in "${mods[@]}"; do echo "Updating migrations for $provider..." echo "Provider path: ${providerPath:?}/${migrationsPath}" echo "Migrations path: $migrationsPath" - ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.Common.Contracts.IElsaDbContextSchema --efOptions "migrations add V3_1 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath""" + ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_1 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath""" done done diff --git a/scripts/migrations/efcore-3.2.sh b/scripts/migrations/efcore-3.2.sh index 0a2edd654..c9fc38e44 100755 --- a/scripts/migrations/efcore-3.2.sh +++ b/scripts/migrations/efcore-3.2.sh @@ -16,6 +16,6 @@ for module in "${mods[@]}"; do echo "Updating migrations for $provider..." echo "Provider path: ${providerPath:?}/${migrationsPath}" echo "Migrations path: $migrationsPath" - ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.Common.Contracts.IElsaDbContextSchema --efOptions "migrations add V3_2 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath""" + ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_2 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath""" done done diff --git a/src/apps/Directory.Build.props b/src/apps/Directory.Build.props index 2e6c5f63d..1ead41344 100644 --- a/src/apps/Directory.Build.props +++ b/src/apps/Directory.Build.props @@ -1,6 +1,6 @@ - net9.0 + net8.0 latest enable enable diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20231024160940_Initial.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20231024160940_Initial.cs index 29faf3e35..cf3eb698a 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20231024160940_Initial.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20231024160940_Initial.cs @@ -1,4 +1,4 @@ - +using Elsa.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240329200651_V3_1.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240329200651_V3_1.cs index dcda40b23..0092d23c1 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240329200651_V3_1.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240329200651_V3_1.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime /// public partial class V3_1 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_1(IElsaDbContextSchema schema) + public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240610184652_V3_2.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240610184652_V3_2.cs index 891b4ba00..978783615 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240610184652_V3_2.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20240610184652_V3_2.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime /// public partial class V3_2 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_2(IElsaDbContextSchema schema) + public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs index 16473bb4d..66b24b533 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs @@ -3,6 +3,7 @@ using System; using Elsa.EntityFrameworkCore.Modules.Runtime; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -11,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241212211632_V3_3")] + [Migration("20241231124504_V3_3")] partial class V3_3 { /// @@ -20,9 +21,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { b.Property("Id") @@ -213,10 +216,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime .IsRequired() .HasColumnType("varchar(255)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("longtext"); - b.Property("CorrelationId") .HasColumnType("longtext"); diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs similarity index 86% rename from src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs index 1bd14cf16..ab100e254 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241212211632_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs @@ -24,10 +24,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", + migrationBuilder.RenameColumn( + name: "BookmarkId", schema: _schema.Schema, - table: "Bookmarks"); + table: "Bookmarks", + newName: "Id"); migrationBuilder.AddColumn( name: "TenantId", @@ -81,26 +82,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime nullable: true) .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AlterColumn( - name: "BookmarkId", - schema: _schema.Schema, - table: "Bookmarks", - type: "longtext", - nullable: false, - oldClrType: typeof(string), - oldType: "varchar(255)") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks", - type: "varchar(255)", - nullable: false, - defaultValue: "") - .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -123,12 +104,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime table: "KeyValuePairs", column: "Id"); - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -266,11 +241,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropIndex( name: "IX_StoredBookmark_TenantId", schema: _schema.Schema, @@ -306,11 +276,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -321,6 +286,12 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "Bookmarks", + newName: "BookmarkId"); + migrationBuilder.AlterColumn( name: "Key", schema: _schema.Schema, @@ -332,28 +303,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime .Annotation("MySql:CharSet", "utf8mb4") .OldAnnotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AlterColumn( - name: "BookmarkId", - schema: _schema.Schema, - table: "Bookmarks", - type: "varchar(255)", - nullable: false, - oldClrType: typeof(string), - oldType: "longtext") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AddPrimaryKey( name: "PK_KeyValuePairs", schema: _schema.Schema, table: "KeyValuePairs", column: "Key"); - - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "BookmarkId"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index b82e19325..5a4d7a6af 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -3,6 +3,7 @@ using System; using Elsa.EntityFrameworkCore.Modules.Runtime; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -17,9 +18,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { b.Property("Id") @@ -210,10 +213,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime .IsRequired() .HasColumnType("varchar(255)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("longtext"); - b.Property("CorrelationId") .HasColumnType("longtext"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs index 340dce4a6..f554d55de 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241212211722_V3_3")] + [Migration("20241231124811_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -76,7 +76,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("HasBookmarks") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("SerializedActivityState") .HasColumnType("NVARCHAR2(2000)"); @@ -216,10 +216,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .IsRequired() .HasColumnType("NVARCHAR2(450)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); - b.Property("CorrelationId") .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs similarity index 99% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs index 672dc5645..435afcca0 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241212211722_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs @@ -35,7 +35,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime ActivityTypeVersion = table.Column(type: "NUMBER(10)", nullable: false), ActivityName = table.Column(type: "NVARCHAR2(450)", nullable: true), StartedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), - HasBookmarks = table.Column(type: "NUMBER(1)", nullable: false), + HasBookmarks = table.Column(type: "BOOLEAN", nullable: false), Status = table.Column(type: "NVARCHAR2(450)", nullable: false), CompletedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true), SerializedActivityState = table.Column(type: "NVARCHAR2(2000)", nullable: true), @@ -78,7 +78,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime columns: table => new { Id = table.Column(type: "NVARCHAR2(450)", nullable: false), - BookmarkId = table.Column(type: "NVARCHAR2(2000)", nullable: false), ActivityTypeName = table.Column(type: "NVARCHAR2(450)", nullable: false), Hash = table.Column(type: "NVARCHAR2(450)", nullable: false), WorkflowInstanceId = table.Column(type: "NVARCHAR2(450)", nullable: false), diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index cad99cefa..41ef0f940 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -73,7 +73,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("HasBookmarks") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("SerializedActivityState") .HasColumnType("NVARCHAR2(2000)"); @@ -213,10 +213,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .IsRequired() .HasColumnType("NVARCHAR2(450)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); - b.Property("CorrelationId") .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20231024160952_Initial.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20231024160952_Initial.cs index 64226b886..8412c641b 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20231024160952_Initial.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20231024160952_Initial.cs @@ -1,4 +1,4 @@ - +using Elsa.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240329200711_V3_1.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240329200711_V3_1.cs index 00dfc44e8..821d29f2e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240329200711_V3_1.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240329200711_V3_1.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime /// public partial class V3_1 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_1(IElsaDbContextSchema schema) + public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240610184748_V3_2.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240610184748_V3_2.cs index 830c75445..d61e4aec4 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240610184748_V3_2.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20240610184748_V3_2.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime /// public partial class V3_2 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_2(IElsaDbContextSchema schema) + public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs index 516ec8a2a..c86657426 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241212211710_V3_3")] + [Migration("20241231124728_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -216,10 +216,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime .IsRequired() .HasColumnType("text"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .HasColumnType("text"); diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs similarity index 91% rename from src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs index 17e35fbe8..9f8654a83 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241212211710_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs @@ -24,10 +24,11 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", + migrationBuilder.RenameColumn( + name: "BookmarkId", schema: _schema.Schema, - table: "Bookmarks"); + table: "Bookmarks", + newName: "Id"); migrationBuilder.AddColumn( name: "TenantId", @@ -65,14 +66,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime type: "text", nullable: true); - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks", - type: "text", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -93,12 +86,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime table: "KeyValuePairs", column: "Id"); - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -226,11 +213,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropIndex( name: "IX_StoredBookmark_TenantId", schema: _schema.Schema, @@ -266,11 +248,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -281,17 +258,17 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "Bookmarks", + newName: "BookmarkId"); + migrationBuilder.AddPrimaryKey( name: "PK_KeyValuePairs", schema: _schema.Schema, table: "KeyValuePairs", column: "Key"); - - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "BookmarkId"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index a16cbd662..643dc2474 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -213,10 +213,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime .IsRequired() .HasColumnType("text"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("text"); - b.Property("CorrelationId") .HasColumnType("text"); diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20231024160944_Initial.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20231024160944_Initial.cs index af48889a8..85ae54f44 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20231024160944_Initial.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20231024160944_Initial.cs @@ -1,4 +1,4 @@ - +using Elsa.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240329200657_V3_1.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240329200657_V3_1.cs index abaac578e..342469eec 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240329200657_V3_1.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240329200657_V3_1.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime /// public partial class V3_1 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_1(IElsaDbContextSchema schema) + public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240610184719_V3_2.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240610184719_V3_2.cs index 67ba36b5f..4f0f4c3ef 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240610184719_V3_2.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20240610184719_V3_2.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime /// public partial class V3_2 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_2(IElsaDbContextSchema schema) + public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs index 117ee983b..fdc1b9e23 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241212211646_V3_3")] + [Migration("20241231124643_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -216,10 +216,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime .IsRequired() .HasColumnType("nvarchar(450)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("CorrelationId") .HasColumnType("nvarchar(max)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs similarity index 87% rename from src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs index b16e9c9ea..c8a162b3e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241212211646_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs @@ -24,10 +24,11 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", + migrationBuilder.RenameColumn( + name: "BookmarkId", schema: _schema.Schema, - table: "Bookmarks"); + table: "Bookmarks", + newName: "Id"); migrationBuilder.AddColumn( name: "TenantId", @@ -74,23 +75,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime type: "nvarchar(450)", nullable: true); - migrationBuilder.AlterColumn( - name: "BookmarkId", - schema: _schema.Schema, - table: "Bookmarks", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(450)"); - - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks", - type: "nvarchar(450)", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -111,12 +95,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime table: "KeyValuePairs", column: "Id"); - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -244,11 +222,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropIndex( name: "IX_StoredBookmark_TenantId", schema: _schema.Schema, @@ -284,11 +257,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "KeyValuePairs"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "Bookmarks"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -299,6 +267,12 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "Bookmarks", + newName: "BookmarkId"); + migrationBuilder.AlterColumn( name: "Key", schema: _schema.Schema, @@ -308,26 +282,11 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime oldClrType: typeof(string), oldType: "nvarchar(max)"); - migrationBuilder.AlterColumn( - name: "BookmarkId", - schema: _schema.Schema, - table: "Bookmarks", - type: "nvarchar(450)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - migrationBuilder.AddPrimaryKey( name: "PK_KeyValuePairs", schema: _schema.Schema, table: "KeyValuePairs", column: "Key"); - - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - schema: _schema.Schema, - table: "Bookmarks", - column: "BookmarkId"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 752f26c97..663fb6f45 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -213,10 +213,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime .IsRequired() .HasColumnType("nvarchar(450)"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("CorrelationId") .HasColumnType("nvarchar(max)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240329200705_V3_1.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240329200705_V3_1.cs index ddea13893..c6e73b527 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240329200705_V3_1.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240329200705_V3_1.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime /// public partial class V3_1 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_1(IElsaDbContextSchema schema) + public V3_1(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240610184731_V3_2.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240610184731_V3_2.cs index af8aed20a..d84cfc43f 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240610184731_V3_2.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20240610184731_V3_2.cs @@ -7,10 +7,10 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime /// public partial class V3_2 : Migration { - private readonly IElsaDbContextSchema _schema; + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; /// - public V3_2(IElsaDbContextSchema schema) + public V3_2(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) { _schema = schema; } diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs index 097593805..904170343 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs @@ -11,14 +11,14 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241212211658_V3_3")] + [Migration("20241231122946_V3_3")] partial class V3_3 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.20"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { @@ -210,10 +210,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime .IsRequired() .HasColumnType("TEXT"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("CorrelationId") .HasColumnType("TEXT"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs similarity index 91% rename from src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs index 193c3a8dc..4490f83bd 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241212211658_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs @@ -23,9 +23,10 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "PK_KeyValuePairs", table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", - table: "Bookmarks"); + migrationBuilder.RenameColumn( + name: "BookmarkId", + table: "Bookmarks", + newName: "Id"); migrationBuilder.AddColumn( name: "TenantId", @@ -58,13 +59,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime type: "TEXT", nullable: true); - migrationBuilder.AddColumn( - name: "Id", - table: "Bookmarks", - type: "TEXT", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", table: "Bookmarks", @@ -82,11 +76,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime table: "KeyValuePairs", column: "Id"); - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - table: "Bookmarks", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", columns: table => new @@ -195,10 +184,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "IX_SerializedKeyValuePair_TenantId", table: "KeyValuePairs"); - migrationBuilder.DropPrimaryKey( - name: "PK_Bookmarks", - table: "Bookmarks"); - migrationBuilder.DropIndex( name: "IX_StoredBookmark_TenantId", table: "Bookmarks"); @@ -227,10 +212,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "TenantId", table: "KeyValuePairs"); - migrationBuilder.DropColumn( - name: "Id", - table: "Bookmarks"); - migrationBuilder.DropColumn( name: "TenantId", table: "Bookmarks"); @@ -239,15 +220,15 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "TenantId", table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + table: "Bookmarks", + newName: "BookmarkId"); + migrationBuilder.AddPrimaryKey( name: "PK_KeyValuePairs", table: "KeyValuePairs", column: "Key"); - - migrationBuilder.AddPrimaryKey( - name: "PK_Bookmarks", - table: "Bookmarks", - column: "BookmarkId"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 16fbb9bcd..21a5c53c9 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.20"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { @@ -207,10 +207,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime .IsRequired() .HasColumnType("TEXT"); - b.Property("BookmarkId") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("CorrelationId") .HasColumnType("TEXT"); diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.3.sh b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.3.sh index 69fc0c617..dff83d4a2 100644 --- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.3.sh +++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/efcore-3.3.sh @@ -1 +1 @@ -ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.Common.Contracts.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/efcore-3.3.sh b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/efcore-3.3.sh index 69fc0c617..dff83d4a2 100644 --- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/efcore-3.3.sh +++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/efcore-3.3.sh @@ -1 +1 @@ -ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.Common.Contracts.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh index 69fc0c617..dff83d4a2 100644 --- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh +++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh @@ -1 +1 @@ -ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.Common.Contracts.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c SecretsDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Entities/StoredBookmark.cs b/src/modules/Elsa.Workflows.Runtime/Entities/StoredBookmark.cs index 8c44881ff..b34658e51 100644 --- a/src/modules/Elsa.Workflows.Runtime/Entities/StoredBookmark.cs +++ b/src/modules/Elsa.Workflows.Runtime/Entities/StoredBookmark.cs @@ -7,12 +7,6 @@ namespace Elsa.Workflows.Runtime.Entities; /// public class StoredBookmark : Entity { - [Obsolete("Use Id instead.")] public string BookmarkId - { - get => Id; - set => Id = value; - } - /// /// The name of the activity type associated with the bookmark. /// From f609013a7947d045a9571164c4168e18bfaae80e Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Wed, 1 Jan 2025 16:46:23 +0000 Subject: [PATCH 018/166] Adds SQL UIHints handler and provider. --- .../Features/WorkflowsFeature.cs | 3 +++ .../Elsa.Workflows.Core/UIHints/InputUIHints.cs | 1 + .../UIHints/SqlEditor/SqlCodeOptionsProvider.cs | 10 ++++++++++ .../UIHints/SqlEditor/SqlEditorUIHintHandler.cs | 16 ++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs create mode 100644 src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 6720e3e7a..2faa301fb 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -23,6 +23,7 @@ using Elsa.Workflows.Services; using Elsa.Workflows.UIHints.CheckList; using Elsa.Workflows.UIHints.Dropdown; using Elsa.Workflows.UIHints.JsonEditor; +using Elsa.Workflows.UIHints.SqlEditor; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Features; @@ -229,11 +230,13 @@ public class WorkflowsFeature : FeatureBase .AddScoped() .AddScoped() .AddScoped() + .AddScoped() // UI property handlers. .AddScoped() .AddScoped() .AddScoped() + .AddScoped() // Logger state generators. .AddSingleton(WorkflowLoggerStateGenerator) diff --git a/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs b/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs index 8465e1bbf..6616ea606 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs +++ b/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs @@ -21,5 +21,6 @@ public static class InputUIHints public const string OutputPicker = "output-picker"; public const string OutcomePicker = "outcome-picker"; public const string JsonEditor = "json-editor"; + public const string SqlEditor = "sql-editor"; public const string DynamicOutcomes = "dynamic-outcomes"; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs b/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs new file mode 100644 index 000000000..588ea20cf --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using Elsa.Workflows.UIHints.CodeEditor; + +// ReSharper disable once CheckNamespace +namespace Elsa.Workflows.UIHints.SqlEditor; + +internal class SqlCodeOptionsProvider : CodeEditorOptionsProviderBase +{ + protected override string GetLanguage(PropertyInfo propertyInfo, object? context) => "sql"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs b/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs new file mode 100644 index 000000000..642bc6878 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +namespace Elsa.Workflows.UIHints.SqlEditor; + +/// +public class SqlEditorUIHintHandler : IUIHintHandler +{ + /// + public string UIHint => InputUIHints.SqlEditor; + + /// + public ValueTask> GetPropertyUIHandlersAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken) + { + return new([typeof(SqlCodeOptionsProvider)]); + } +} \ No newline at end of file From 9828737ad019ac2083b1c5a7c508b3aca171259a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 1 Jan 2025 18:18:02 +0100 Subject: [PATCH 019/166] Add validation for null factory types and missing type strings Ensure `WorkerManager` and `TypeTypeConverter` handle invalid inputs gracefully by throwing exceptions. This prevents potential runtime issues caused by null factory types or unresolved type strings. --- src/modules/Elsa.Common/Serialization/TypeTypeConverter.cs | 2 +- src/modules/Elsa.Kafka/Implementations/WorkerManager.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Common/Serialization/TypeTypeConverter.cs b/src/modules/Elsa.Common/Serialization/TypeTypeConverter.cs index 4d89b6c5e..63e1b210e 100644 --- a/src/modules/Elsa.Common/Serialization/TypeTypeConverter.cs +++ b/src/modules/Elsa.Common/Serialization/TypeTypeConverter.cs @@ -18,7 +18,7 @@ public class TypeTypeConverter : TypeConverter { if (TypeAliasRegistry.GetType(stringValue) is { } type) return type; - return Type.GetType(stringValue); + return Type.GetType(stringValue) ?? throw new InvalidOperationException($"Type '{stringValue}' not found."); } return base.ConvertFrom(context, culture, value); } diff --git a/src/modules/Elsa.Kafka/Implementations/WorkerManager.cs b/src/modules/Elsa.Kafka/Implementations/WorkerManager.cs index b3383d123..364ffd6a7 100644 --- a/src/modules/Elsa.Kafka/Implementations/WorkerManager.cs +++ b/src/modules/Elsa.Kafka/Implementations/WorkerManager.cs @@ -165,6 +165,10 @@ public class WorkerManager(IHasher hasher, IServiceScopeFactory scopeFactory) : private async Task CreateWorkerAsync(IServiceProvider serviceProvider, ConsumerDefinition consumerDefinition, CancellationToken cancellationToken) { var factoryType = consumerDefinition.FactoryType; + + if(factoryType == null!) + throw new InvalidOperationException("Worker factory type not specified."); + var consumerFactory = ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, factoryType) as IConsumerFactory; if (consumerFactory == null) From adf2a672b87d0d7f2b29ec48e2a71f7c3a4701a5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 1 Jan 2025 18:18:48 +0100 Subject: [PATCH 020/166] Restore .NET 8 packages Updated `UseElsaMySql` method to use default `null` values instead of `default` and added a conditional compilation directive for `.NET 9.0`. Also introduced new package versions for targeting `.NET 8.0` and `.NET 9.0` to ensure compatibility and maintainability. --- Directory.Packages.props | 42 +++++++++++++++++++ .../DbContextOptionsBuilder.cs | 13 ++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a14c8b2a..dd2e28eac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -110,6 +110,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs index 29af18c12..403d09f77 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/DbContextOptionsBuilder.cs @@ -14,8 +14,12 @@ public static class DbContextOptionsBuilderExtensions /// /// Configures Entity Framework Core with MySQL. /// - public static DbContextOptionsBuilder UseElsaMySql(this DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, - ElsaDbContextOptions? options = default, ServerVersion? serverVersion = default, Action? configure = default) => + public static DbContextOptionsBuilder UseElsaMySql(this DbContextOptionsBuilder builder, + Assembly migrationsAssembly, + string connectionString, + ElsaDbContextOptions? options = null, + ServerVersion? serverVersion = null, + Action? configure = null) => builder .UseElsaDbContextOptions(options) .UseMySql(connectionString, serverVersion ?? ServerVersion.AutoDetect(connectionString), db => @@ -25,7 +29,10 @@ public static class DbContextOptionsBuilderExtensions .MigrationsHistoryTable(options.GetMigrationsHistoryTableName(), options.GetSchemaName()) .SchemaBehavior(MySqlSchemaBehavior.Ignore) .EnablePrimitiveCollectionsSupport() - .TranslateParameterizedCollectionsToConstants(); +#if NET9_0 + .TranslateParameterizedCollectionsToConstants() +#endif + ; configure?.Invoke(db); }); From fdd71f80071612b8b6aef91d35393ac70334413f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 1 Jan 2025 22:07:49 +0100 Subject: [PATCH 021/166] Handle exceptions in variable loading and fix type checking. Added try-catch block to log failures when reading variables from storage, ensuring robust error handling. Also refined type checking for `ExpandoObject` deserialization in `ObjectConverter` to prevent invalid operations. --- .../Helpers/ObjectConverter.cs | 2 +- .../Services/VariablePersistenceManager.cs | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 93a0b2a77..7bb35b1b4 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -104,7 +104,7 @@ public static class ObjectConverter return underlyingTargetType switch { { } t when t == typeof(string) => jsonNode.ToString(), - { } t when t == typeof(ExpandoObject) => JsonSerializer.Deserialize(jsonNode.ToJsonString()), + { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()), { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), _ => jsonNode }; diff --git a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs index 3b351eddf..2ca6dcc86 100644 --- a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs +++ b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs @@ -40,18 +40,26 @@ public class VariablePersistenceManager(IStorageDriverManager storageDriverManag continue; var id = GetStateId(variable); - var value = await driver.ReadAsync(id, storageDriverContext); - if (value == null) continue; - register.Declare(variable); - - if (!variable.TryParseValue(value, out var parsedValue)) + try { - logger.LogWarning("Failed to parse value for variable {VariableId} of type {VariableType} with value {Value}", variable.Id, variable.GetVariableType().FullName, value); - continue; - } + var value = await driver.ReadAsync(id, storageDriverContext); + if (value == null) continue; - variable.Set(register, parsedValue); + register.Declare(variable); + + if (!variable.TryParseValue(value, out var parsedValue)) + { + logger.LogWarning("Failed to parse value for variable {VariableId} of type {VariableType} with value {Value}", variable.Id, variable.GetVariableType().FullName, value); + continue; + } + + variable.Set(register, parsedValue); + } + catch (Exception e) + { + logger.LogError(e, "Failed to read variable {VariableId} from storage driver {StorageDriverType}", variable.Id, driver.GetType().FullName); + } } } } From c4e04c4434c297f9e00bb991c3c7cb22b75a4f99 Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Wed, 1 Jan 2025 23:14:09 +0000 Subject: [PATCH 022/166] Adds SQL Activities. Includes implimentations for MS SQL Server, PostgreSql, MySql and Sqlite. --- Directory.Packages.props | 302 +++++++++--------- Elsa.sln | 38 +++ .../Elsa.Server.Web/Elsa.Server.Web.csproj | 79 ++--- src/apps/Elsa.Server.Web/Program.cs | 15 + .../Elsa.Sql.MySql/Elsa.Sql.MySql.csproj | 18 ++ src/modules/Elsa.Sql.MySql/FodyWeavers.xml | 3 + src/modules/Elsa.Sql.MySql/MySqlClient.cs | 55 ++++ .../Elsa.Sql.PostgreSql.csproj | 18 ++ .../Elsa.Sql.PostgreSql/FodyWeavers.xml | 3 + .../Elsa.Sql.PostgreSql/PostgreSqlClient.cs | 55 ++++ .../Elsa.Sql.SqlServer.csproj | 18 ++ .../Elsa.Sql.SqlServer/FodyWeavers.xml | 3 + .../Elsa.Sql.SqlServer/SqlServerClient.cs | 55 ++++ .../Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj | 18 ++ src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml | 3 + src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 55 ++++ src/modules/Elsa.Sql/Activities/SqlCommand.cs | 71 ++++ src/modules/Elsa.Sql/Activities/SqlQuery.cs | 72 +++++ .../Elsa.Sql/Activities/SqlSingleValue.cs | 71 ++++ src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 50 +++ src/modules/Elsa.Sql/Client/ISqlClient.cs | 27 ++ .../Elsa.Sql/Contracts/ISqlClientFactory.cs | 14 + .../Contracts/ISqlClientNamesProvider.cs | 11 + src/modules/Elsa.Sql/Elsa.Sql.csproj | 15 + .../Elsa.Sql/Extensions/ModuleExtensions.cs | 22 ++ .../Elsa.Sql/Factory/SqlClientFactory.cs | 43 +++ src/modules/Elsa.Sql/Features/SqlFeature.cs | 57 ++++ src/modules/Elsa.Sql/FodyWeavers.xml | 3 + .../Implimentations/SqlClientNamesProvider.cs | 23 ++ src/modules/Elsa.Sql/Services/ClientStore.cs | 36 +++ .../UIHints/SqlClientsDropDownProvider.cs | 18 ++ 31 files changed, 1084 insertions(+), 187 deletions(-) create mode 100644 src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj create mode 100644 src/modules/Elsa.Sql.MySql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.MySql/MySqlClient.cs create mode 100644 src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj create mode 100644 src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs create mode 100644 src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj create mode 100644 src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs create mode 100644 src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj create mode 100644 src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.Sqlite/SqliteClient.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlCommand.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlQuery.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlSingleValue.cs create mode 100644 src/modules/Elsa.Sql/Client/BaseSqlClient.cs create mode 100644 src/modules/Elsa.Sql/Client/ISqlClient.cs create mode 100644 src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs create mode 100644 src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs create mode 100644 src/modules/Elsa.Sql/Elsa.Sql.csproj create mode 100644 src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs create mode 100644 src/modules/Elsa.Sql/Factory/SqlClientFactory.cs create mode 100644 src/modules/Elsa.Sql/Features/SqlFeature.cs create mode 100644 src/modules/Elsa.Sql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs create mode 100644 src/modules/Elsa.Sql/Services/ClientStore.cs create mode 100644 src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a14c8b2a..408e07ec6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,152 +1,154 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elsa.sln b/Elsa.sln index 5f0731b5a..1e29473d9 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -387,6 +387,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.Ent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Kafka", "src\modules\Elsa.Kafka\Elsa.Kafka.csproj", "{BF934627-F531-44FB-BEC2-ECA801FF31E7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sql", "sql", "{A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql", "src\modules\Elsa.Sql\Elsa.Sql.csproj", "{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.MySql", "src\modules\Elsa.Sql.MySql\Elsa.Sql.MySql.csproj", "{3BED411B-79B5-4CCC-BD46-9549A427B908}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.PostgreSql", "src\modules\Elsa.Sql.PostgreSql\Elsa.Sql.PostgreSql.csproj", "{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.Sqlite", "src\modules\Elsa.Sql.Sqlite\Elsa.Sql.Sqlite.csproj", "{FA5E857F-B173-4B5D-8049-B817A210DEF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.SqlServer", "src\modules\Elsa.Sql.SqlServer\Elsa.Sql.SqlServer.csproj", "{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -823,6 +835,26 @@ Global {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.Build.0 = Release|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.Build.0 = Release|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.Build.0 = Release|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.Build.0 = Release|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.Build.0 = Release|Any CPU + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -968,6 +1000,12 @@ Global {D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37} {BF934627-F531-44FB-BEC2-ECA801FF31E7} = {DD089B8B-DA73-492A-9010-F772D1C178DA} + {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {3BED411B-79B5-4CCC-BD46-9549A427B908} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {FA5E857F-B173-4B5D-8049-B817A210DEF5} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index bf4d53171..506defc69 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -9,9 +9,9 @@ - + - + @@ -19,48 +19,53 @@ + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - + + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index fc36b8e1c..ded168c62 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -34,6 +34,11 @@ using Elsa.Server.Web; using Elsa.Server.Web.Extensions; using Elsa.Server.Web.Filters; using Elsa.Server.Web.Messages; +using Elsa.Sql.Extensions; +using Elsa.Sql.MySql; +using Elsa.Sql.PostgreSql; +using Elsa.Sql.Sqlite; +using Elsa.Sql.SqlServer; using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.Workflows.Api; @@ -340,6 +345,16 @@ services if (useCaching) http.UseCache(); }) + .UseSql(options => + { + options.Clients = client => + { + client.Register("MySql"); + client.Register("PostgreSql"); + client.Register("Sqlite"); + client.Register("Sql Server"); + }; + }) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) .UseAlterations(alterations => { diff --git a/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj new file mode 100644 index 000000000..54c6148dc --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with MySql databases. + + elsa module activities sql mysql + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.MySql/FodyWeavers.xml b/src/modules/Elsa.Sql.MySql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs new file mode 100644 index 000000000..60d84fd3e --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -0,0 +1,55 @@ +using MySql.Data.MySqlClient; +using Elsa.Sql.Client; +using System.Data; + +namespace Elsa.Sql.MySql; + +public class MySqlClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// MySql client implimentation. + /// + /// + public MySqlClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj new file mode 100644 index 000000000..82a290850 --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with PostgreSql databases. + + elsa module activities sql postgresql + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml b/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs new file mode 100644 index 000000000..495439f30 --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs @@ -0,0 +1,55 @@ +using Npgsql; +using Elsa.Sql.Client; +using System.Data; + +namespace Elsa.Sql.PostgreSql; + +public class PostgreSqlClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// PostgreSQL client implimentation. + /// + /// + public PostgreSqlClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj new file mode 100644 index 000000000..cab38ce95 --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with Microsoft SQL Server databases. + + elsa module activities sql mssqlserver sqlserver + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml b/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs new file mode 100644 index 000000000..f5b57c263 --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -0,0 +1,55 @@ +using System.Data; +using Elsa.Sql.Client; +using Microsoft.Data.SqlClient; + +namespace Elsa.Sql.SqlServer; + +public class SqlServerClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// Microsoft SQL server client implimentation. + /// + /// + public SqlServerClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj new file mode 100644 index 000000000..43bf2bf5a --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with Sqlite databases. + + elsa module activities sql sqlite + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml b/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs new file mode 100644 index 000000000..d67e69342 --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -0,0 +1,55 @@ +using System.Data; +using Elsa.Sql.Client; +using Microsoft.Data.Sqlite; + +namespace Elsa.Sql.Sqlite; + +public class SqliteClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// Sqlite client implimentation. + /// + /// + public SqliteClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlCommand.cs b/src/modules/Elsa.Sql/Activities/SqlCommand.cs new file mode 100644 index 000000000..0038e1aeb --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlCommand.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL command and returns the number of rows affected. +/// +[Activity("Elsa", "SQL", "Execute given SQL command and returns the number of rows affected.", DisplayName = "SQL Command", Kind = ActivityKind.Task)] +public class SqlCommand : Activity +{ + /// + /// + /// + public SqlCommand([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base (source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Command to run against the database. + /// + [Input( + Description = "Command to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Command { get; set; } = default!; + + + /// + /// The number of affected rows. + /// + [Output( + Description = "The number of rows affected.")] + public Output Result { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var result = await client.ExecuteCommandAsync(Command.GetOrDefault(context)); + context.Set(Result, result); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/Elsa.Sql/Activities/SqlQuery.cs new file mode 100644 index 000000000..c1d922149 --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlQuery.cs @@ -0,0 +1,72 @@ +using System.Data; +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL query and return the resulting data. +/// +[Activity("Elsa", "SQL", "Execute given SQL query and return the resulting data.", DisplayName = "SQL Query", Kind = ActivityKind.Task)] +public class SqlQuery : Activity +{ + /// + /// + /// + public SqlQuery([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Query to run against the database. + /// + [Input( + Description = "Query to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Query { get; set; } = default!; + + + /// + /// of queried results. + /// + [Output( + Description = "DataSet of queried results.")] + public Output Results { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var results = await client.ExecuteQueryAsync(Query.GetOrDefault(context)); + context.Set(Results, results); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs new file mode 100644 index 000000000..037863911 --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL command and return a single result. +/// +[Activity("Elsa", "SQL", "Execute given SQL command and return a single result.", DisplayName = "SQL Single Value", Kind = ActivityKind.Task)] +public class SqlSingleValue : Activity +{ + /// + /// + /// + public SqlSingleValue([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Query to run against the database. + /// + [Input( + Description = "Query to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Query { get; set; } = default!; + + + /// + /// Command result. + /// + [Output( + Description = "Command result.")] + public Output Result { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var result = await client.ExecuteScalarAsync(Query.GetOrDefault(context)); + context.Set(Result, result); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs new file mode 100644 index 000000000..0298d5493 --- /dev/null +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -0,0 +1,50 @@ +using System.Data; + +namespace Elsa.Sql.Client; + +public abstract class BaseSqlClient +{ + /// + /// Returns data as a . + /// + /// Reader to return data from. + /// of data. + protected static DataSet ReadAsDataSet(IDataReader reader) + { + var dataSet = new DataSet("dataset"); + + var schematable = reader.GetSchemaTable(); + var data = new DataSet(); + dataSet.Tables.Add(ReadAsDataTable(reader)); + + return dataSet; + } + + /// + /// Returns data as a . + /// + /// Reader to return data from. + /// of data. + protected static DataTable ReadAsDataTable(IDataReader reader) + { + var data = new DataTable(); + var schemaTable =reader.GetSchemaTable(); + + foreach (DataRow row in schemaTable.Rows) + { + string colName = row.Field("ColumnName"); + Type t = row.Field("DataType"); + data.Columns.Add(colName, t); + } + + while (reader.Read()) + { + var newRow = data.Rows.Add(); + foreach (DataColumn col in data.Columns) + { + newRow[col.ColumnName] = reader[col.ColumnName]; + } + } + return data; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/ISqlClient.cs b/src/modules/Elsa.Sql/Client/ISqlClient.cs new file mode 100644 index 000000000..8e4e3fe43 --- /dev/null +++ b/src/modules/Elsa.Sql/Client/ISqlClient.cs @@ -0,0 +1,27 @@ +using System.Data; + +namespace Elsa.Sql.Client; + +public interface ISqlClient +{ + /// + /// Asyncronously executes a Transact-SQL statement against the connection and returns the number of rows affected. + /// + /// The command to execute + /// The number of rows affected. + public Task ExecuteCommandAsync(string sqlCommand); + + /// + /// Asyncronously executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored. + /// + /// The query to execute + /// The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters. + public Task ExecuteScalarAsync(string sqlQuery); + + /// + /// Asyncronously executes the query, and returns a dataset of data returned by the query. + /// + /// Query to execute + /// DataSet of the quiried data + public Task ExecuteQueryAsync(string sqlQuery); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs b/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs new file mode 100644 index 000000000..de2d7771c --- /dev/null +++ b/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs @@ -0,0 +1,14 @@ +using Elsa.Sql.Client; + +namespace Elsa.Sql.Contracts; + +public interface ISqlClientFactory +{ + /// + /// Create an instance of the registered client. + /// + /// The name of the registered client to create. This can either be clientName used during registration or the default nameof(client) itself. + /// Connection string. + /// + public ISqlClient CreateClient(string clientName, string connectionString); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs b/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs new file mode 100644 index 000000000..b1b48eef4 --- /dev/null +++ b/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs @@ -0,0 +1,11 @@ +namespace Elsa.Sql.Contracts; + +public interface ISqlClientNamesProvider +{ + /// + /// Returns a dictionary of registered clients. + /// + /// A token to monitor cancellation requests. + /// A of registered client names their . + Task> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj new file mode 100644 index 000000000..bfb24be1e --- /dev/null +++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj @@ -0,0 +1,15 @@ + + + + + Provides activities to interact with sql databases. + + elsa module activities sql + + + + + + + + diff --git a/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..de787d172 --- /dev/null +++ b/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs @@ -0,0 +1,22 @@ +using Elsa.Features.Services; +using Elsa.Sql.Features; + +namespace Elsa.Sql.Extensions; + +/// +/// Provides methods to install and configure SQL client features. +/// +public static class ModuleExtensions +{ + /// + /// Adds the feature to the system. + /// + /// + /// + /// + public static IModule UseSql(this IModule configuration, Action? configure= default) + { + configuration.Configure(configure); + return configuration; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs b/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs new file mode 100644 index 000000000..4422b099b --- /dev/null +++ b/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs @@ -0,0 +1,43 @@ +using Elsa.Sql.Client; +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Factory; + +/// +/// SQL client factory +/// +public class SqlClientFactory : ISqlClientFactory +{ + private readonly IServiceProvider _serviceProvider; + + public SqlClientFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + /// + /// + /// + public ISqlClient CreateClient(string clientName, string connectionString) + { + if (string.IsNullOrEmpty(clientName)) + { + throw new ArgumentException($"Client name can not be empty or null.", nameof(clientName)); + } + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException($"Connection string can not be empty or null.", nameof(connectionString)); + } + if (_serviceProvider.GetRequiredService().Clients.TryGetValue(clientName, out var clientType)) + { + try + { + return ActivatorUtilities.CreateInstance(_serviceProvider, clientType, connectionString) as ISqlClient; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to create instance of '{clientName}' of type '{clientType}'.", ex); + } + } + throw new ArgumentException($"No registered SQL client provider for '{clientName}'."); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Features/SqlFeature.cs b/src/modules/Elsa.Sql/Features/SqlFeature.cs new file mode 100644 index 000000000..b0e4cbab0 --- /dev/null +++ b/src/modules/Elsa.Sql/Features/SqlFeature.cs @@ -0,0 +1,57 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Elsa.Sql.Contracts; +using Elsa.Sql.Factory; +using Elsa.Sql.Implimentations; +using Elsa.Sql.Services; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Features; + +/// +/// Setup SQL client features +/// +public class SqlFeature : FeatureBase +{ + /// + /// Set a callback to configure . + /// + public Action Clients { get; set; } = _ => { }; + + /// + /// + /// + /// + public SqlFeature(IModule module) : base(module) + { + } + + /// + /// + /// + public override void Configure() + { + Module.AddActivitiesFrom(); + } + + /// + /// + /// + public override void Apply() + { + Services + .AddSingleton(provider => + { + ClientStore clientRegistry = new(); + Clients.Invoke(clientRegistry); + return clientRegistry; + }) + .AddSingleton() + + .AddScoped() + .AddScoped(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/FodyWeavers.xml b/src/modules/Elsa.Sql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs b/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs new file mode 100644 index 000000000..5c2cba45c --- /dev/null +++ b/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs @@ -0,0 +1,23 @@ +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Implimentations; + +/// +/// Returns registered client names +/// +public class SqlClientNamesProvider : ISqlClientNamesProvider +{ + private readonly IServiceProvider _serviceProvider; + + public SqlClientNamesProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + /// + /// + /// + public Task> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_serviceProvider.GetRequiredService().Clients); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Services/ClientStore.cs b/src/modules/Elsa.Sql/Services/ClientStore.cs new file mode 100644 index 000000000..d76727aea --- /dev/null +++ b/src/modules/Elsa.Sql/Services/ClientStore.cs @@ -0,0 +1,36 @@ +using Elsa.Sql.Client; + +namespace Elsa.Sql.Services; +public class ClientStore +{ + private readonly Dictionary clients = new(); + + /// + /// Dictionary of registered clients and their type. + /// + public IReadOnlyDictionary Clients => clients; + + /// + /// Registers the specified client type with the store. + /// The client type must inherit from . + /// + /// + /// The type of the client to be registered. The client must be a class that implements the interface. + /// + /// + /// The name of the client to register. If not provided, the name defaults to nameof(TClient). + /// This value is used as a key to identify the client in the store. + /// + /// + /// Thrown when a client with the same name is already registered in the store. + /// + /// + /// This method registers a client type to the store using a unique key. The key is either the provided or the default name derived from . + /// + public void Register(string? name) where TClient : class, ISqlClient + { + var key = string.IsNullOrEmpty(name) ? nameof(TClient) : name; + if (clients.ContainsKey(key)) { throw new InvalidOperationException($"Client with key '{name}' is already registered."); } + clients.Add(key, typeof(TClient)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs b/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs new file mode 100644 index 000000000..019271df6 --- /dev/null +++ b/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Elsa.Sql.Contracts; +using Elsa.Workflows.UIHints.Dropdown; + +namespace Elsa.Sql.UIHints; + +/// +/// Provides registered clients for the Client input field. +/// +/// +public class SqlClientsDropDownProvider(ISqlClientNamesProvider sqlClientNamesProvider) : DropDownOptionsProviderBase +{ + protected override async ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken) + { + var clients = await sqlClientNamesProvider.GetRegisteredSqlClientNamesAsync(cancellationToken); + return clients.Select(x => new SelectListItem(x.Key, x.Key)).ToList(); + } +} \ No newline at end of file From 6c0f7a48e7cc9e0469acf96a244e668ac4caa804 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 2 Jan 2025 11:11:42 +0100 Subject: [PATCH 023/166] Refactor recurring tasks scheduling logic. Removed `ConfigureRecurringTasksScheduleStartupTask` and moved its functionality into `RecurringTaskScheduleManager`. Simplified recurring task configuration and streamlined dependencies, improving maintainability. Added retention policies to `Elsa.Server.Web`. --- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + src/apps/Elsa.Server.Web/Program.cs | 16 +++++++++ .../Features/MultitenancyFeature.cs | 2 -- .../EventHandlers/StartRecurringTasks.cs | 4 +-- ...figureRecurringTasksScheduleStartupTask.cs | 26 -------------- .../RecurringTaskScheduleManager.cs | 34 ++++++++++++------- 6 files changed, 41 insertions(+), 42 deletions(-) delete mode 100644 src/modules/Elsa.Common/RecurringTasks/ConfigureRecurringTasksScheduleStartupTask.cs diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index bf4d53171..8801d54d9 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -14,6 +14,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index fc36b8e1c..3e5a9b3f9 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -27,6 +27,8 @@ using Elsa.MongoDb.Modules.Management; using Elsa.MongoDb.Modules.Runtime; using Elsa.MongoDb.Modules.Tenants; using Elsa.OpenTelemetry.Middleware; +using Elsa.Retention.Extensions; +using Elsa.Retention.Models; using Elsa.Secrets.Extensions; using Elsa.Secrets.Management.Tasks; using Elsa.Secrets.Persistence; @@ -36,6 +38,7 @@ using Elsa.Server.Web.Filters; using Elsa.Server.Web.Messages; using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; +using Elsa.Workflows; using Elsa.Workflows.Api; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; @@ -513,6 +516,19 @@ services .UseSecretsScripting() ; } + + elsa.UseRetention(r => + { + r.SweepInterval = TimeSpan.FromHours(5); + r.AddDeletePolicy("Delete all finished workflows", sp => + { + var filter = new RetentionWorkflowInstanceFilter + { + WorkflowStatus = WorkflowStatus.Finished + }; + return filter; + }); + }); if (useMultitenancy) { diff --git a/src/modules/Elsa.Common/Features/MultitenancyFeature.cs b/src/modules/Elsa.Common/Features/MultitenancyFeature.cs index 84facff7b..89bbb7d66 100644 --- a/src/modules/Elsa.Common/Features/MultitenancyFeature.cs +++ b/src/modules/Elsa.Common/Features/MultitenancyFeature.cs @@ -2,7 +2,6 @@ using Elsa.Common.Multitenancy; using Elsa.Common.Multitenancy.EventHandlers; using Elsa.Common.Multitenancy.HostedServices; using Elsa.Common.RecurringTasks; -using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; using Microsoft.Extensions.DependencyInjection; @@ -52,7 +51,6 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module) .AddSingleton() .AddSingleton() .AddSingleton() - .AddStartupTask() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/modules/Elsa.Common/Multitenancy/EventHandlers/StartRecurringTasks.cs b/src/modules/Elsa.Common/Multitenancy/EventHandlers/StartRecurringTasks.cs index c89c79c70..f83479af9 100644 --- a/src/modules/Elsa.Common/Multitenancy/EventHandlers/StartRecurringTasks.cs +++ b/src/modules/Elsa.Common/Multitenancy/EventHandlers/StartRecurringTasks.cs @@ -7,7 +7,7 @@ namespace Elsa.Common.Multitenancy.EventHandlers; public class StartRecurringTasks(RecurringTaskScheduleManager scheduleManager, ILogger logger) : ITenantActivatedEvent, ITenantDeactivatedEvent { private readonly ICollection _scheduledTimers = new List(); - private CancellationTokenSource _cancellationTokenSource = default!; + private CancellationTokenSource _cancellationTokenSource = null!; public async Task TenantActivatedAsync(TenantActivatedEventArgs args) { @@ -16,7 +16,7 @@ public class StartRecurringTasks(RecurringTaskScheduleManager scheduleManager, I var tenantScope = args.TenantScope; var tasks = tenantScope.ServiceProvider.GetServices().ToList(); var taskExecutor = tenantScope.ServiceProvider.GetRequiredService(); - + foreach (var task in tasks) { var schedule = scheduleManager.GetScheduleFor(task.GetType()); diff --git a/src/modules/Elsa.Common/RecurringTasks/ConfigureRecurringTasksScheduleStartupTask.cs b/src/modules/Elsa.Common/RecurringTasks/ConfigureRecurringTasksScheduleStartupTask.cs deleted file mode 100644 index a376f3b8e..000000000 --- a/src/modules/Elsa.Common/RecurringTasks/ConfigureRecurringTasksScheduleStartupTask.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Cronos; -using JetBrains.Annotations; -using Microsoft.Extensions.Options; - -namespace Elsa.Common.RecurringTasks; - -[UsedImplicitly] -public class ConfigureRecurringTasksScheduleStartupTask(IOptions options, ISystemClock systemClock, RecurringTaskScheduleManager recurringTaskScheduleManager) : IStartupTask -{ - public Task ExecuteAsync(CancellationToken cancellationToken) - { - foreach(var entry in options.Value.Schedule.ScheduledTasks) - { - var taskType = entry.Key; - var intervalExpression = entry.Value; - var schedule = intervalExpression.Type switch - { - IntervalExpressionType.Cron => (ISchedule)new CronSchedule(systemClock, CronExpression.Parse(intervalExpression.Expression)), - IntervalExpressionType.Interval => new IntervalSchedule(TimeSpan.Parse(intervalExpression.Expression)), - _ => throw new NotSupportedException($"Interval expression type '{intervalExpression.Type}' is not supported.") - }; - recurringTaskScheduleManager.ConfigureScheduledTask(taskType, schedule); - } - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Common/RecurringTasks/RecurringTaskScheduleManager.cs b/src/modules/Elsa.Common/RecurringTasks/RecurringTaskScheduleManager.cs index b5418383f..fbb286d56 100644 --- a/src/modules/Elsa.Common/RecurringTasks/RecurringTaskScheduleManager.cs +++ b/src/modules/Elsa.Common/RecurringTasks/RecurringTaskScheduleManager.cs @@ -1,21 +1,31 @@ +using Cronos; +using Microsoft.Extensions.Options; + namespace Elsa.Common.RecurringTasks; -public class RecurringTaskScheduleManager +public class RecurringTaskScheduleManager(IOptions options, ISystemClock systemClock) { public IDictionary ScheduledTasks { get; set; } = new Dictionary(); - public void ConfigureScheduledTask(ISchedule schedule) where T : IRecurringTask - { - ConfigureScheduledTask(typeof(T), schedule); - } - - public void ConfigureScheduledTask(Type recurringTaskType, ISchedule schedule) - { - ScheduledTasks[recurringTaskType] = schedule; - } - public ISchedule GetScheduleFor(Type taskType) { - return ScheduledTasks.TryGetValue(taskType, out var schedule) ? schedule : new IntervalSchedule(TimeSpan.FromMinutes(1)); + if (!ScheduledTasks.TryGetValue(taskType, out var schedule)) + { + var intervalExpression = options.Value.Schedule.ScheduledTasks.TryGetValue(taskType, out var expr) ? expr : null; + schedule = intervalExpression != null ? CreateSchedule(intervalExpression) : new IntervalSchedule(TimeSpan.FromMinutes(1)); + ScheduledTasks[taskType] = schedule; + } + + return schedule; + } + + private ISchedule CreateSchedule(IntervalExpression intervalExpression) + { + return intervalExpression.Type switch + { + IntervalExpressionType.Cron => new CronSchedule(systemClock, CronExpression.Parse(intervalExpression.Expression)), + IntervalExpressionType.Interval => new IntervalSchedule(TimeSpan.Parse(intervalExpression.Expression)), + _ => throw new NotSupportedException($"Interval expression type '{intervalExpression.Type}' is not supported.") + }; } } \ No newline at end of file From 5e2644f751a238502f264751b180904a67896309 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 2 Jan 2025 11:43:21 +0100 Subject: [PATCH 024/166] Refactor workflow runtime structure and update handlers. Moved `CancelWorkflowsCommandHandler` to the shared runtime module. Updated related project references, features, and configurations accordingly. Improved `WorkflowInstance` actor to save state using `IWorkflowInstanceManager`. --- src/apps/Elsa.Server.Web/Program.cs | 2 +- .../Elsa.Workflows.Runtime.Distributed.csproj | 4 ++++ .../Features/DistributedRuntimeFeature.cs | 2 +- .../Actors/WorkflowInstance.cs | 2 ++ .../Features/DefaultWorkflowRuntimeFeature.cs | 13 +------------ .../Features/WorkflowRuntimeFeature.cs | 1 + .../Handlers/CancelWorkflowsCommandHandler.cs | 2 +- 7 files changed, 11 insertions(+), 15 deletions(-) rename src/modules/{Elsa.Workflows.Runtime.Distributed => Elsa.Workflows.Runtime}/Handlers/CancelWorkflowsCommandHandler.cs (92%) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3e5a9b3f9..dd266fa03 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -74,7 +74,7 @@ const bool useAzureServiceBus = false; const bool useKafka = false; const bool useReadOnlyMode = false; const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests. -const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; +const WorkflowRuntime workflowRuntime = WorkflowRuntime.ProtoActor; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; const bool useMultitenancy = false; diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj index 870cd0d78..8f3983f11 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs index 49a6526fe..0e05eca4a 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs @@ -2,8 +2,8 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; -using Elsa.Workflows.Runtime.Distributed.Handlers; using Elsa.Workflows.Runtime.Features; +using Elsa.Workflows.Runtime.Handlers; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Distributed.Features; diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs index b89314b12..7486c27b5 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs @@ -211,6 +211,8 @@ internal class WorkflowInstance( var workflowRunner = scope.ServiceProvider.GetRequiredService(); var workflowResult = await workflowRunner.RunAsync(WorkflowGraph, WorkflowState, runWorkflowOptions, _linkedCancellationToken); WorkflowState = workflowResult.WorkflowState; + var workflowInstanceManager = scope.ServiceProvider.GetRequiredService(); + await workflowInstanceManager.SaveAsync(WorkflowState, Context.CancellationToken); return workflowResult; } diff --git a/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs index 41de318ac..6adf87b86 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/DefaultWorkflowRuntimeFeature.cs @@ -8,15 +8,4 @@ namespace Elsa.Workflows.Runtime.Features; /// Installs the default runtime services. /// [DependsOn(typeof(WorkflowRuntimeFeature))] -public class DefaultWorkflowRuntimeFeature : FeatureBase -{ - /// - public DefaultWorkflowRuntimeFeature(IModule module) : base(module) - { - } - - /// - public override void Apply() - { - } -} \ No newline at end of file +public class DefaultWorkflowRuntimeFeature(IModule module) : FeatureBase(module); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 91f0592b0..4ef109615 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -310,6 +310,7 @@ public class WorkflowRuntimeFeature : FeatureBase // Domain handlers. .AddCommandHandler() + .AddCommandHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Handlers/CancelWorkflowsCommandHandler.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs similarity index 92% rename from src/modules/Elsa.Workflows.Runtime.Distributed/Handlers/CancelWorkflowsCommandHandler.cs rename to src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs index 0f1f080bf..776db86c9 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Handlers/CancelWorkflowsCommandHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs @@ -2,7 +2,7 @@ using Elsa.Mediator.Contracts; using Elsa.Mediator.Models; using Elsa.Workflows.Runtime.Commands; -namespace Elsa.Workflows.Runtime.Distributed.Handlers; +namespace Elsa.Workflows.Runtime.Handlers; /// /// Handles the . From 9aeacd6e4d055227c299068fbae6440bea23f884 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 2 Jan 2025 11:45:49 +0100 Subject: [PATCH 025/166] Update workflow state persistence during cancellation Enhanced the workflow cancellation process by saving the updated workflow state using `IWorkflowInstanceManager`. Similar updates were applied to state import logic to ensure consistency in persisting workflow state changes. --- .../Actors/WorkflowInstance.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs index 7486c27b5..cc6b40908 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs @@ -161,7 +161,9 @@ internal class WorkflowInstance( await using var scope = scopeFactory.CreateAsyncScope(); var serviceProvider = scope.ServiceProvider; var workflowCanceler = serviceProvider.GetRequiredService(); - _workflowState = await workflowCanceler.CancelWorkflowAsync(WorkflowGraph, WorkflowState, Context.CancellationToken); + WorkflowState = await workflowCanceler.CancelWorkflowAsync(WorkflowGraph, WorkflowState, Context.CancellationToken); + var workflowInstanceManager = scope.ServiceProvider.GetRequiredService(); + await workflowInstanceManager.SaveAsync(WorkflowState, Context.CancellationToken); } public override async Task ExportState() @@ -179,6 +181,9 @@ internal class WorkflowInstance( var workflowState = mappers.WorkflowStateJsonMapper.Map(request.SerializedWorkflowState); await EnsureStateAsync(); WorkflowState = workflowState; + await using var scope = scopeFactory.CreateAsyncScope(); + var workflowInstanceManager = scope.ServiceProvider.GetRequiredService(); + await workflowInstanceManager.SaveAsync(WorkflowState, Context.CancellationToken); } private async Task RunAsync(RunWorkflowOptions runWorkflowOptions) From b7781dcc3e09711420d96cb8ee96cd8ba3f5c46c Mon Sep 17 00:00:00 2001 From: KnibbsyMan Date: Sun, 5 Jan 2025 18:36:45 +0000 Subject: [PATCH 026/166] Adds the IsSerializable flag to the SqlQuery results output. --- src/modules/Elsa.Sql/Activities/SqlQuery.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/Elsa.Sql/Activities/SqlQuery.cs index c1d922149..4a2653055 100644 --- a/src/modules/Elsa.Sql/Activities/SqlQuery.cs +++ b/src/modules/Elsa.Sql/Activities/SqlQuery.cs @@ -53,7 +53,8 @@ public class SqlQuery : Activity /// of queried results. /// [Output( - Description = "DataSet of queried results.")] + Description = "DataSet of queried results.", + IsSerializable = false)] public Output Results { get; set; } = default!; /// From 965a67c62f7e65cb90eec99f94a8ee5db08c132b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 5 Jan 2025 20:29:08 +0100 Subject: [PATCH 027/166] Fix punctuation in link text for better clarity. Added a missing comma in the README link text for the Elsa 2 section to improve readability and align with proper punctuation standards. No functional changes were made. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 383c3a8aa..ca3e3efae 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Stack Overflow questions](https://img.shields.io/badge/stackoverflow-elsa_workflows-orange.svg)]( http://stackoverflow.com/questions/tagged/elsa-workflows ) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Elsa%20Guru-006BFF)](https://gurubase.io/g/elsa) -### [For Elsa 2 Click Here](https://github.com/elsa-workflows/elsa-core/tree/2.x) +### [For Elsa 2, Click Here](https://github.com/elsa-workflows/elsa-core/tree/2.x) ## Introduction Elsa is a powerful workflow library that enables workflow execution within any .NET application. Elsa allows you to define workflows in various ways, including: From ba4645ea62c63a1aa45f49ee4a38d9bab5de8e2f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 15:34:43 +0100 Subject: [PATCH 028/166] Refactor JsonWorkflowStateSerializer to improve performance - **Improves performance:** The converters and static properties are pre-configured when the class is initialized. Only the `ReferenceHandler` is refreshed per call, avoiding repeated configuration of common options like converters. - **Thread-safe:** Since `_cachedOptions` is immutable, it can safely be reused across threads. **Tradeoffs:** Small performance cost during cloning, but still much faster than fully recreating options each time. --- .../JsonWorkflowStateSerializer.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index 90093d688..681893fca 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -129,29 +129,18 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat /// public override JsonSerializerOptions GetOptions() { - // Bypass cached options to ensure that the reference handler is always fresh. - return GetOptionsInternal(); - } - - /// - protected override void Configure(JsonSerializerOptions options) - { - var referenceHandler = new CrossScopedReferenceHandler(); - - options.ReferenceHandler = referenceHandler; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.PropertyNameCaseInsensitive = true; - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + var options = base.GetOptions(); + return new JsonSerializerOptions(options) + { + ReferenceHandler = new CrossScopedReferenceHandler() + }; } /// protected override void AddConverters(JsonSerializerOptions options) { - options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry)); - options.Converters.Add(JsonMetadataServices.TimeSpanConverter); options.Converters.Add(new PolymorphicObjectConverterFactory(_wellKnownTypeRegistry)); - options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry)); options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); } } \ No newline at end of file From 506a36574b457a443661b3eacc9e676303d76618 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 15:35:06 +0100 Subject: [PATCH 029/166] Update target framework to .NET 9 and improve null handling Updated the target framework from .NET 8 to .NET 9 for future compatibility and language improvements. Adjusted null handling in `WorkflowDefinitionImporter` for better readability and correctness. Added a cancellation token to `ReadToEndAsync` for improved async operation control. --- src/apps/Directory.Build.props | 2 +- .../Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs | 2 +- .../Services/WorkflowDefinitionImporter.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apps/Directory.Build.props b/src/apps/Directory.Build.props index 1ead41344..2e6c5f63d 100644 --- a/src/apps/Directory.Build.props +++ b/src/apps/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net9.0 latest enable enable diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs index 8b4c7622f..f0d0eee81 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/ImportFiles/Endpoint.cs @@ -108,7 +108,7 @@ internal class ImportFiles : ElsaEndpoint private async Task ImportJsonStreamAsync(Stream jsonStream, CancellationToken cancellationToken) { - var json = await new StreamReader(jsonStream).ReadToEndAsync(); + var json = await new StreamReader(jsonStream).ReadToEndAsync(cancellationToken); var model = _apiSerializer.Deserialize(json); await ImportSingleWorkflowDefinitionAsync(model, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs index 536a1a834..b63260be4 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs @@ -36,7 +36,7 @@ namespace Elsa.Workflows.Management.Services // Get a workflow draft version. var draft = !string.IsNullOrWhiteSpace(definitionId) ? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken) - : default; + : null; var isNew = draft == null; From d2777f70dedad8da5ac7abb8d8adb2d7671c7310 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 17:13:20 +0100 Subject: [PATCH 030/166] Refactor reference handling to use a wrapper class Replaced direct use of `CrossScopedReferenceHandler` with `PerCallReferenceHandlerWrapper` for improved abstraction and reusability. Updated `ApplyOptions` to leverage the new wrapper, simplifying reference resolver management. Commented out redundant code to streamline the implementation. --- .../CrossScopedReferenceHandler.cs | 8 ++++++++ .../JsonWorkflowStateSerializer.cs | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs index 32477b95c..fcabf5237 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs @@ -24,4 +24,12 @@ public class CrossScopedReferenceHandler : ReferenceHandler /// /// The reference resolver. public ReferenceResolver GetResolver() => _rootedResolver!; +} + +public class PerCallReferenceHandlerWrapper : ReferenceHandler +{ + public override ReferenceResolver CreateResolver() + { + return new CrossScopedReferenceHandler().CreateResolver(); + } } \ 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 681893fca..80be1aa78 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -126,14 +126,19 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat return JsonSerializer.Deserialize(serializedState, options)!; } - /// - public override JsonSerializerOptions GetOptions() + // /// + // public override JsonSerializerOptions GetOptions() + // { + // var options = base.GetOptions(); + // return new JsonSerializerOptions(options) + // { + // ReferenceHandler = new CrossScopedReferenceHandler() + // }; + // } + + public override void ApplyOptions(JsonSerializerOptions options) { - var options = base.GetOptions(); - return new JsonSerializerOptions(options) - { - ReferenceHandler = new CrossScopedReferenceHandler() - }; + options.ReferenceHandler = new PerCallReferenceHandlerWrapper(); } /// From a3ce523faa1a042a55c9afddd9567eac97b1ede1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 17:27:24 +0100 Subject: [PATCH 031/166] Fix missing base method call in ApplyOptions implementation The `ApplyOptions` method now correctly calls the base implementation before applying custom configurations. This ensures that any base behavior is preserved, preventing potential issues with serialization options. --- .../Serialization/Serializers/JsonWorkflowStateSerializer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index 80be1aa78..6822bb663 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -138,6 +138,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat public override void ApplyOptions(JsonSerializerOptions options) { + base.ApplyOptions(options); options.ReferenceHandler = new PerCallReferenceHandlerWrapper(); } From 25d9608b25e613bfe0a9529f42911ba9ae6c50ae Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 17:57:05 +0100 Subject: [PATCH 032/166] Refactor reference handling logic for serialization. Replaced `PerCallReferenceHandlerWrapper` with `CrossScopedReferenceHandler` and optimized the resolver initialization using `AsyncLocal`. This simplifies the code and ensures a more efficient handling of reference resolution during serialization. --- .../CrossScopedReferenceHandler.cs | 32 ++++++++----------- .../JsonWorkflowStateSerializer.cs | 14 +------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs index fcabf5237..41a31ccfc 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs @@ -7,29 +7,23 @@ namespace Elsa.Workflows.Serialization.ReferenceHandlers; /// public class CrossScopedReferenceHandler : ReferenceHandler { - /// - public CrossScopedReferenceHandler() => Reset(); - private ReferenceResolver? _rootedResolver; + private static readonly AsyncLocal RootedResolverState = new(); + + private CustomPreserveReferenceResolver RootedResolver + { + get + { + RootedResolverState.Value ??= new CustomPreserveReferenceResolver(); + return RootedResolverState.Value!; + } + } /// - public override ReferenceResolver CreateResolver() => _rootedResolver!; - - /// - /// Resets the reference resolver. - /// - public void Reset() => _rootedResolver = new CustomPreserveReferenceResolver(); - + public override ReferenceResolver CreateResolver() => RootedResolver; + /// /// Gets the reference resolver. /// /// The reference resolver. - public ReferenceResolver GetResolver() => _rootedResolver!; -} - -public class PerCallReferenceHandlerWrapper : ReferenceHandler -{ - public override ReferenceResolver CreateResolver() - { - return new CrossScopedReferenceHandler().CreateResolver(); - } + public ReferenceResolver GetResolver() => RootedResolver; } \ 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 6822bb663..74357c197 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -1,7 +1,5 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using Elsa.Common.Serialization; using Elsa.Expressions.Contracts; using Elsa.Workflows.Serialization.Converters; @@ -126,20 +124,10 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat return JsonSerializer.Deserialize(serializedState, options)!; } - // /// - // public override JsonSerializerOptions GetOptions() - // { - // var options = base.GetOptions(); - // return new JsonSerializerOptions(options) - // { - // ReferenceHandler = new CrossScopedReferenceHandler() - // }; - // } - public override void ApplyOptions(JsonSerializerOptions options) { base.ApplyOptions(options); - options.ReferenceHandler = new PerCallReferenceHandlerWrapper(); + options.ReferenceHandler = new CrossScopedReferenceHandler(); } /// From aef59219790dd370a62cc903ed189dea1a0c8d92 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 18:11:11 +0100 Subject: [PATCH 033/166] Refactor reference handling in serialization classes Replaces static resolver state with an instance-level resolver in `CrossScopedReferenceHandler` to enhance flexibility and thread-safety. Refactors `JsonWorkflowStateSerializer` to use `GetOptions` for improved API consistency and clarity. --- .../CrossScopedReferenceHandler.cs | 24 +++++++++---------- .../JsonWorkflowStateSerializer.cs | 10 +++++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs index 41a31ccfc..32477b95c 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/ReferenceHandlers/CrossScopedReferenceHandler.cs @@ -7,23 +7,21 @@ namespace Elsa.Workflows.Serialization.ReferenceHandlers; /// public class CrossScopedReferenceHandler : ReferenceHandler { - private static readonly AsyncLocal RootedResolverState = new(); - - private CustomPreserveReferenceResolver RootedResolver - { - get - { - RootedResolverState.Value ??= new CustomPreserveReferenceResolver(); - return RootedResolverState.Value!; - } - } + /// + public CrossScopedReferenceHandler() => Reset(); + private ReferenceResolver? _rootedResolver; /// - public override ReferenceResolver CreateResolver() => RootedResolver; - + public override ReferenceResolver CreateResolver() => _rootedResolver!; + + /// + /// Resets the reference resolver. + /// + public void Reset() => _rootedResolver = new CustomPreserveReferenceResolver(); + /// /// Gets the reference resolver. /// /// The reference resolver. - public ReferenceResolver GetResolver() => RootedResolver; + public ReferenceResolver GetResolver() => _rootedResolver!; } \ 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 74357c197..d23869f4a 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -124,10 +124,14 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat return JsonSerializer.Deserialize(serializedState, options)!; } - public override void ApplyOptions(JsonSerializerOptions options) + /// + public override JsonSerializerOptions GetOptions() { - base.ApplyOptions(options); - options.ReferenceHandler = new CrossScopedReferenceHandler(); + var options = base.GetOptions(); + return new JsonSerializerOptions(options) + { + ReferenceHandler = new CrossScopedReferenceHandler() + }; } /// From 1c9328597f07a8a5a9cbc9cd6020bd819ef6f44c Mon Sep 17 00:00:00 2001 From: Lars Nijholt Date: Tue, 7 Jan 2025 14:34:14 +0100 Subject: [PATCH 034/166] Add workflow name property to workflow instance filter. Add the name property to the workflow instance filter to make use of the name property that can be used in the Request class for the list endpoint. This allows us to filter purely on workflow instance name without using the SearchTerm --- .../Endpoints/WorkflowInstances/List/Endpoint.cs | 1 + .../Filters/WorkflowInstanceFilter.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs index a69047659..785de7b1d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs @@ -38,6 +38,7 @@ internal class List(IWorkflowInstanceStore store) : ElsaEndpoint public string? SearchTerm { get; set; } + + /// + /// Filter workflow instances that match the specified name. + /// + public string? Name { get; set; } /// /// Filter workflow instances by definition ID. @@ -125,6 +130,7 @@ public class WorkflowInstanceFilter if (filter.WorkflowSubStatuses != null) query = query.Where(x => filter.WorkflowSubStatuses.Contains(x.SubStatus)); if (filter.HasIncidents != null) query = filter.HasIncidents == true ? query.Where(x => x.IncidentCount > 0) : query.Where(x => x.IncidentCount == 0); if (filter.IsSystem != null) query = query.Where(x => x.IsSystem == filter.IsSystem); + if (filter.Name != null) query = query.Where(x => x.Name!.Contains(filter.Name, StringComparison.InvariantCultureIgnoreCase)); if (TimestampFilters != null) { From e48010e3f93cd04d6d44677ddb534e4fbabe0a66 Mon Sep 17 00:00:00 2001 From: Lars Nijholt Date: Tue, 7 Jan 2025 14:46:03 +0100 Subject: [PATCH 035/166] Use ToLower method for comparison --- .../Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs index 6b6ea9ec8..89ebafd46 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs @@ -130,7 +130,7 @@ public class WorkflowInstanceFilter if (filter.WorkflowSubStatuses != null) query = query.Where(x => filter.WorkflowSubStatuses.Contains(x.SubStatus)); if (filter.HasIncidents != null) query = filter.HasIncidents == true ? query.Where(x => x.IncidentCount > 0) : query.Where(x => x.IncidentCount == 0); if (filter.IsSystem != null) query = query.Where(x => x.IsSystem == filter.IsSystem); - if (filter.Name != null) query = query.Where(x => x.Name!.Contains(filter.Name, StringComparison.InvariantCultureIgnoreCase)); + if (filter.Name != null) query = query.Where(x => x.Name!.ToLower().Contains(filter.Name.ToLower())); if (TimestampFilters != null) { From 788e6f7a0f099f41c4818c9fcd426e7dded35f76 Mon Sep 17 00:00:00 2001 From: Lars Nijholt Date: Tue, 7 Jan 2025 14:53:21 +0100 Subject: [PATCH 036/166] Add case insensitivity to the name and description of the workflow definition filter --- .../Filters/WorkflowDefinitionFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs index b69f486ad..9b6d6d16f 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowDefinitionFilter.cs @@ -102,7 +102,7 @@ public class WorkflowDefinitionFilter if (Name != null) queryable = queryable.Where(x => x.Name == Name); if (Names != null) queryable = queryable.Where(x => Names.Contains(x.Name!)); if (UsableAsActivity != null) queryable = queryable.Where(x => x.Options.UsableAsActivity == UsableAsActivity); - if (!string.IsNullOrWhiteSpace(SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(SearchTerm) || x.Description!.Contains(SearchTerm) || x.Id.Contains(SearchTerm) || x.DefinitionId.Contains(SearchTerm)); + if (!string.IsNullOrWhiteSpace(SearchTerm)) queryable = queryable.Where(x => x.Name!.ToLower().Contains(SearchTerm.ToLower()) || x.Description!.ToLower().Contains(SearchTerm.ToLower()) || x.Id.Contains(SearchTerm) || x.DefinitionId.Contains(SearchTerm)); if (IsSystem != null) queryable = queryable.Where(x => x.IsSystem == IsSystem); if (IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == IsReadonly); From dc2def3ad9887a89ca161effe0d127cebf27182d Mon Sep 17 00:00:00 2001 From: Lars Nijholt Date: Tue, 7 Jan 2025 15:43:56 +0100 Subject: [PATCH 037/166] Add string comparison for searching definitions case insensitively to EfCoreWorkflowDefinitionStore --- .../Modules/Management/WorkflowDefinitionStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs index 02e5286c1..226bb40e9 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionStore.cs @@ -206,7 +206,7 @@ public class EFCoreWorkflowDefinitionStore(EntityStore x.Name == filter.Name); if (filter.Names != null) queryable = queryable.Where(x => filter.Names.Contains(x.Name!)); if (filter.UsableAsActivity != null) queryable = queryable.Where(x => EF.Property(x, "UsableAsActivity") == filter.UsableAsActivity); - if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(filter.SearchTerm) || x.Description!.Contains(filter.SearchTerm) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm)); + if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.ToLower().Contains(filter.SearchTerm.ToLower()) || x.Description!.ToLower().Contains(filter.SearchTerm.ToLower()) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm)); // TEMP: IsSystem may be null when upgrading from older versions of Elsa to 3.2. See issue #5366. // In a future version, we should remove this check and simply do queryable.Where(x => x.IsSystem == filter.IsSystem). From 05f8bc640b2bead6ee57693232690843cc56ea9e Mon Sep 17 00:00:00 2001 From: Lars Nijholt Date: Tue, 7 Jan 2025 16:00:20 +0100 Subject: [PATCH 038/166] Add case insensitivity to the search term of the WorkflowInstanceFilter --- .../Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs index 89ebafd46..f279df52a 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs @@ -166,7 +166,7 @@ public class WorkflowInstanceFilter { query = from instance in query - where instance.Name!.Contains(searchTerm) + where instance.Name!.ToLower().Contains(searchTerm.ToLower()) || instance.DefinitionVersionId.Contains(searchTerm) || instance.DefinitionId.Contains(searchTerm) || instance.Id.Contains(searchTerm) From 67b950f86caaa9a9ab85769c8906a3fe5f7de0a3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 9 Jan 2025 10:27:55 +0100 Subject: [PATCH 039/166] Refactor to use target-typed object creation Replaced explicit type initializations with concise target-typed `new()` expressions where applicable, improving code readability and aligning with updated C# conventions. Updated .editorconfig to enforce consistent use of `var` for type declarations. --- .editorconfig | 8 ++++---- .../Elsa.Workflows.Core/Services/ActivityInvoker.cs | 4 ++-- .../Services/ActivityLoggerStateGenerator.cs | 2 +- .../Elsa.Workflows.Core/Services/WorkflowRunner.cs | 6 +++--- .../Services/DefaultWorkflowStarter.cs | 4 ++-- .../Services/LocalWorkflowClient.cs | 8 ++++---- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.editorconfig b/.editorconfig index 31a97b083..ae542588f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true +# Remove the line below if you want to inherit .editorconfig settings from higher directories # C# files [*.cs] @@ -76,9 +76,9 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true # Expression-bodied members csharp_style_expression_bodied_accessors = true diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs index b180f61e7..13f074bfa 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs @@ -12,7 +12,7 @@ public class ActivityInvoker( { /// - public async Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = default) + public async Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null) { // Setup an activity execution context, potentially reusing an existing one if requested. var existingActivityExecutionContext = options?.ExistingActivityExecutionContext; @@ -20,7 +20,7 @@ public class ActivityInvoker( // Perform a lookup to make sure the activity execution context is part of the workflow execution context. var activityExecutionContext = existingActivityExecutionContext != null ? workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == existingActivityExecutionContext.Id) - : default; + : null; if (activityExecutionContext == null) { diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityLoggerStateGenerator.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityLoggerStateGenerator.cs index 959e6726c..eb2bd5f6f 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityLoggerStateGenerator.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityLoggerStateGenerator.cs @@ -12,7 +12,7 @@ public class ActivityLoggerStateGenerator : ILoggerStateGeneratorA containing the state related to the . public Dictionary GenerateLoggerState(ActivityExecutionContext activityExecutionContext) { - return new Dictionary + return new() { ["ActivityInstanceId"] = activityExecutionContext.Id }; diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index 69cdf8785..7962a96b7 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -43,7 +43,7 @@ public class WorkflowRunner( public async Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) { var result = await RunAsync((IWorkflow)workflow, options, cancellationToken); - return new RunWorkflowResult(result.WorkflowState, result.Workflow, (TResult)result.Result!); + return new(result.WorkflowState, result.Workflow, (TResult)result.Result!); } /// @@ -141,7 +141,7 @@ public class WorkflowRunner( if (!string.IsNullOrEmpty(activityHandle.ActivityInstanceId)) { var activityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == activityHandle.ActivityInstanceId) - ?? throw new Exception("No activity execution context found with the specified ID."); + ?? throw new("No activity execution context found with the specified ID."); workflowExecutionContext.ScheduleActivityExecutionContext(activityExecutionContext); } else @@ -191,6 +191,6 @@ public class WorkflowRunner( var result = workflow.ResultVariable?.Get(workflowExecutionContext.MemoryRegister); await notificationSender.SendAsync(new WorkflowExecuted(workflow, workflowState, workflowExecutionContext), cancellationToken); await commitStateHandler.CommitAsync(workflowExecutionContext, workflowState, cancellationToken); - return new RunWorkflowResult(workflowState, workflowExecutionContext.Workflow, result); + return new(workflowState, workflowExecutionContext.Workflow, result); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs index 11d861160..e1c2e79f7 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs @@ -19,7 +19,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio }); if (!canStart) - return new StartWorkflowResponse + return new() { CannotStart = true }; @@ -35,7 +35,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio }; var runWorkflowResponse = await workflowClient.CreateAndRunInstanceAsync(createWorkflowInstanceRequest, cancellationToken); - return new StartWorkflowResponse + return new() { CannotStart = false, WorkflowInstanceId = runWorkflowResponse.WorkflowInstanceId, diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 1747e31f3..5e4812f89 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -42,7 +42,7 @@ public class LocalWorkflowClient( }; await workflowInstanceManager.CreateWorkflowInstanceAsync(workflowGraph.Workflow, options, cancellationToken); - return new CreateWorkflowInstanceResponse(); + return new(); } /// @@ -54,7 +54,7 @@ public class LocalWorkflowClient( if (workflowInstance.Status != WorkflowStatus.Running) { logger.LogWarning("Attempt to resume workflow {WorkflowInstanceId} that is not in the Running state. The actual state is {ActualWorkflowStatus}", workflowState.Id, workflowState.Status); - return new RunWorkflowInstanceResponse + return new() { WorkflowInstanceId = WorkflowInstanceId, Status = workflowInstance.Status, @@ -76,7 +76,7 @@ public class LocalWorkflowClient( workflowState = workflowResult.WorkflowState; - return new RunWorkflowInstanceResponse + return new() { WorkflowInstanceId = WorkflowInstanceId, Status = workflowState.Status, @@ -97,7 +97,7 @@ public class LocalWorkflowClient( ParentId = request.ParentId }; await CreateInstanceAsync(createRequest, cancellationToken); - return await RunInstanceAsync(new RunWorkflowInstanceRequest + return await RunInstanceAsync(new() { Input = request.Input, Properties = request.Properties, From 288097d503c61080c2f2f2f33e3998d4b81a44b5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 9 Jan 2025 19:34:53 +0100 Subject: [PATCH 040/166] Simplify and optimize workflow instance creation. Replaced asynchronous instance creation methods with streamlined synchronous alternatives where applicable. Introduced `CreateAndCommitWorkflowInstanceAsync` for combined instantiation and persistence, along with a separate `CreateWorkflowInstance` method for non-committal instantiation. Refactored related code to improve readability, maintainability, and runtime performance. --- src/apps/Elsa.Server.Web/Program.cs | 2 +- .../Services/MassTransitWorkflowDispatcher.cs | 2 +- .../Execute/EndpointBase.cs | 11 +- .../Contracts/IWorkflowInstanceManager.cs | 7 +- .../Services/WorkflowInstanceManager.cs | 10 +- .../Services/DistributedWorkflowClient.cs | 5 +- .../Actors/WorkflowInstance.cs | 2 +- .../Services/LocalWorkflowClient.cs | 106 +++++++++++------- 8 files changed, 91 insertions(+), 54 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index dd266fa03..3e5a9b3f9 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -74,7 +74,7 @@ const bool useAzureServiceBus = false; const bool useKafka = false; const bool useReadOnlyMode = false; const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests. -const WorkflowRuntime workflowRuntime = WorkflowRuntime.ProtoActor; +const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; const bool useMultitenancy = false; diff --git a/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs b/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs index 472950649..d4ee6d498 100644 --- a/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs +++ b/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs @@ -127,7 +127,7 @@ public class MassTransitWorkflowDispatcher( private async Task DispatchWorkflowAsync(Workflow workflow, WorkflowInstanceOptions? workflowInstanceOptions, string? triggerActivityId, DispatchWorkflowOptions? options, CancellationToken cancellationToken) { - var workflowInstance = await workflowInstanceManager.CreateWorkflowInstanceAsync(workflow, workflowInstanceOptions, cancellationToken); + var workflowInstance = await workflowInstanceManager.CreateAndCommitWorkflowInstanceAsync(workflow, workflowInstanceOptions, cancellationToken); var sendEndpoint = await GetSendEndpointAsync(options); var message = DispatchWorkflowDefinition.DispatchExistingWorkflowInstance(workflowInstance.Id, triggerActivityId); await sendEndpoint.Send(message, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs index c22a50f7e..d5a77fc6a 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs @@ -49,7 +49,9 @@ internal abstract class EndpointBase( }; var startResponse = await workflowStarter.StartWorkflowAsync(startRequest, cancellationToken); - HttpContext.Response.Headers.Append("x-elsa-workflow-cannot-start", startResponse.CannotStart.ToString()); + + if(!HttpContext.Response.HasStarted) + HttpContext.Response.Headers.Append("x-elsa-workflow-cannot-start", startResponse.CannotStart.ToString()); if (startResponse.CannotStart) { @@ -61,8 +63,9 @@ internal abstract class EndpointBase( // Write the workflow instance ID to the response header. // This allows clients to read the header even if the workflow writes a response body - // (in which case, we can't transmit a JSON body that includes the instance ID). - HttpContext.Response.Headers.Append("x-elsa-workflow-instance-id", instanceId); + // (in which case, we can't transmit a JSON body that includes the instance ID). + if(!HttpContext.Response.HasStarted) + HttpContext.Response.Headers.Append("x-elsa-workflow-instance-id", instanceId); var workflowClient = await workflowRuntime.CreateClientAsync(instanceId, cancellationToken); @@ -84,7 +87,7 @@ internal abstract class EndpointBase( if (HttpContext.Response.StatusCode == StatusCodes.Status200OK) { var workflowState = await workflowClient.ExportStateAsync(cancellationToken); - await SendOkAsync(new Response(workflowState), cancellationToken); + await SendOkAsync(new(workflowState), cancellationToken); } } } diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs index e54a08a28..2ae45c492 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs @@ -105,5 +105,10 @@ public interface IWorkflowInstanceManager /// /// Instantiates and saves a new workflow instance. /// - Task CreateWorkflowInstanceAsync(Workflow workflow, WorkflowInstanceOptions? options = null, CancellationToken cancellationToken = default); + Task CreateAndCommitWorkflowInstanceAsync(Workflow workflow, WorkflowInstanceOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// Instantiates a new workflow instance. + /// + WorkflowInstance CreateWorkflowInstance(Workflow workflow, WorkflowInstanceOptions? options = null); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs index 16fbb109b..12bc8ca29 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs @@ -130,10 +130,16 @@ public class WorkflowInstanceManager( } /// - public async Task CreateWorkflowInstanceAsync(Workflow workflow, WorkflowInstanceOptions? options = null, CancellationToken cancellationToken = default) + public async Task CreateAndCommitWorkflowInstanceAsync(Workflow workflow, WorkflowInstanceOptions? options = null, CancellationToken cancellationToken = default) { - var workflowInstance = workflowInstanceFactory.CreateWorkflowInstance(workflow, options); + var workflowInstance = CreateWorkflowInstance(workflow, options); await SaveAsync(workflowInstance, cancellationToken); return workflowInstance; } + + /// + public WorkflowInstance CreateWorkflowInstance(Workflow workflow, WorkflowInstanceOptions? options = null) + { + return workflowInstanceFactory.CreateWorkflowInstance(workflow, options); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs index 497086f51..55179d545 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs @@ -11,7 +11,8 @@ public class DistributedWorkflowClient( string workflowInstanceId, IDistributedLockProvider distributedLockProvider, IOptions distributedLockingOptions, - IServiceProvider serviceProvider) : IWorkflowClient + IServiceProvider serviceProvider) + : IWorkflowClient { private readonly LocalWorkflowClient _localWorkflowClient = ActivatorUtilities.CreateInstance(serviceProvider, workflowInstanceId); @@ -30,7 +31,7 @@ public class DistributedWorkflowClient( public async Task CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { - var result = await WithLockAsync(async () => await _localWorkflowClient.CreateAndRunInstanceAsync(request, cancellationToken)); + var result = await _localWorkflowClient.CreateAndRunInstanceAsync(request, cancellationToken); return result; } diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs index cc6b40908..370b77e09 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs @@ -262,7 +262,7 @@ internal class WorkflowInstance( await using var scope = scopeFactory.CreateAsyncScope(); var workflowInstanceManager = scope.ServiceProvider.GetRequiredService(); - var workflowInstance = await workflowInstanceManager.CreateWorkflowInstanceAsync(workflowGraph.Workflow, workflowInstanceOptions, cancellationToken); + var workflowInstance = await workflowInstanceManager.CreateAndCommitWorkflowInstanceAsync(workflowGraph.Workflow, workflowInstanceOptions, cancellationToken); var workflowState = workflowInstance.WorkflowState; _workflowInstanceId = workflowState.Id; WorkflowGraph = workflowGraph; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 5e4812f89..b5380ed29 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -41,7 +41,7 @@ public class LocalWorkflowClient( Properties = request.Properties }; - await workflowInstanceManager.CreateWorkflowInstanceAsync(workflowGraph.Workflow, options, cancellationToken); + workflowInstanceManager.CreateWorkflowInstance(workflowGraph.Workflow, options); return new(); } @@ -49,6 +49,56 @@ public class LocalWorkflowClient( public async Task RunInstanceAsync(RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); + return await RunInstanceAsync(workflowInstance, request, cancellationToken); + } + + /// + public async Task CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) + { + var createRequest = new CreateWorkflowInstanceRequest + { + Properties = request.Properties, + CorrelationId = request.CorrelationId, + Input = request.Input, + WorkflowDefinitionHandle = request.WorkflowDefinitionHandle, + ParentId = request.ParentId + }; + var workflowInstance = await CreateInstanceInternalAsync(createRequest, cancellationToken); + return await RunInstanceAsync(workflowInstance, new() + { + Input = request.Input, + Properties = request.Properties, + TriggerActivityId = request.TriggerActivityId, + ActivityHandle = request.ActivityHandle + }, cancellationToken); + } + + /// + public async Task CancelAsync(CancellationToken cancellationToken = default) + { + var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); + if (workflowInstance.Status != WorkflowStatus.Running) return; + var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken); + var workflowState = await workflowCanceler.CancelWorkflowAsync(workflowGraph, workflowInstance.WorkflowState, cancellationToken); + await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); + } + + /// + public async Task ExportStateAsync(CancellationToken cancellationToken = default) + { + var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); + return workflowInstance.WorkflowState; + } + + /// + public async Task ImportStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var workflowInstance = workflowStateMapper.Map(workflowState)!; + await workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken); + } + + private async Task RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) + { var workflowState = workflowInstance.WorkflowState; if (workflowInstance.Status != WorkflowStatus.Running) @@ -71,7 +121,7 @@ public class LocalWorkflowClient( ActivityHandle = request.ActivityHandle, }; - var workflowGraph = await GetWorkflowGraphAsync(cancellationToken); + var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken); var workflowResult = await workflowRunner.RunAsync(workflowGraph, workflowState, runWorkflowOptions, cancellationToken); workflowState = workflowResult.WorkflowState; @@ -84,50 +134,23 @@ public class LocalWorkflowClient( Incidents = workflowState.Incidents }; } - - /// - public async Task CreateAndRunInstanceAsync(CreateAndRunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) + + public async Task CreateInstanceInternalAsync(CreateWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { - var createRequest = new CreateWorkflowInstanceRequest + var workflowDefinitionHandle = request.WorkflowDefinitionHandle; + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken); + if (workflowGraph == null) throw new InvalidOperationException($"Workflow with version ID {workflowDefinitionHandle} not found."); + + var options = new WorkflowInstanceOptions { - Properties = request.Properties, + WorkflowInstanceId = WorkflowInstanceId, CorrelationId = request.CorrelationId, + ParentWorkflowInstanceId = request.ParentId, Input = request.Input, - WorkflowDefinitionHandle = request.WorkflowDefinitionHandle, - ParentId = request.ParentId + Properties = request.Properties }; - await CreateInstanceAsync(createRequest, cancellationToken); - return await RunInstanceAsync(new() - { - Input = request.Input, - Properties = request.Properties, - TriggerActivityId = request.TriggerActivityId, - ActivityHandle = request.ActivityHandle - }, cancellationToken); - } - /// - public async Task CancelAsync(CancellationToken cancellationToken = default) - { - var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); - if (workflowInstance.Status != WorkflowStatus.Running) return; - var workflowGraph = await GetWorkflowGraphAsync(cancellationToken); - var workflowState = await workflowCanceler.CancelWorkflowAsync(workflowGraph, workflowInstance.WorkflowState, cancellationToken); - await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); - } - - /// - public async Task ExportStateAsync(CancellationToken cancellationToken = default) - { - var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); - return workflowInstance.WorkflowState; - } - - /// - public async Task ImportStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) - { - var workflowInstance = workflowStateMapper.Map(workflowState)!; - await workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken); + return workflowInstanceManager.CreateWorkflowInstance(workflowGraph.Workflow, options); } private async Task GetWorkflowInstanceAsync(CancellationToken cancellationToken) @@ -137,9 +160,8 @@ public class LocalWorkflowClient( return workflowInstance; } - private async Task GetWorkflowGraphAsync(CancellationToken cancellationToken) + private async Task GetWorkflowGraphAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) { - var workflowInstance = await GetWorkflowInstanceAsync(cancellationToken); var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowInstance.DefinitionVersionId, cancellationToken); if (workflowGraph == null) throw new InvalidOperationException($"Workflow graph with version ID {workflowInstance.DefinitionVersionId} not found."); return workflowGraph; From ada9f3967eda9f9a77cab17ee29a886c66f3cd6a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 9 Jan 2025 21:20:02 +0100 Subject: [PATCH 041/166] Use `null` instead of `default` for optional parameters Replaced `default` with explicit `null` for optional parameters to improve clarity and readability. Additionally, simplified object initialization by leveraging newer C# features like target-typed `new`. --- .../Services/MassTransitWorkflowDispatcher.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs b/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs index d4ee6d498..e7b7d64ce 100644 --- a/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs +++ b/src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs @@ -30,12 +30,12 @@ public class MassTransitWorkflowDispatcher( : IWorkflowDispatcher { /// - public async Task DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.DefinitionVersionId, cancellationToken); if (workflowGraph == null) - throw new Exception($"Workflow definition version with ID '{request.DefinitionVersionId}' not found"); + throw new($"Workflow definition version with ID '{request.DefinitionVersionId}' not found"); var workflowInstanceOptions = new WorkflowInstanceOptions { @@ -51,7 +51,7 @@ public class MassTransitWorkflowDispatcher( } /// - public async Task DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task DispatchAsync(DispatchWorkflowInstanceRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var sendEndpoint = await GetSendEndpointAsync(options); var serializedInput = SerializeInput(request.Input); @@ -68,7 +68,7 @@ public class MassTransitWorkflowDispatcher( } /// - public async Task DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { await DispatchTriggersAsync(request, options, cancellationToken); await DispatchBookmarksAsync(request, options, cancellationToken); @@ -76,7 +76,7 @@ public class MassTransitWorkflowDispatcher( } /// - public async Task DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var hash = stimulusHasher.Hash(request.ActivityTypeName, request.BookmarkPayload, request.ActivityInstanceId); var correlationId = request.CorrelationId; @@ -94,7 +94,7 @@ public class MassTransitWorkflowDispatcher( return DispatchWorkflowResponse.Success(); } - private async Task DispatchTriggersAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + private async Task DispatchTriggersAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var triggerHash = stimulusHasher.Hash(request.ActivityTypeName, request.BookmarkPayload); var triggerFilter = new TriggerFilter @@ -133,7 +133,7 @@ public class MassTransitWorkflowDispatcher( await sendEndpoint.Send(message, cancellationToken); } - private async Task DispatchBookmarksAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default) + private async Task DispatchBookmarksAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var correlationId = request.CorrelationId; var workflowInstanceId = request.WorkflowInstanceId; @@ -169,10 +169,10 @@ public class MassTransitWorkflowDispatcher( } } - private async Task GetSendEndpointAsync(DispatchWorkflowOptions? options = default) + private async Task GetSendEndpointAsync(DispatchWorkflowOptions? options = null) { var endpointName = endpointChannelFormatter.FormatEndpointName(options?.Channel); - var sendEndpoint = await bus.GetSendEndpoint(new Uri($"queue:{endpointName}")); + var sendEndpoint = await bus.GetSendEndpoint(new($"queue:{endpointName}")); return sendEndpoint; } From 605a46fdf741425672f986f05f52f4af3674ebaa Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 Jan 2025 10:07:54 +0100 Subject: [PATCH 042/166] Refactor workflow graph retrieval logic. Extract shared workflow graph retrieval logic into a new `GetWorkflowGraphAsync` method, reducing code duplication. Updated call sites to use the new method for better maintainability and readability. --- .../Services/LocalWorkflowClient.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index b5380ed29..a81b08e66 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -29,8 +29,7 @@ public class LocalWorkflowClient( public async Task CreateInstanceAsync(CreateWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowDefinitionHandle = request.WorkflowDefinitionHandle; - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken); - if (workflowGraph == null) throw new InvalidOperationException($"Workflow with version ID {workflowDefinitionHandle} not found."); + var workflowGraph = await GetWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken); var options = new WorkflowInstanceOptions { @@ -138,8 +137,7 @@ public class LocalWorkflowClient( public async Task CreateInstanceInternalAsync(CreateWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowDefinitionHandle = request.WorkflowDefinitionHandle; - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken); - if (workflowGraph == null) throw new InvalidOperationException($"Workflow with version ID {workflowDefinitionHandle} not found."); + var workflowGraph = await GetWorkflowGraphAsync(workflowDefinitionHandle, cancellationToken); var options = new WorkflowInstanceOptions { @@ -162,8 +160,14 @@ public class LocalWorkflowClient( private async Task GetWorkflowGraphAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) { - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowInstance.DefinitionVersionId, cancellationToken); - if (workflowGraph == null) throw new InvalidOperationException($"Workflow graph with version ID {workflowInstance.DefinitionVersionId} not found."); + var handle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowInstance.DefinitionVersionId); + return await GetWorkflowGraphAsync(handle, cancellationToken); + } + + private async Task GetWorkflowGraphAsync(WorkflowDefinitionHandle definitionHandle, CancellationToken cancellationToken) + { + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionHandle, cancellationToken); + if (workflowGraph == null) throw new InvalidOperationException($"Workflow graph with handle {definitionHandle} not found."); return workflowGraph; } } \ No newline at end of file From d6c0ba45df903fc013d9cc715174719aaf02f1eb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 Jan 2025 10:08:25 +0100 Subject: [PATCH 043/166] Use sliding expiration for cache entries. Replaced absolute expiration with sliding expiration for cache entries to ensure the cache is kept active during frequent access. This change optimizes cache behavior and improves resource utilization. --- .../Stores/CachingWorkflowDefinitionStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs index 876e52168..796a55755 100644 --- a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs +++ b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs @@ -144,7 +144,7 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt { var invalidationRequestToken = cacheManager.GetToken(CacheInvalidationTokenKey); entry.AddExpirationToken(invalidationRequestToken); - entry.SetAbsoluteExpiration(cacheManager.CachingOptions.Value.CacheDuration); + entry.SetSlidingExpiration(cacheManager.CachingOptions.Value.CacheDuration); return await factory(); }); } From 03344bdc20e689ad56c9f0d968bc4b6a4335e9d9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 Jan 2025 10:09:35 +0100 Subject: [PATCH 044/166] Adjust caching options and disable secrets usage Enabled CachingOptions configuration and set a 1-day cache duration while disabling the use of secrets. These changes improve caching behavior and streamline application setup by avoiding secret dependencies. --- src/apps/Elsa.Server.Web/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3e5a9b3f9..7cae86cb3 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -2,6 +2,7 @@ using System.Text.Encodings.Web; using Elsa.Agents; using Elsa.Alterations.Extensions; using Elsa.Alterations.MassTransit.Extensions; +using Elsa.Caching.Options; using Elsa.Common.DistributedHosting.DistributedLocks; using Elsa.Common.RecurringTasks; using Elsa.Common.Serialization; @@ -80,7 +81,7 @@ const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; const bool useMultitenancy = false; const bool useTenantsFromConfiguration = false; const bool useAgents = false; -const bool useSecrets = true; +const bool useSecrets = false; const bool disableVariableWrappers = false; var builder = WebApplication.CreateBuilder(args); @@ -599,7 +600,7 @@ services.Configure(options => services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); -//services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); +services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); services.AddHealthChecks(); services.AddControllers(); services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*"))); From 41d5160c41fe22b3385c6581aeaab56454cc519f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 Jan 2025 10:29:03 +0100 Subject: [PATCH 045/166] Update workflow creation to use async commit method Replaced synchronous `CreateWorkflowInstance` with `CreateAndCommitWorkflowInstanceAsync` to ensure instance creation is properly committed. Included support for cancellation tokens to improve process control and reliability. --- .../Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index a81b08e66..6b73d2906 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -40,7 +40,7 @@ public class LocalWorkflowClient( Properties = request.Properties }; - workflowInstanceManager.CreateWorkflowInstance(workflowGraph.Workflow, options); + await workflowInstanceManager.CreateAndCommitWorkflowInstanceAsync(workflowGraph.Workflow, options, cancellationToken); return new(); } From d759624321615e3ec978ba395b2add5627d06f5c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 Jan 2025 11:40:54 +0100 Subject: [PATCH 046/166] Refactor array type handling in variable and JSON converters Updated the logic to use `MakeArrayType` for array handling instead of generic collection types, ensuring consistency and better alignment with expected type structures. Adjustments were made in both the `VariableDefinitionMapper` and the `TypeJsonConverter`. --- .../Serialization/Converters/TypeJsonConverter.cs | 2 +- .../Mappers/VariableDefinitionMapper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs index c8a4e9a3d..24ff2dfcf 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs @@ -36,7 +36,7 @@ public class TypeJsonConverter : JsonConverter { var elementTypeAlias = typeAlias[..^"[]".Length]; var elementType = _wellKnownTypeRegistry.TryGetType(elementTypeAlias, out var t) ? t : Type.GetType(elementTypeAlias)!; - return typeof(List<>).MakeGenericType(elementType); + return elementType.MakeArrayType(); } return _wellKnownTypeRegistry.TryGetType(typeAlias, out var type) ? type : Type.GetType(typeAlias); diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 787c5d341..2d741dbc6 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -30,7 +30,7 @@ public class VariableDefinitionMapper if (!_wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type)) return null; - var valueType = source.IsArray ? typeof(ICollection<>).MakeGenericType(type) : type; + var valueType = source.IsArray ? type.MakeArrayType() : type; var variableGenericType = typeof(Variable<>).MakeGenericType(valueType); var variable = (Variable)Activator.CreateInstance(variableGenericType)!; From 48c453d153c624f6fdbac0c4a625cc7b3a326d2d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 11 Jan 2025 19:12:38 +0100 Subject: [PATCH 047/166] Enable customizable Hangfire job storage and deprecate obsolete APIs. Added support for configuring Hangfire job storage per database provider, including PostgreSql, SQLite, and SQL Server. Introduced new methods for flexible Hangfire setup, while marking older APIs and storage configuration extensions as obsolete. Refactored related configurations for streamlined and centralized job scheduling logic. --- Directory.Packages.props | 77 ++++++++++--------- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + src/apps/Elsa.Server.Web/Program.cs | 41 +++++++++- src/apps/Elsa.Server.Web/appsettings.json | 9 +-- .../Elsa.Hangfire/Elsa.Hangfire.csproj | 10 +-- .../Extensions/ModuleExtensions.cs | 2 + .../Elsa.Hangfire/Features/HangfireFeature.cs | 54 ++++++++++--- .../HangfireSqlServerStorageFeature.cs | 20 ++--- .../Features/HangfireSqliteStorageFeature.cs | 16 ++-- .../Contexts/WorkflowExecutionContext.cs | 20 ++--- 10 files changed, 150 insertions(+), 100 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd2e28eac..005c17183 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,6 +49,7 @@ + @@ -112,44 +113,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 8801d54d9..30802d6a5 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -58,6 +58,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 7cae86cb3..d065d535b 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -49,6 +49,12 @@ using Elsa.Workflows.Runtime.Distributed.Extensions; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stores; using Elsa.Workflows.Runtime.Tasks; +using Hangfire; +using Hangfire.MemoryStorage; +using Hangfire.PostgreSql; +using Hangfire.PostgreSql.Factories; +using Hangfire.SqlServer; +using Hangfire.Storage.SQLite; using JetBrains.Annotations; using Medallion.Threading.FileSystem; using Medallion.Threading.Postgres; @@ -78,8 +84,8 @@ const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; -const bool useMultitenancy = false; -const bool useTenantsFromConfiguration = false; +const bool useMultitenancy = true; +const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; @@ -133,7 +139,36 @@ services }); if (useHangfire) - elsa.UseHangfire(); + { + JobStorage jobStorage; + if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + { + jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + })); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) + { + jobStorage = new SQLiteStorage(sqliteConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) + { + jobStorage = new SqlServerStorage(sqlServerConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else + { + jobStorage = new MemoryStorage(); + } + + elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage)); + } elsa .AddActivitiesFrom() diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 66598c7b9..221cdcf22 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -2,14 +2,7 @@ "Logging": { "LogLevel": { "Default": "Warning", - "Elsa": "Warning", - "MassTransit": "Warning", - "Microsoft.Extensions.Http": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore": "Warning", - "Microsoft.AspNetCore": "Warning", - "Quartz": "Warning", - "System.Net.Http": "Warning" + "Microsoft.Hosting.Lifetime": "Information" } }, "HostBuilder": { diff --git a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj index 8b940ae22..2474602ce 100644 --- a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj +++ b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj @@ -8,14 +8,14 @@ - - - + + + - - + + diff --git a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs index ab493f267..b06060b46 100644 --- a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs @@ -25,6 +25,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); @@ -34,6 +35,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); diff --git a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs index 25c359cfe..0162ccd11 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs @@ -1,5 +1,7 @@ using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Workflows.Runtime.Features; using Hangfire; using Hangfire.MemoryStorage; using Newtonsoft.Json; @@ -9,35 +11,63 @@ namespace Elsa.Hangfire.Features; /// /// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature. /// -public class HangfireFeature : FeatureBase +[DependsOn(typeof(WorkflowRuntimeFeature))] // Ensure that the workflow runtime feature's hosted services have executed before Hangfire Server starts. +public class HangfireFeature(IModule module) : FeatureBase(module) { - /// - public HangfireFeature(IModule module) : base(module) - { - } - /// /// A delegate that configures Hangfire. /// - public Action ConfigureHangfire { get; set; } = (_, cfg) => cfg.UseMemoryStorage(); + private Action _configureHangfire = (_, _) => { }; /// /// A delegate that configures Hangfire's background job server options. /// - public Action ConfigureBackgroundServerOptions { get; set; } = (_, _) => { }; + private Action _configureBackgroundServerOptions = (_, _) => { }; /// /// A delegate that creates a job storage instance. /// - public Func CreateJobStorage { get; set; } = () => new MemoryStorage(); + private Func _createJobStorage = () => new MemoryStorage(); + + /// + /// Configures Hangfire. + /// + public HangfireFeature ConfigureHangfire(Action configure) + { + _configureHangfire += configure; + return this; + } + + /// + /// Configures Hangfire's background job server options. + /// + public HangfireFeature ConfigureBackgroundServerOptions(Action configure) + { + _configureBackgroundServerOptions += configure; + return this; + } + + public HangfireFeature UseMemoryStorage() + { + return UseJobStorage(new MemoryStorage()); + } + + public HangfireFeature UseJobStorage(JobStorage storage) + { + _createJobStorage = () => storage; + return this; + } /// public override void Apply() { + var jobStorage = _createJobStorage(); + Action configAction = (sp, cfg) => { cfg.UseSimpleAssemblyNameTypeSerializer(); cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects); + cfg.UseStorage(jobStorage); }; Action serverOptionsAction = (sp, options) => @@ -46,10 +76,10 @@ public class HangfireFeature : FeatureBase options.SchedulePollingInterval = TimeSpan.FromSeconds(1); }; - configAction += ConfigureHangfire; - serverOptionsAction += ConfigureBackgroundServerOptions; + configAction += _configureHangfire; + serverOptionsAction += _configureBackgroundServerOptions; Services.AddHangfire(configAction); - Services.AddHangfireServer(serverOptionsAction, CreateJobStorage()); + Services.AddHangfireServer(serverOptionsAction, jobStorage); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs index 480a5a3bf..fafb92528 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs @@ -11,18 +11,14 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqlServerStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqlServerStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; - + public string NameOrConnectionString { get; set; } = null!; + /// /// Configures the SQL Server storage options. /// @@ -41,13 +37,11 @@ public class HangfireSqlServerStorageFeature : FeatureBase UseRecommendedIsolationLevel = true }; ConfigureSqlServerStorageOptions(storageOptions); - - hangfireFeature.ConfigureHangfire = (_, cfg) => + + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SqlServerStorage(NameOrConnectionString, storageOptions); + }); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs index 7982a078b..dcd7353df 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs @@ -10,17 +10,13 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqliteStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqliteStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; + public string NameOrConnectionString { get; set; } = null!; /// /// Configures the SQL Server storage options. @@ -38,12 +34,10 @@ public class HangfireSqliteStorageFeature : FeatureBase }; ConfigureSqlServerStorageOptions(storageOptions); - hangfireFeature.ConfigureHangfire = (_, cfg) => + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SQLiteStorage(NameOrConnectionString, storageOptions); + }); }); } } \ 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 bd8623ce8..545ba3bbc 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -22,7 +22,7 @@ namespace Elsa.Workflows; /// The child being scheduled. /// The delegate to invoke when the scheduled activity completes. /// An optional tag. -public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default); +public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = null); /// /// Provides context to the currently executing workflow. @@ -239,7 +239,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public WorkflowSubStatus SubStatus { get; internal set; } /// The root associated with the execution context. - public MemoryRegister MemoryRegister { get; private set; } = default!; + public MemoryRegister MemoryRegister { get; private set; } = null!; /// A unique ID of the execution context. public string Id { get; set; } @@ -406,7 +406,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// /// Registers a completion callback for the specified activity. /// - internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default) + internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = null, object? tag = null) { var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag); _completionCallbackEntries.Add(entry); @@ -420,7 +420,7 @@ public partial class WorkflowExecutionContext : IExecutionContext var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); if (entry == null) - return default; + return null; RemoveCompletionCallback(entry); return entry; @@ -449,25 +449,25 @@ public partial class WorkflowExecutionContext : IExecutionContext ? FindActivityByInstanceId(handle.ActivityInstanceId) : handle.ActivityHash != null ? FindActivityByHash(handle.ActivityHash) - : default; + : null; } /// /// Returns the with the specified activity ID from the workflow graph. /// - public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default; + public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null; /// /// Returns the with the specified hash of the activity node ID from the workflow graph. /// /// The hash of the activity node ID. /// The with the specified hash of the activity node ID. - public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : default; + public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null; /// Returns the containing the specified activity from the workflow graph. public ActivityNode? FindNodeByActivity(IActivity activity) { - return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default; + return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null; } /// Returns the associated with the specified activity ID. @@ -526,7 +526,7 @@ public partial class WorkflowExecutionContext : IExecutionContext } /// Creates a new for the specified activity. - public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default) + public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = null) { var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); var tag = options?.Tag; @@ -568,7 +568,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister()); /// Returns the last activity result. - public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default; + public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : null; /// Adds the specified to the workflow execution context. public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); From 60db7cb96479b8f4f8d6531e985dd671b31ab6e1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 11 Jan 2025 19:55:06 +0100 Subject: [PATCH 048/166] Disable multitenancy and streamline NuGet publishing workflow. Multitenancy is now disabled by default in `Program.cs` to simplify configuration. Additionally, removed the `publish_preview_nuget` step and refined conditions for `publish_nuget` to improve the workflow and align with the release process. --- .github/workflows/packages.yml | 17 +---------------- src/apps/Elsa.Server.Web/Program.cs | 2 +- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index f77924633..f594544c1 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -99,27 +99,12 @@ jobs: - name: Publish to feedz.io run: dotnet nuget push *.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s ${{ env.feedz_feed_source }} --skip-duplicate - publish_preview_nuget: - name: Publish preview to nuget.org - needs: build - runs-on: ubuntu-latest - timeout-minutes: 10 - if: ${{ github.event_name == 'prereleased' && github.event.action == 'published' }} - steps: - - name: Download Packages - uses: actions/download-artifact@v4.1.7 - with: - name: elsa-nuget-packages - - - name: Publish to nuget.org - run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_API_KEY }} -s ${{ env.nuget_feed_source }} --skip-duplicate - publish_nuget: name: Publish release to nuget.org needs: build runs-on: ubuntu-latest timeout-minutes: 10 - if: ${{ github.event_name == 'release' && github.event.action == 'published' }} + if: ${{ github.event.action == 'published' }} steps: - name: Download Packages uses: actions/download-artifact@v4.1.7 diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index d065d535b..3e1f36246 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -84,7 +84,7 @@ const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; -const bool useMultitenancy = true; +const bool useMultitenancy = false; const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; From b2331f73db3da5b7f4c432ee83ca516874eb7d31 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 11 Jan 2025 20:23:05 +0100 Subject: [PATCH 049/166] Handle collection and array serialization in TypeJsonConverter Refactored TypeJsonConverter to distinguish and properly handle serialization of arrays and generic collections. Updated integration tests to include cases for round-tripping primitive arrays and collections for improved coverage. --- .../Converters/TypeJsonConverter.cs | 23 ++++++++++-- .../Serialization/JsonSerialization/Tests.cs | 35 +++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs index 24ff2dfcf..816b5fb5b 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs @@ -31,13 +31,21 @@ public class TypeJsonConverter : JsonConverter { var typeAlias = reader.GetString()!; - // Handle collection types. + // Handle array types. if (typeAlias.EndsWith("[]")) { - var elementTypeAlias = typeAlias[..^"[]".Length]; + var elementTypeAlias = typeAlias[..^2]; var elementType = _wellKnownTypeRegistry.TryGetType(elementTypeAlias, out var t) ? t : Type.GetType(elementTypeAlias)!; return elementType.MakeArrayType(); } + + // Handle collection types. + if (typeAlias.EndsWith("()")) + { + var elementTypeAlias = typeAlias[..^"()".Length]; + var elementType = _wellKnownTypeRegistry.TryGetType(elementTypeAlias, out var t) ? t : Type.GetType(elementTypeAlias)!; + return typeof(List<>).MakeGenericType(elementType); + } return _wellKnownTypeRegistry.TryGetType(typeAlias, out var type) ? type : Type.GetType(typeAlias); } @@ -45,6 +53,15 @@ public class TypeJsonConverter : JsonConverter /// public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) { + // Handle array types. + if (value.IsArray) + { + var elementType = value.GetElementType()!; + var elementTypeAlias = _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementTypeAliasValue) ? elementTypeAliasValue : elementType.GetSimpleAssemblyQualifiedName(); + writer.WriteStringValue($"{elementTypeAlias}[]"); + return; + } + // Handle collection types. if (value is { IsGenericType: true, GenericTypeArguments.Length: 1 }) { @@ -53,7 +70,7 @@ public class TypeJsonConverter : JsonConverter if (typedEnumerable.IsAssignableFrom(value) && _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementTypeAlias)) { - writer.WriteStringValue($"{elementTypeAlias}[]"); + writer.WriteStringValue($"{elementTypeAlias}()"); return; } } diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs index 1a6ff38d5..f4a853f28 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Serialization/JsonSerialization/Tests.cs @@ -74,7 +74,8 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) { var dict = new Dictionary { - { "Content", new List() + { + "Content", new List() { new() { @@ -94,9 +95,10 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) { var dict = new Dictionary { - { "Content", new List + { + "Content", new List { - Guid.NewGuid() + Guid.NewGuid() } } }; @@ -106,6 +108,24 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) Assert.Equal(typeof(List), result.GetType()); } + [Fact] + public void RoundtripPrimitiveArrays() + { + var dict = new Dictionary + { + { + "Content", new[] + { + Guid.NewGuid() + } + } + }; + var jsonSerialized = SerializeUsingPayloadSerializer(dict); + var transformationModel = DeSerializeDictionaryUsingPayloadSerializer(jsonSerialized); + var result = transformationModel["Content"]; + Assert.Equal(typeof(Guid[]), result.GetType()); + } + private string SerializeUsingPayloadSerializer(object obj) { var payloadSerializer = _services.GetRequiredService(); @@ -134,10 +154,11 @@ public class SerializationTests(ITestOutputHelper testOutputHelper) var dict = new Dictionary { - { "StatusCode", "Created" }, - { "Content",isArray ? - (type == typeof(JArray)? JArray.Parse(jsonContent):JsonArray.Parse(jsonContent)): - (type == typeof(JObject)? JObject.Parse(jsonContent):JsonObject.Parse(jsonContent)) + { + "StatusCode", "Created" + }, + { + "Content", isArray ? (type == typeof(JArray) ? JArray.Parse(jsonContent) : JsonArray.Parse(jsonContent)) : (type == typeof(JObject) ? JObject.Parse(jsonContent) : JsonObject.Parse(jsonContent)) } }; return dict; From e365d446371fc5a4693c403df56897f93e89088d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Vasile=20Vu=C8=99can?= Date: Mon, 13 Jan 2025 16:48:24 +0200 Subject: [PATCH 050/166] Added DeleteVariablesAsync method for the workflow context --- .../Contracts/IVariablePersistenceManager.cs | 7 ++++- .../Services/VariablePersistenceManager.cs | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IVariablePersistenceManager.cs b/src/modules/Elsa.Workflows.Core/Contracts/IVariablePersistenceManager.cs index aa4105e3b..bb61acd67 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IVariablePersistenceManager.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IVariablePersistenceManager.cs @@ -19,7 +19,12 @@ public interface IVariablePersistenceManager Task SaveVariablesAsync(WorkflowExecutionContext context); /// - /// Deletes the specified variables from the . + /// Deletes the specified variables from the . /// Task DeleteVariablesAsync(ActivityExecutionContext context); + + /// + /// Deletes the specified variables from the . + /// + Task DeleteVariablesAsync(WorkflowExecutionContext context); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs index 2ca6dcc86..c486c24e8 100644 --- a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs +++ b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs @@ -120,6 +120,33 @@ public class VariablePersistenceManager(IStorageDriverManager storageDriverManag } } + /// + public async Task DeleteVariablesAsync(WorkflowExecutionContext context) + { + var cancellationToken = context.CancellationTokens.ApplicationCancellationToken; + var activityContexts = context.ActivityExecutionContexts.ToList(); + + foreach (var activityContext in activityContexts) + { + var variables = GetLocalVariables(activityContext).ToList(); + + foreach (var variable in variables) + { + var block = variable.GetBlock(activityContext.ExpressionExecutionContext); + var metadata = (VariableBlockMetadata)block.Metadata!; + var driver = _storageDriverManager.Get(metadata.StorageDriverType!); + + if (driver == null) + continue; + + var id = GetStateId(variable); + var storageDriverContext = new StorageDriverContext(activityContext, variable, cancellationToken); + + await driver.DeleteAsync(id, storageDriverContext); + } + } + } + private IEnumerable GetLocalVariables(IExecutionContext context) => context.Variables; private MemoryBlock EnsureBlock(MemoryRegister register, Variable variable) From 190d6c3bda9afde5c2f0d0616c1da864cbaa210b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Vasile=20Vu=C8=99can?= Date: Tue, 14 Jan 2025 12:23:21 +0200 Subject: [PATCH 051/166] Improvements --- .../Services/VariablePersistenceManager.cs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs index c486c24e8..1e19a9d88 100644 --- a/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs +++ b/src/modules/Elsa.Workflows.Core/Services/VariablePersistenceManager.cs @@ -123,27 +123,11 @@ public class VariablePersistenceManager(IStorageDriverManager storageDriverManag /// public async Task DeleteVariablesAsync(WorkflowExecutionContext context) { - var cancellationToken = context.CancellationTokens.ApplicationCancellationToken; var activityContexts = context.ActivityExecutionContexts.ToList(); foreach (var activityContext in activityContexts) { - var variables = GetLocalVariables(activityContext).ToList(); - - foreach (var variable in variables) - { - var block = variable.GetBlock(activityContext.ExpressionExecutionContext); - var metadata = (VariableBlockMetadata)block.Metadata!; - var driver = _storageDriverManager.Get(metadata.StorageDriverType!); - - if (driver == null) - continue; - - var id = GetStateId(variable); - var storageDriverContext = new StorageDriverContext(activityContext, variable, cancellationToken); - - await driver.DeleteAsync(id, storageDriverContext); - } + await DeleteVariablesAsync(activityContext); } } From efd114944cff904ba727494abca3a19646673657 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Jan 2025 23:16:29 +0100 Subject: [PATCH 052/166] Refactor and enhance JavaScript and object conversions. Replaced InputProxy with alternative implementations, adding flexibility to handle inputs. Introduced a JsonElementConverter to deepen JavaScript and JSON element integration. Enhanced testing and object conversion logic, improving type handling and support for complex JSON scenarios. --- Directory.Packages.props | 2 +- src/apps/Elsa.Server.Web/Program.cs | 6 + .../Workflows/SetVariableWorkflow.cs | 23 ++ .../GenerateWorkflowInputAccessors.cs | 3 +- .../GenerateWorkflowVariableAccessors.cs | 3 +- src/modules/Elsa.CSharp/Models/Globals.cs | 6 - src/modules/Elsa.CSharp/Models/InputProxy.cs | 30 -- src/modules/Elsa.CSharp/Models/OutputProxy.cs | 8 +- .../Helpers/ObjectConverter.cs | 32 +- .../ObjectConverters/JsonElementConverter.cs | 37 +++ .../Services/JintJavaScriptEvaluator.cs | 2 +- .../JsonElementConverterTests.cs | 155 ++++++++++ .../ObjectConversion/Tests.cs | 286 ++++++++++++++++++ 13 files changed, 535 insertions(+), 58 deletions(-) create mode 100644 src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs delete mode 100644 src/modules/Elsa.CSharp/Models/InputProxy.cs create mode 100644 src/modules/Elsa.JavaScript/ObjectConverters/JsonElementConverter.cs create mode 100644 test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs create mode 100644 test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 005c17183..891e11d74 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,7 +87,7 @@ - + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3e1f36246..3f7f5f733 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -369,6 +369,12 @@ services // Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11 // alternatively, you can set the PYTHONNET_PYDLL environment variable. configuration.GetSection("Scripting:Python").Bind(options); + + options.AddScript(sb => + { + sb.AppendLine("def greet():"); + sb.AppendLine(" return \"Hello, welcome to Python!\""); + }); }; }) .UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default) diff --git a/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs b/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs new file mode 100644 index 000000000..bbbe76aa4 --- /dev/null +++ b/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs @@ -0,0 +1,23 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Server.Web.Workflows; + +public class SetVariableWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var myVar = builder.WithVariable(); + builder.Root = new Sequence + { + Activities = + [ + new SetVariable + { + Variable = myVar, + Value = new(42) + } + ] + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowInputAccessors.cs b/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowInputAccessors.cs index 1fcf00b72..b8702fdc9 100644 --- a/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowInputAccessors.cs +++ b/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowInputAccessors.cs @@ -49,7 +49,8 @@ public class GenerateWorkflowInputAccessors(IOptions options) : I } sb.AppendLine("}"); - sb.AppendLine("var Inputs = new WorkflowInputsProxy(ExecutionContext);"); + sb.AppendLine("var Inputs = new WorkflowInputsProxy(ExecutionContext);"); // Obsolete; use Input instead. + sb.AppendLine("var Input = Inputs;"); notification.AppendScript(sb.ToString()); return Task.CompletedTask; } diff --git a/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs b/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs index e63ed99d5..c7f03d0b1 100644 --- a/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs +++ b/src/modules/Elsa.CSharp/Handlers/GenerateWorkflowVariableAccessors.cs @@ -48,7 +48,8 @@ public class GenerateWorkflowVariableAccessors(IOptions options) } sb.AppendLine("}"); - sb.AppendLine("var Variables = new WorkflowVariablesProxy(ExecutionContext);"); + sb.AppendLine("var Variables = new WorkflowVariablesProxy(ExecutionContext);"); // Obsolete; use Variable instead. + sb.AppendLine("var Variable = Variables;"); notification.AppendScript(sb.ToString()); return Task.CompletedTask; } diff --git a/src/modules/Elsa.CSharp/Models/Globals.cs b/src/modules/Elsa.CSharp/Models/Globals.cs index 86997a7f7..f86b9693d 100644 --- a/src/modules/Elsa.CSharp/Models/Globals.cs +++ b/src/modules/Elsa.CSharp/Models/Globals.cs @@ -16,7 +16,6 @@ public partial class Globals ExpressionExecutionContext = expressionExecutionContext; Arguments = arguments; ExecutionContext = new ExecutionContextProxy(expressionExecutionContext); - Input = new InputProxy(expressionExecutionContext); Output = new OutputProxy(expressionExecutionContext); Outcome = new OutcomeProxy(expressionExecutionContext); } @@ -31,11 +30,6 @@ public partial class Globals /// public OutputProxy Output { get; set; } - /// - /// Provides access to workflow inputs. - /// - public InputProxy Input { get; set; } - /// /// Gets the current execution context. /// diff --git a/src/modules/Elsa.CSharp/Models/InputProxy.cs b/src/modules/Elsa.CSharp/Models/InputProxy.cs deleted file mode 100644 index 98b16df40..000000000 --- a/src/modules/Elsa.CSharp/Models/InputProxy.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Elsa.Expressions.Models; -using Elsa.Extensions; - -namespace Elsa.CSharp.Models; - -/// -/// Provides access to workflow inputs. -/// -public class InputProxy -{ - private readonly ExpressionExecutionContext _expressionExecutionContext; - - /// - /// Initializes a new instance of the class. - /// - public InputProxy(ExpressionExecutionContext expressionExecutionContext) - { - _expressionExecutionContext = expressionExecutionContext; - } - - /// - /// Gets the value of the specified input. - /// - public object? Get(string name) => _expressionExecutionContext.GetInput(name); - - /// - /// Gets the value of the specified input. - /// - public T? Get(string name) => _expressionExecutionContext.GetInput(name); -} \ No newline at end of file diff --git a/src/modules/Elsa.CSharp/Models/OutputProxy.cs b/src/modules/Elsa.CSharp/Models/OutputProxy.cs index fc7f45ceb..b641aedd5 100644 --- a/src/modules/Elsa.CSharp/Models/OutputProxy.cs +++ b/src/modules/Elsa.CSharp/Models/OutputProxy.cs @@ -25,7 +25,7 @@ public class OutputProxy /// The ID or name of the activity that produced the output. /// The name of the output. /// The value of the output. - public object? From(string activityIdOrName, string? outputName = default) => _expressionExecutionContext.GetOutput(activityIdOrName, outputName); + public object? From(string activityIdOrName, string? outputName = null) => _expressionExecutionContext.GetOutput(activityIdOrName, outputName); /// /// Gets the value of the specified output from the specified activity. @@ -33,7 +33,7 @@ public class OutputProxy /// The ID or name of the activity that produced the output. /// The name of the output. /// The value of the output. - public T? From(string activityIdOrName, string? outputName = default) => Get(activityIdOrName, outputName).ConvertTo(); + public T? From(string activityIdOrName, string? outputName = null) => From(activityIdOrName, outputName).ConvertTo(); /// /// Gets the value of the specified output. @@ -42,7 +42,7 @@ public class OutputProxy /// The name of the output. /// The value of the output. [Obsolete("Use From instead.")] - public object? Get(string activityIdOrName, string? outputName = default) => From(activityIdOrName, outputName); + public object? Get(string activityIdOrName, string? outputName = null) => From(activityIdOrName, outputName); /// /// Gets the value of the specified output. @@ -51,7 +51,7 @@ public class OutputProxy /// The name of the output. /// The value of the output. [Obsolete("Use From instead.")] - public T? Get(string activityIdOrName, string? outputName = default) => From(activityIdOrName, outputName); + public T? Get(string activityIdOrName, string? outputName = null) => From(activityIdOrName, outputName); /// /// Gets the result of the last activity that executed. diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 7bb35b1b4..04bfa40e7 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -52,19 +52,22 @@ public static class ObjectConverter /// Attempts to convert the source value into the destination type. /// public static T? ConvertTo(this object? value, ObjectConverterOptions? converterOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), converterOptions) : default; - + private static JsonSerializerOptions? _defaultSerializerOptions; private static JsonSerializerOptions? _internalSerializerOptions; - + private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, ReferenceHandler = ReferenceHandler.Preserve, - Converters = { new JsonStringEnumConverter() }, + Converters = + { + new JsonStringEnumConverter() + }, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; - + private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new JsonSerializerOptions { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) @@ -81,7 +84,7 @@ public static class ObjectConverter var sourceType = value.GetType(); - if (sourceType == targetType) + if (targetType.IsAssignableFrom(sourceType)) return value; var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions; @@ -99,7 +102,7 @@ public static class ObjectConverter return jsonElement.Deserialize(targetType, serializerOptions); } - if (value is JsonNode jsonNode) + if (value is JsonNode jsonNode and not JsonArray) // If the value is a JsonNode, we can convert it to the target type. If it's a JsonArray, we let the enumerable conversion logic handle it. { return underlyingTargetType switch { @@ -113,13 +116,13 @@ public static class ObjectConverter if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object)) { var stringValue = (string)value; - + if (underlyingTargetType == typeof(byte[])) { // Byte arrays are serialized to base64, so in this case, we convert the string back to the requested target type of byte[]. return Convert.FromBase64String(stringValue); } - + try { var firstChar = stringValue.TrimStart().FirstOrDefault(); @@ -146,7 +149,7 @@ public static class ObjectConverter return ConvertAnyDateType(value, underlyingTargetType); var internalSerializerOptions = InternalSerializerOptions; - + if (typeof(IDictionary).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass) { if (typeof(ExpandoObject) == underlyingTargetType) @@ -189,7 +192,7 @@ public static class ObjectConverter if (underlyingSourceType == typeof(double)) return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture)); - + if (underlyingSourceType == typeof(long)) return Enum.ToObject(underlyingTargetType, Convert.ChangeType(value, typeof(int), CultureInfo.InvariantCulture)); } @@ -204,7 +207,10 @@ public static class ObjectConverter // Perhaps it's a bit of a leap, but if the input is a string and the target type is IEnumerable, then let's assume the string is a comma-separated list of strings. if (typeof(IEnumerable).IsAssignableFrom(underlyingTargetType)) - return new[] { s }; + return new[] + { + s + }; } if (value is IEnumerable enumerable) @@ -247,9 +253,7 @@ public static class ObjectConverter { var dateTypes = new[] { - typeof(DateTime), - typeof(DateTimeOffset), - typeof(DateOnly) + typeof(DateTime), typeof(DateTimeOffset), typeof(DateOnly) }; return dateTypes.Contains(type); diff --git a/src/modules/Elsa.JavaScript/ObjectConverters/JsonElementConverter.cs b/src/modules/Elsa.JavaScript/ObjectConverters/JsonElementConverter.cs new file mode 100644 index 000000000..499f1a990 --- /dev/null +++ b/src/modules/Elsa.JavaScript/ObjectConverters/JsonElementConverter.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; + +namespace Elsa.JavaScript.ObjectConverters; + +internal class JsonElementConverter : IObjectConverter +{ + public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result) + { + if (value is JsonElement jsonElement) + { + result = ConvertJsonElementToJsValue(engine, jsonElement); + return true; + } + + result = JsValue.Null; + return false; + } + + private static JsValue ConvertJsonElementToJsValue(Engine engine, JsonElement element) => + element.ValueKind switch + { + JsonValueKind.Object => JsValue.FromObject(engine, JsonObject.Create(element)), + JsonValueKind.Array => JsValue.FromObject(engine, JsonArray.Create(element)), + JsonValueKind.String => JsValue.FromObject(engine, element.GetString()), + JsonValueKind.Number => element.TryGetInt32(out var intValue) ? JsNumber.Create(intValue) : JsNumber.Create(element.GetDouble()), + JsonValueKind.True => JsBoolean.True, + JsonValueKind.False => JsBoolean.False, + JsonValueKind.Undefined => JsValue.Undefined, + JsonValueKind.Null => JsValue.Null, + _ => throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}") + }; +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index 0f16cab66..94fdf51b6 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -91,7 +91,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification private void ConfigureObjectConverters(Jint.Options options) { - options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new EnumToStringConverter()]); + options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new EnumToStringConverter(), new JsonElementConverter()]); } private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options) diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs b/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs new file mode 100644 index 000000000..f48eb58c6 --- /dev/null +++ b/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs @@ -0,0 +1,155 @@ +using System.Text.Json; +using Elsa.Expressions.Models; +using Elsa.JavaScript.Contracts; +using Elsa.Testing.Shared; +using Elsa.Workflows.Memory; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.JavaScript.IntegrationTests; + +public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) +{ + private readonly IServiceProvider _serviceProvider = new TestApplicationBuilder(testOutputHelper).Build(); + + [Fact(DisplayName = "JsonElement JsonObject can be passed to JavaScript")] + public async Task TestJsonObjectPassedAsJsonElement() + { + var javaScriptEvaluator = _serviceProvider.GetRequiredService(); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister()); + var jsonVariable = new Variable + { + Name = "JsonVariable" + }; + + string jsonString = "{\"name\": \"John\", \"age\": 30}"; + JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + + jsonVariable.Set(expressionExecutionContext, jsonElement); + var script = "getVariable('JsonVariable').age"; + var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(int), expressionExecutionContext); + Assert.Equal(30, result); + } + + [Fact(DisplayName = "JsonElement JsonArray can be passed to JavaScript")] + public async Task TestJsonArrayPassedAsJsonElement() + { + var javaScriptEvaluator = _serviceProvider.GetRequiredService(); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister()); + var jsonVariable = new Variable + { + Name = "JsonVariable" + }; + + string jsonString = "[1, 2, 3, 4, 5, 6]"; + JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + + jsonVariable.Set(expressionExecutionContext, jsonElement); + var script = "getVariable('JsonVariable')[3]"; + var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(int), expressionExecutionContext); + Assert.Equal(4, result); + } + + [Fact(DisplayName = "JsonElement string can be passed to JavaScript")] + public async Task TestStringPassedAsJsonElement() + { + var javaScriptEvaluator = _serviceProvider.GetRequiredService(); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister()); + var jsonVariable = new Variable + { + Name = "JsonVariable" + }; + + string jsonString = "\"I'm just a string\""; + JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + + jsonVariable.Set(expressionExecutionContext, jsonElement); + var script = "getVariable('JsonVariable')"; + var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext); + Assert.Equal("I'm just a string", result); + } + + [Fact(DisplayName = "JsonElement boolean can be passed to JavaScript")] + public async Task TestBooleanPassedAsJsonElement() + { + var javaScriptEvaluator = _serviceProvider.GetRequiredService(); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister()); + var jsonVariable = new Variable + { + Name = "JsonVariable" + }; + + string jsonString = "false"; + JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + + jsonVariable.Set(expressionExecutionContext, jsonElement); + var script = "getVariable('JsonVariable')"; + var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(bool), expressionExecutionContext); + Assert.Equal(false, result); + } + + [Fact(DisplayName = "JsonElement containing nested Json objects and arrays be passed to JavaScript")] + public async Task TestNestedJsonPassedAsJsonElement() + { + var javaScriptEvaluator = _serviceProvider.GetRequiredService(); + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister()); + var jsonVariable = new Variable + { + Name = "JsonVariable" + }; + + string jsonString = @" + { + ""name"": ""John Doe"", + ""age"": 35, + ""address"": { + ""street"": ""123 Main St"", + ""city"": ""Springfield"", + ""zip"": ""12345"", + ""coordinates"": { ""lat"": 40.7128, ""lng"": -74.006 } + }, + ""skills"": [ + { ""name"": ""Programming"", ""level"": ""Advanced"" }, + { ""name"": ""Writing"", ""level"": ""Intermediate"" } + ], + ""projects"": [ + { + ""title"": ""Project A"", + ""status"": ""Completed"", + ""team"": [""Alice"", ""Bob""] + }, + { + ""title"": ""Project B"", + ""status"": ""In Progress"", + ""team"": [""Charlie"", ""David"", ""Eve""] + } + ], + ""isEmployed"": true, + ""contact"": { ""email"": ""john.doe@example.com"", ""phone"": ""555-1234"" } + }"; + JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + + jsonVariable.Set(expressionExecutionContext, jsonElement); + + var script = "getVariable('JsonVariable').projects[1].team[0]"; + var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext); + Assert.Equal("Charlie", result); + + var script2 = "getVariable('JsonVariable').isEmployed"; + var result2 = await javaScriptEvaluator.EvaluateAsync(script2, typeof(bool), expressionExecutionContext); + Assert.Equal(true, result2); + + var script3 = "getVariable('JsonVariable').skills[0].level"; + var result3 = await javaScriptEvaluator.EvaluateAsync(script3, typeof(string), expressionExecutionContext); + Assert.Equal("Advanced", result3); + + var script4 = "getVariable('JsonVariable').address.coordinates.lat"; + var result4 = await javaScriptEvaluator.EvaluateAsync(script4, typeof(double), expressionExecutionContext); + Assert.Equal(40.7128, result4); + + var script5 = "getVariable('JsonVariable').age"; + var result5 = await javaScriptEvaluator.EvaluateAsync(script5, typeof(int), expressionExecutionContext); + Assert.Equal(35, result5); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs new file mode 100644 index 000000000..6525f5d0e --- /dev/null +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs @@ -0,0 +1,286 @@ +using System.Dynamic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Elsa.Expressions.Exceptions; +using Elsa.Expressions.Helpers; + +namespace Elsa.Workflows.Core.UnitTests.ObjectConversion; + +public class Tests +{ + [Fact] + public void TryConvertTo_SameType_ReturnsSuccess() + { + // Arrange + var value = 42; + + // Act + var result = value.TryConvertTo(); + + // Assert + Assert.True(result.Success); + Assert.Equal(42, result.Value); + } + + [Fact] + public void TryConvertTo_DifferentType_ReturnsConvertedValue() + { + // Arrange + var value = "42"; + + // Act + var result = value.TryConvertTo(); + + // Assert + Assert.True(result.Success); + Assert.Equal(42, result.Value); + } + + [Fact] + public void TryConvertTo_InvalidConversion_ReturnsFailure() + { + // Arrange + var value = "invalid"; + + // Act + var result = value.TryConvertTo(); + + // Assert + + // I would have expected this conversion to not be successful. It seems there are many cases like this + //Assert.False(result.Success); + //Assert.NotNull(result.Exception); + + Assert.True(result.Success); + Assert.Equal(0, result.Value); + } + + [Fact] + public void TryConvertTo_InvalidJsonString_ReturnsFailure() + { + // Arrange + var value = "{ invalid json }"; + + // Act + var result = value.TryConvertTo>(); + + // Assert + Assert.False(result.Success); + Assert.NotNull(result.Exception); + } + + [Fact] + public void ConvertTo_NullValue_ReturnsDefault() + { + // Arrange + object? value = null; + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void ConvertTo_JsonElementNumberToString_ReturnsString() + { + // Arrange + var jsonElement = JsonNode.Parse("42").AsValue(); + + // Act + var result = jsonElement.ConvertTo(); + + // Assert + Assert.Equal("42", result); + } + + [Fact] + public void ConvertTo_JsonNodeToExpandoObject_ReturnsExpandoObject() + { + // Arrange + var jsonNode = JsonNode.Parse("{ \"key\": \"value\" }"); + + // Act + var result = jsonNode.ConvertTo(); + + // Assert + dynamic expando = Assert.IsType(result); + object value = expando.key; + + // This is not the result I expect, I would have expected the JsonNode to have been recursively converted + + Assert.True(value is JsonElement); + Assert.Equal(JsonValueKind.String, ((JsonElement)value).ValueKind); + Assert.Equal("value", ((JsonElement)value).GetString()); + } + + [Fact] + public void ConvertTo_StringToDateTime_ReturnsDateTime() + { + // Arrange + var value = "2023-01-01T00:00:00"; + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(new(2023, 1, 1, 0, 0, 0), result); + } + + [Fact] + public void ConvertTo_StringToEnum_ReturnsEnum() + { + // Arrange + var value = "Monday"; + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(DayOfWeek.Monday, result); + } + + [Fact] + public void ConvertTo_StringToByteArray_ReturnsByteArray() + { + // Arrange + var value = Convert.ToBase64String(new byte[] + { + 1, 2, 3 + }); + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(new byte[] + { + 1, 2, 3 + }, result); + } + + [Fact] + public void ConvertTo_InvalidJsonString_ThrowsException() + { + // Arrange + var value = "{ invalid json }"; + + // Act & Assert + Assert.Throws(() => value.ConvertTo>()); + } + + [Fact] + public void ConvertTo_EnumerableToList_ReturnsConvertedList() + { + // Arrange + var value = new[] + { + "1", "2", "3" + }; + + // Act + var result = value.ConvertTo>(); + + // Assert + Assert.Equal(new() + { + 1, + 2, + 3 + }, result); + } + + [Fact] + public void ConvertTo_DateTimeToDateOnly_ReturnsDateOnly() + { + // Arrange + var value = new DateTime(2023, 1, 1); + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(new(2023, 1, 1), result); + } + + [Fact] + public void ConvertTo_DateOnlyToDateTime_ReturnsDateTime() + { + // Arrange + var value = new DateOnly(2023, 1, 1); + + // Act + var result = value.ConvertTo(); + + // Assert + Assert.Equal(new(2023, 1, 1, 0, 0, 0), result); + } + + [Fact] + public void ConvertTo_UnknownConversion_ThrowsInvalidCastException() + { + // Arrange + var value = new object(); + + // Act & Assert + Assert.Throws(() => value.ConvertTo()); + } + + [Fact] + public void ConvertTo_JsonArrayToListOfObject_ReturnsListOfJsonObject() + { + // Arrange + var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]"; + var jsonArray = JsonNode.Parse(jsonArrayString); + var options = new ObjectConverterOptions(); + + // Act + var result = jsonArray.ConvertTo>(options); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.IsType(result[0]); + Assert.IsType(result[1]); + + var firstElement = result[0] as JsonObject; + var secondElement = result[1] as JsonObject; + + Assert.NotNull(firstElement); + Assert.Equal("Alice", firstElement["name"]?.ToString()); + Assert.Equal("30", firstElement["age"]?.ToString()); + + Assert.NotNull(secondElement); + Assert.Equal("Bob", secondElement["name"]?.ToString()); + Assert.Equal("25", secondElement["age"]?.ToString()); + } + + [Fact] + public void ConvertTo_JsonArrayToICollectionOfObject_ReturnsCollectionOfJsonObject() + { + // Arrange + var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]"; + var jsonArray = JsonNode.Parse(jsonArrayString); + var options = new ObjectConverterOptions(); + + // Act + var result = jsonArray.ConvertTo>(options); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.All(result, item => Assert.IsType(item)); + + var firstElement = result.First() as JsonObject; + var secondElement = result.Last() as JsonObject; + + Assert.NotNull(firstElement); + Assert.Equal("Alice", firstElement["name"]?.ToString()); + Assert.Equal("30", firstElement["age"]?.ToString()); + + Assert.NotNull(secondElement); + Assert.Equal("Bob", secondElement["name"]?.ToString()); + Assert.Equal("25", secondElement["age"]?.ToString()); + } +} \ No newline at end of file From 9842a0dd7bcf698186f20a3b093477849a19d7e7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Jan 2025 23:17:32 +0100 Subject: [PATCH 053/166] Remove unused SetVariableWorkflow class The SetVariableWorkflow.cs file was deleted as it was no longer needed. This simplifies the codebase by removing unused workflows and reduces maintenance overhead. --- .../Workflows/SetVariableWorkflow.cs | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs diff --git a/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs b/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs deleted file mode 100644 index bbbe76aa4..000000000 --- a/src/apps/Elsa.Server.Web/Workflows/SetVariableWorkflow.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Elsa.Workflows; -using Elsa.Workflows.Activities; - -namespace Elsa.Server.Web.Workflows; - -public class SetVariableWorkflow : WorkflowBase -{ - protected override void Build(IWorkflowBuilder builder) - { - var myVar = builder.WithVariable(); - builder.Root = new Sequence - { - Activities = - [ - new SetVariable - { - Variable = myVar, - Value = new(42) - } - ] - }; - } -} \ No newline at end of file From cc694bf15baaceff542059afb2ba403984cc6d94 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Jan 2025 23:19:51 +0100 Subject: [PATCH 054/166] Use `var` for local variables in test cases Updated all instances of explicitly typed `string` and `JsonElement` to `var` in `JsonElementConverterTests.cs` to improve code readability and maintain consistency with modern C# coding practices. This change does not affect functionality but aligns with better style conventions. --- .../JsonElementConverterTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs b/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs index f48eb58c6..b7203e554 100644 --- a/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs +++ b/test/integration/Elsa.JavaScript.IntegrationTests/JsonElementConverterTests.cs @@ -23,8 +23,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) Name = "JsonVariable" }; - string jsonString = "{\"name\": \"John\", \"age\": 30}"; - JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + var jsonString = "{\"name\": \"John\", \"age\": 30}"; + var jsonElement = JsonSerializer.Deserialize(jsonString); jsonVariable.Set(expressionExecutionContext, jsonElement); var script = "getVariable('JsonVariable').age"; @@ -42,8 +42,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) Name = "JsonVariable" }; - string jsonString = "[1, 2, 3, 4, 5, 6]"; - JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + var jsonString = "[1, 2, 3, 4, 5, 6]"; + var jsonElement = JsonSerializer.Deserialize(jsonString); jsonVariable.Set(expressionExecutionContext, jsonElement); var script = "getVariable('JsonVariable')[3]"; @@ -61,8 +61,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) Name = "JsonVariable" }; - string jsonString = "\"I'm just a string\""; - JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + var jsonString = "\"I'm just a string\""; + var jsonElement = JsonSerializer.Deserialize(jsonString); jsonVariable.Set(expressionExecutionContext, jsonElement); var script = "getVariable('JsonVariable')"; @@ -80,8 +80,8 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) Name = "JsonVariable" }; - string jsonString = "false"; - JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + var jsonString = "false"; + var jsonElement = JsonSerializer.Deserialize(jsonString); jsonVariable.Set(expressionExecutionContext, jsonElement); var script = "getVariable('JsonVariable')"; @@ -99,7 +99,7 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) Name = "JsonVariable" }; - string jsonString = @" + var jsonString = @" { ""name"": ""John Doe"", ""age"": 35, @@ -128,7 +128,7 @@ public class JsonElementConverterTests(ITestOutputHelper testOutputHelper) ""isEmployed"": true, ""contact"": { ""email"": ""john.doe@example.com"", ""phone"": ""555-1234"" } }"; - JsonElement jsonElement = JsonSerializer.Deserialize(jsonString); + var jsonElement = JsonSerializer.Deserialize(jsonString); jsonVariable.Set(expressionExecutionContext, jsonElement); From 0adc3acd59b37af0d5c70606553b62b96ec87cee Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 14 Jan 2025 23:38:38 +0100 Subject: [PATCH 055/166] Remove unused code from CleanupJob Deleted an unnecessary `using` directive and a debug `Console.WriteLine` call. These changes clean up the code, improve readability, and avoid potential clutter. --- src/modules/Elsa.Retention/Jobs/CleanupJob.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Elsa.Retention/Jobs/CleanupJob.cs b/src/modules/Elsa.Retention/Jobs/CleanupJob.cs index 330e9d41e..764e21395 100644 --- a/src/modules/Elsa.Retention/Jobs/CleanupJob.cs +++ b/src/modules/Elsa.Retention/Jobs/CleanupJob.cs @@ -4,7 +4,6 @@ using Elsa.Retention.Contracts; using Elsa.Retention.Options; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Entities; -using Elsa.Workflows.Management.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -31,7 +30,6 @@ public class CleanupJob( /// public async Task ExecuteAsync(CancellationToken cancellationToken = default) { - Console.WriteLine(DateTime.Now.ToLongTimeString()); var collectors = GetServices(typeof(IRelatedEntityCollector), typeof(IRelatedEntityCollector<>)); var deletedWorkflowInstances = 0L; From 7a08dd70cbfbbf3079943770a34edb6a1ee7f099 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 00:01:26 +0100 Subject: [PATCH 056/166] Refactor scheduling service registrations. Removed unused and duplicate service registrations to simplify DI setup. Ensures cleaner and more maintainable code while preventing potential initialization conflicts. --- src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs index 9cbd1a11c..7ef3dfdec 100644 --- a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs +++ b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs @@ -45,9 +45,7 @@ public class SchedulingFeature : FeatureBase .AddSingleton(CronParser) .AddScoped() .AddScoped() - .AddSingleton() .AddScoped() - .AddSingleton() .AddSingleton(CronParser) .AddScoped(WorkflowScheduler) .AddBackgroundTask() From aaab19509ebe43d75c8e04e7606c6823df56f7f7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 10:50:17 +0100 Subject: [PATCH 057/166] Fix array type handling in ArgumentJsonConverter Previously, the code incorrectly used `MakeCollectionType` for arrays. This has been corrected to use `MakeArrayType`, ensuring proper deserialization of array arguments. --- .../Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs index 0900930e8..81f5a0d42 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs @@ -44,7 +44,7 @@ public class ArgumentJsonConverter : JsonConverter var type = _wellKnownTypeRegistry.GetTypeOrDefault(typeName); if (isArray) - type = type.MakeCollectionType(); + type = type.MakeArrayType(); var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); From 2c3843d5f5f3ed536299b085c50787a5473603b5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 10:50:34 +0100 Subject: [PATCH 058/166] Refactor variable mapping and improve type alias handling Updated `VariableMapper` for null assignment consistency and streamlined `VariableModel` instantiation. Improved `TypeJsonConverter` to handle list type aliases more explicitly, replacing ambiguous syntax with clearer format. --- .../Serialization/Converters/TypeJsonConverter.cs | 6 +++--- src/modules/Elsa.Workflows.Core/Services/VariableMapper.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs index 816b5fb5b..5fe74064e 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/TypeJsonConverter.cs @@ -40,9 +40,9 @@ public class TypeJsonConverter : JsonConverter } // Handle collection types. - if (typeAlias.EndsWith("()")) + if (typeAlias.StartsWith("List<") && typeAlias.EndsWith(">")) { - var elementTypeAlias = typeAlias[..^"()".Length]; + var elementTypeAlias = typeAlias[5..^1]; var elementType = _wellKnownTypeRegistry.TryGetType(elementTypeAlias, out var t) ? t : Type.GetType(elementTypeAlias)!; return typeof(List<>).MakeGenericType(elementType); } @@ -70,7 +70,7 @@ public class TypeJsonConverter : JsonConverter if (typedEnumerable.IsAssignableFrom(value) && _wellKnownTypeRegistry.TryGetAlias(elementType, out var elementTypeAlias)) { - writer.WriteStringValue($"{elementTypeAlias}()"); + writer.WriteStringValue($"List<{elementTypeAlias}>"); return; } } diff --git a/src/modules/Elsa.Workflows.Core/Services/VariableMapper.cs b/src/modules/Elsa.Workflows.Core/Services/VariableMapper.cs index 2524822c9..37899fc1b 100644 --- a/src/modules/Elsa.Workflows.Core/Services/VariableMapper.cs +++ b/src/modules/Elsa.Workflows.Core/Services/VariableMapper.cs @@ -59,7 +59,7 @@ public class VariableMapper .OnSuccess(value => variable.Value = value) .OnFailure(e => _logger.LogWarning("Failed to convert {SourceValue} to {TargetType}", source.Value, type.Name)); - variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : default; + variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null; return variable; } @@ -76,6 +76,6 @@ public class VariableMapper var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); var serializedValue = value.Format(); - return new VariableModel(source.Id, source.Name, valueTypeAlias, serializedValue, storageDriverTypeName); + return new(source.Id, source.Name, valueTypeAlias, serializedValue, storageDriverTypeName); } } \ No newline at end of file From 18765cc386cd3b178548d80d74e134de23dffb46 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 11:17:21 +0100 Subject: [PATCH 059/166] Refactor route normalization logic Simplified route normalization logic by removing unnecessary lowercase conversion and adjusted usages accordingly. These changes improve code readability, maintain consistency and fixes that route values are stored lower-cased instead of original-cased. --- .../Elsa.Http/Activities/HttpEndpoint.cs | 30 +++++++++---------- .../Elsa.Http/Extensions/RouteExtensions.cs | 4 +-- .../Middleware/HttpWorkflowsMiddleware.cs | 6 ++-- .../Elsa.Http/Services/RouteMatcher.cs | 6 ++-- .../WorkflowDefinitions/Post/Endpoint.cs | 2 +- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index dc148b15f..428d73adc 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -23,10 +23,10 @@ namespace Elsa.Http; public class HttpEndpoint : Trigger { internal const string HttpContextInputKey = "HttpContext"; - internal const string RequestPathInputKey = "RequestPath"; + internal const string PathInputKey = "Path"; /// - public HttpEndpoint([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public HttpEndpoint([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } @@ -38,7 +38,7 @@ public class HttpEndpoint : Trigger UIHint = InputUIHints.SingleLine, UIHandler = typeof(HttpEndpointPathUIHandler) )] - public Input Path { get; set; } = default!; + public Input Path { get; set; } = null!; /// /// The HTTP methods to accept. @@ -65,37 +65,37 @@ public class HttpEndpoint : Trigger /// The maximum time allowed to process the request. /// [Input(Description = "The maximum time allowed to process the request.", Category = "Upload")] - public Input RequestTimeout { get; set; } = default!; + public Input RequestTimeout { get; set; } = null!; /// /// The maximum request size allowed in bytes. /// [Input(Description = "The maximum request size allowed in bytes.", Category = "Upload")] - public Input RequestSizeLimit { get; set; } = default!; + public Input RequestSizeLimit { get; set; } = null!; /// /// The maximum request size allowed in bytes. /// [Input(Description = "The maximum file size allowed in bytes for an individual file.", Category = "Upload")] - public Input FileSizeLimit { get; set; } = default!; + public Input FileSizeLimit { get; set; } = null!; /// /// The allowed file extensions, /// [Input(Description = "Only file extensions in this list are allowed. Leave empty to allow all extensions", Category = "Upload", UIHint = InputUIHints.MultiText)] - public Input> AllowedFileExtensions { get; set; } = default!; + public Input> AllowedFileExtensions { get; set; } = null!; /// /// The allowed file extensions, /// [Input(Description = "File extensions in this list are forbidden. Leave empty to not block any extension.", Category = "Upload", UIHint = InputUIHints.MultiText)] - public Input> BlockedFileExtensions { get; set; } = default!; + public Input> BlockedFileExtensions { get; set; } = null!; /// /// The allowed file extensions, /// [Input(Description = "Only MIME types in this list are allowed. Leave empty to allow all types", Category = "Upload", UIHint = InputUIHints.MultiText)] - public Input> AllowedMimeTypes { get; set; } = default!; + public Input> AllowedMimeTypes { get; set; } = null!; /// /// A value indicating whether to expose the "Request too large" outcome. @@ -125,31 +125,31 @@ public class HttpEndpoint : Trigger /// The parsed request content, if any. /// [Output(Description = "The parsed request content, if any.")] - public Output ParsedContent { get; set; } = default!; + public Output ParsedContent { get; set; } = null!; /// /// The uploaded files, if any. /// [Output(Description = "The uploaded files, if any.", IsSerializable = false)] - public Output Files { get; set; } = default!; + public Output Files { get; set; } = null!; /// /// The parsed route data, if any. /// [Output(Description = "The parsed route data, if any.")] - public Output> RouteData { get; set; } = default!; + public Output> RouteData { get; set; } = null!; /// /// The querystring data, if any. /// [Output(Description = "The querystring data, if any.")] - public Output> QueryStringData { get; set; } = default!; + public Output> QueryStringData { get; set; } = null!; /// /// The headers, if any. /// [Output(Description = "The headers, if any.")] - public Output> Headers { get; set; } = default!; + public Output> Headers { get; set; } = null!; /// protected override IEnumerable GetTriggerPayloads(TriggerIndexingContext context) => GetBookmarkPayloads(context.ExpressionExecutionContext); @@ -205,7 +205,7 @@ public class HttpEndpoint : Trigger context.Set(Result, request); // Read route data, if any. - var path = context.GetWorkflowInput(RequestPathInputKey); + var path = context.GetWorkflowInput(PathInputKey); var routeData = GetRouteData(httpContext, path); var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!); var queryStringDictionary = httpContext.Request.Query.ToObjectDictionary(); diff --git a/src/modules/Elsa.Http/Extensions/RouteExtensions.cs b/src/modules/Elsa.Http/Extensions/RouteExtensions.cs index a59e69013..79a9d0220 100644 --- a/src/modules/Elsa.Http/Extensions/RouteExtensions.cs +++ b/src/modules/Elsa.Http/Extensions/RouteExtensions.cs @@ -8,7 +8,7 @@ namespace Elsa.Extensions; public static class RouteExtensions { /// - /// Normalizes a route by ensuring a leading slash, removing any trailing slash and converting the path to lowercase. + /// Normalizes a route by ensuring a leading slash, removing any trailing slash. /// - public static string NormalizeRoute(this string path) => $"/{path.Trim('/').ToLowerInvariant()}"; + public static string NormalizeRoute(this string path) => $"/{path.Trim('/')}"; } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs index bc0d81194..87cb34100 100644 --- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs @@ -39,7 +39,7 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, ITenantAccessor tenan [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceProvider) { - var path = GetPath(httpContext); + var path = httpContext.Request.Path.Value!.NormalizeRoute(); var matchingPath = GetMatchingRoute(serviceProvider, path).Route; var basePath = options.Value.BasePath?.ToString().NormalizeRoute(); @@ -61,7 +61,7 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, ITenantAccessor tenan var input = new Dictionary { [HttpEndpoint.HttpContextInputKey] = true, - [HttpEndpoint.RequestPathInputKey] = path.NormalizeRoute() + [HttpEndpoint.PathInputKey] = path }; var cancellationToken = httpContext.RequestAborted; @@ -325,8 +325,6 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, ITenantAccessor tenan } } - private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value!.NormalizeRoute(); - [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")] private async Task HandleMultipleWorkflowsFoundAsync(HttpContext httpContext, Func> workflowMatches, CancellationToken cancellationToken) { diff --git a/src/modules/Elsa.Http/Services/RouteMatcher.cs b/src/modules/Elsa.Http/Services/RouteMatcher.cs index b72e59d1b..4d9de3ae7 100644 --- a/src/modules/Elsa.Http/Services/RouteMatcher.cs +++ b/src/modules/Elsa.Http/Services/RouteMatcher.cs @@ -12,13 +12,11 @@ public class RouteMatcher : IRouteMatcher /// public RouteValueDictionary? Match(string routeTemplate, string route) { - var normalizedRoute = route.NormalizeRoute(); - var normalizedRouteTemplate = routeTemplate.NormalizeRoute(); - var template = TemplateParser.Parse(normalizedRouteTemplate); + var template = TemplateParser.Parse(routeTemplate); var matcher = new TemplateMatcher(template, GetDefaults(template)); var values = new RouteValueDictionary(); - return matcher.TryMatch(normalizedRoute, values) ? values : null; + return matcher.TryMatch(route, values) ? values : null; } private static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index b8ff04d48..4b1ba32f4 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -43,7 +43,7 @@ internal class Post( var draft = !string.IsNullOrWhiteSpace(definitionId) ? await workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken) - : default; + : null; var isNew = draft == null; From eba7eb289da043d1eefd6bcd6a209de85722ee3f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 11:25:43 +0100 Subject: [PATCH 060/166] Update README.md --- README.md | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ca3e3efae..eefa67ecd 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,13 @@ By default, you can access http://localhost:13000 and log in with: ## Documentation -For comprehensive documentation and to get started with Elsa, please visit the [Elsa Documentation Website](https://v3.elsaworkflows.io/). +[Elsa Documentation Website](https://docs.elsaworkflows.io/). ## Known Issues and Limitations Elsa is continually evolving, and while it offers powerful capabilities, there are some known limitations and ongoing work: - Documentation is still a work in progress. -- The designer is not yet fully embeddable in other applications; this feature is planned for a future release. -- C# and Python expressions are not yet fully tested. -- Bulk Dispatch Workflows is a new activity and not yet fully tested. - Input/Output is not yet implemented in the Workflow Instance Viewer. - Starting workflows from the designer is currently supported only for workflows that do not require input and do not start with a trigger; this is planned for a future release. - The designer currently only supports Flowchart activities. Support for Sequence and StateMachine activities is planned for a future release. @@ -90,15 +87,7 @@ Elsa offers a wide range of features for building and executing workflows, inclu ## Roadmap -The following features are planned for future releases of Elsa: - -- [ ] Multi-tenancy -- [ ] State Machine activity -- [ ] Designer support for Sequence activity & StateMachine activity -- [ ] BPMN 2.0 support -- [ ] DMN support -- [ ] Workflow migration to new versions via UI -- [ ] Capsules ("hot" deployable workflow packages containing activities and configuration) +See #3232 ## Use Cases @@ -109,7 +98,7 @@ Elsa can be used in a variety of scenarios, including: - Scheduled workflows such as sending daily reports. - Event-driven workflows such as sending welcome emails when a user signs up. -## Programmatic Workflows +## Coding Workflows Elsa allows you to define workflows in code using C#. The following example shows how to receive HTTP requests and send an email in response: @@ -141,13 +130,12 @@ public class SendEmailWorkflow : WorkflowBase } ``` -## Designed Workflows +## Designing Workflows Elsa allows you to define workflows using a visual designer. The following example shows how to receive HTTP requests and send an email in response: ![Elsa ships with a powerful visual designer](./design/screenshots/http-send-email-workflow-designer.png) - ## Contributing We welcome contributions from the community and are pleased that you are interested in helping to improve the Elsa Workflow project! Here are the steps to contribute to our project: From 96dd00838ac7babf2fe744aca3cc515d5ff67768 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 14:27:37 +0100 Subject: [PATCH 061/166] Refine message handling in MessageReceived activity. Ensure type validation for incoming messages and clean up workflow input to prevent unintended data propagation. Added a dedicated ResumeAsync method to handle bookmark resumption logic consistently. Fixes #6294 --- .../Activities/MessageReceived.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs index d0a32890a..905bc9d53 100644 --- a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs +++ b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs @@ -32,20 +32,32 @@ public class MessageReceived : Trigger protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark. - if (!context.TryGetWorkflowInput(InputKey, out var message)) + if (!context.TryGetWorkflowInput(InputKey, out var message) || message.GetType() != MessageType) { // Create bookmarks for when we receive the expected HTTP request. - context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext)); + context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext), ResumeAsync, includeActivityInstanceId: false); return; } // Provide the received message as output. context.Set(Result, message); + // Remove the input to prevent it from being passed to the next activity. + context.WorkflowInput.Remove(InputKey); + // Complete. await context.CompleteActivityAsync(); } + private ValueTask ResumeAsync(ActivityExecutionContext context) + { + // Remove the input to prevent it from being passed to the next activity. + context.WorkflowInput.Remove(InputKey); + + // Complete. + return context.CompleteActivityAsync(); + } + private object GetBookmarkPayload(ExpressionExecutionContext context) { // Generate bookmark data for message type. From 1ee8fbdb5a2f21fd40693a1cf902daf2f49cc498 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 14:27:53 +0100 Subject: [PATCH 062/166] Refactor default parameter values to null for readability Replaced `default` with `null` for optional parameters to improve code clarity and maintain consistency. This change ensures better readability and aligns with common coding practices, especially when null is the intended default value. --- .../Contexts/ActivityExecutionContext.cs | 40 +++++++++---------- .../ActivityExecutionContextExtensions.cs | 16 ++++---- .../WorkflowExecutionContextExtensions.cs | 2 +- .../Elsa.Workflows.Core/Models/Bookmark.cs | 6 +-- .../Services/StimulusSender.cs | 4 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index f545b0881..5393a9f80 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -198,7 +198,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// An optional callback to invoke when the activity completes. /// An optional tag to associate with the activity execution. /// An optional list of variables to declare with the activity execution. - public ValueTask ScheduleActivityAsync(IActivity? activity, ActivityCompletionCallback? completionCallback, object? tag = default, IEnumerable? variables = default) + public ValueTask ScheduleActivityAsync(IActivity? activity, ActivityCompletionCallback? completionCallback, object? tag = null, IEnumerable? variables = null) { var options = new ScheduleWorkOptions { @@ -214,7 +214,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// The activity to schedule. /// The options used to schedule the activity. - public async ValueTask ScheduleActivityAsync(IActivity? activity, ScheduleWorkOptions? options = default) + public async ValueTask ScheduleActivityAsync(IActivity? activity, ScheduleWorkOptions? options = null) { await ScheduleActivityAsync(activity, this, options); } @@ -225,7 +225,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The activity to schedule. /// The activity execution context that owns the scheduled activity. /// The options used to schedule the activity. - public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityExecutionContext? owner, ScheduleWorkOptions? options = default) + public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityExecutionContext? owner, ScheduleWorkOptions? options = null) { var activityNode = activity != null ? WorkflowExecutionContext.FindNodeByActivity(activity) ?? throw new InvalidOperationException("The specified activity is not part of the workflow.") @@ -239,7 +239,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The activity node to schedule. /// The activity execution context that owns the scheduled activity. /// The options used to schedule the activity. - public async ValueTask ScheduleActivityAsync(ActivityNode? activityNode, ActivityExecutionContext? owner = default, ScheduleWorkOptions? options = default) + public async ValueTask ScheduleActivityAsync(ActivityNode? activityNode, ActivityExecutionContext? owner = null, ScheduleWorkOptions? options = null) { if (this.GetIsBackgroundExecution()) { @@ -261,7 +261,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable Variables = options?.Variables?.ToList(), Input = options?.Input } - : default + : null }; var scheduledActivities = this.GetBackgroundScheduledActivities().ToList(); @@ -303,7 +303,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The callback to invoke when the activities complete. /// An optional tag to associate with the activity execution. /// An optional list of variables to declare with the activity execution. - public ValueTask ScheduleActivities(IEnumerable activities, ActivityCompletionCallback? completionCallback, object? tag = default, IEnumerable? variables = default) + public ValueTask ScheduleActivities(IEnumerable activities, ActivityCompletionCallback? completionCallback, object? tag = null, IEnumerable? variables = null) { var options = new ScheduleWorkOptions { @@ -319,7 +319,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// The activities to schedule. /// The options used to schedule the activities. - public async ValueTask ScheduleActivities(IEnumerable activities, ScheduleWorkOptions? options = default) + public async ValueTask ScheduleActivities(IEnumerable activities, ScheduleWorkOptions? options = null) { foreach (var activity in activities) await ScheduleActivityAsync(activity, options); @@ -331,7 +331,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The payloads to create bookmarks for. /// An optional callback that is invoked when the bookmark is resumed. /// Whether or not the activity instance ID should be included in the bookmark payload. - public void CreateBookmarks(IEnumerable payloads, ExecuteActivityDelegate? callback = default, bool includeActivityInstanceId = true) + public void CreateBookmarks(IEnumerable payloads, ExecuteActivityDelegate? callback = null, bool includeActivityInstanceId = true) { foreach (var payload in payloads) CreateBookmark(new CreateBookmarkArgs @@ -360,7 +360,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// An optional callback that is invoked when the bookmark is resumed. /// Custom properties to associate with the bookmark. /// The created bookmark. - public Bookmark CreateBookmark(ExecuteActivityDelegate callback, IDictionary? metadata = default) + public Bookmark CreateBookmark(ExecuteActivityDelegate callback, IDictionary? metadata = null) { return CreateBookmark(new CreateBookmarkArgs { @@ -377,7 +377,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// Whether or not the activity instance ID should be included in the bookmark payload. /// Custom properties to associate with the bookmark. /// The created bookmark. - public Bookmark CreateBookmark(object stimulus, ExecuteActivityDelegate callback, bool includeActivityInstanceId = true, IDictionary? customProperties = default) + public Bookmark CreateBookmark(object stimulus, ExecuteActivityDelegate callback, bool includeActivityInstanceId = true, IDictionary? customProperties = null) { return CreateBookmark(new CreateBookmarkArgs { @@ -395,7 +395,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// Specifies whether to include the activity instance ID in the bookmark information. Defaults to true. /// Additional custom properties to associate with the bookmark. Defaults to null. /// The created bookmark. - public Bookmark CreateBookmark(object stimulus, bool includeActivityInstanceId, IDictionary? customProperties = default) + public Bookmark CreateBookmark(object stimulus, bool includeActivityInstanceId, IDictionary? customProperties = null) { return CreateBookmark(new CreateBookmarkArgs { @@ -411,9 +411,9 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The payload to associate with the bookmark. /// Custom properties to associate with the bookmark. /// The created bookmark. - public Bookmark CreateBookmark(object stimulus, IDictionary? metadata = default) + public Bookmark CreateBookmark(object stimulus, IDictionary? metadata = null) { - return CreateBookmark(new CreateBookmarkArgs + return CreateBookmark(new() { Stimulus = stimulus, Metadata = metadata @@ -424,7 +424,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// Creates a bookmark so that this activity can be resumed at a later time. /// Creating a bookmark will automatically suspend the workflow after all pending activities have executed. /// - public Bookmark CreateBookmark(CreateBookmarkArgs? options = default) + public Bookmark CreateBookmark(CreateBookmarkArgs? options = null) { var payload = options?.Stimulus; var callback = options?.Callback; @@ -569,7 +569,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// The output. /// The output value. - public object? Get(Output? output) => output == null ? default : Get(output.MemoryBlockReference()); + public object? Get(Output? output) => output == null ? null : Get(output.MemoryBlockReference()); /// /// Gets the value of the specified memory block. @@ -593,7 +593,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable public T? Get(MemoryBlockReference blockReference) { var value = Get(blockReference); - return value != default ? value.ConvertTo() : default; + return value != null ? value.ConvertTo() : default; } /// @@ -628,7 +628,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The memory block reference. /// The value to set. /// An optional callback that can be used to configure the memory block. - public void Set(MemoryBlockReference blockReference, object? value, Action? configure = default) => ExpressionExecutionContext.Set(blockReference, value, configure); + public void Set(MemoryBlockReference blockReference, object? value, Action? configure = null) => ExpressionExecutionContext.Set(blockReference, value, configure); /// /// Sets a value at the specified output. @@ -637,7 +637,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The value to set. /// The name of the output. /// The type of the output. - public void Set(Output? output, T? value, [CallerArgumentExpression("output")] string? outputName = default) => Set((Output?)output, value, outputName); + public void Set(Output? output, T? value, [CallerArgumentExpression("output")] string? outputName = null) => Set((Output?)output, value, outputName); /// /// Sets a value at the specified output. @@ -645,7 +645,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// The output. /// The value to set. /// The name of the output. - public void Set(Output? output, object? value, [CallerArgumentExpression("output")] string? outputName = default) + public void Set(Output? output, object? value, [CallerArgumentExpression("output")] string? outputName = null) { // Store the value in the expression execution memory block. ExpressionExecutionContext.Set(output, value); @@ -667,7 +667,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable private MemoryBlock? GetMemoryBlock(MemoryBlockReference locationBlockReference) { - return ExpressionExecutionContext.TryGetBlock(locationBlockReference, out var memoryBlock) ? memoryBlock : default; + return ExpressionExecutionContext.TryGetBlock(locationBlockReference, out var memoryBlock) ? memoryBlock : null; } void IDisposable.Dispose() diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index a0fc4e7ad..090cff8b8 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -25,13 +25,13 @@ public static partial class ActivityExecutionContextExtensions /// /// Attempts to get a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static bool TryGetWorkflowInput(this ActivityExecutionContext context, string key, out T value, JsonSerializerOptions? serializerOptions = default) + public static bool TryGetWorkflowInput(this ActivityExecutionContext context, string key, out T value, JsonSerializerOptions? serializerOptions = null) { var wellKnownTypeRegistry = context.GetRequiredService(); if (context.WorkflowInput.TryGetValue(key, out var v)) { - value = v.ConvertTo(new ObjectConverterOptions(serializerOptions, wellKnownTypeRegistry))!; + value = v.ConvertTo(new(serializerOptions, wellKnownTypeRegistry))!; return true; } @@ -42,15 +42,15 @@ public static partial class ActivityExecutionContextExtensions /// /// Gets a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static T GetWorkflowInput(this ActivityExecutionContext context, JsonSerializerOptions? serializerOptions = default) => context.GetWorkflowInput(typeof(T).Name, serializerOptions); + public static T GetWorkflowInput(this ActivityExecutionContext context, JsonSerializerOptions? serializerOptions = null) => context.GetWorkflowInput(typeof(T).Name, serializerOptions); /// /// Gets a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static T GetWorkflowInput(this ActivityExecutionContext context, string key, JsonSerializerOptions? serializerOptions = default) + public static T GetWorkflowInput(this ActivityExecutionContext context, string key, JsonSerializerOptions? serializerOptions = null) { var wellKnownTypeRegistry = context.GetRequiredService(); - return context.WorkflowInput[key].ConvertTo(new ObjectConverterOptions(serializerOptions, wellKnownTypeRegistry))!; + return context.WorkflowInput[key].ConvertTo(new(serializerOptions, wellKnownTypeRegistry))!; } /// @@ -61,7 +61,7 @@ public static partial class ActivityExecutionContextExtensions /// Thrown when the specified activity does not implement . public static void SetResult(this ActivityExecutionContext context, object? value) { - var activity = context.Activity as IActivityWithResult ?? throw new Exception($"Cannot set result on activity {context.Activity.Id} because it does not implement {nameof(IActivityWithResult)}."); + var activity = context.Activity as IActivityWithResult ?? throw new($"Cannot set result on activity {context.Activity.Id} because it does not implement {nameof(IActivityWithResult)}."); context.Set(activity.Result, value, "Result"); } @@ -79,7 +79,7 @@ public static partial class ActivityExecutionContextExtensions /// The type of storage driver to use for the variable. /// A callback to configure the memory block. /// The created . - public static Variable CreateVariable(this ActivityExecutionContext context, string name, object? value, Type? storageDriverType = default, Action? configure = default) => + public static Variable CreateVariable(this ActivityExecutionContext context, string name, object? value, Type? storageDriverType = null, Action? configure = null) => context.ExpressionExecutionContext.CreateVariable(name, value, storageDriverType, configure); /// @@ -90,7 +90,7 @@ public static partial class ActivityExecutionContextExtensions /// The value of the variable. /// A callback to configure the memory block. /// The created . - public static Variable SetVariable(this ActivityExecutionContext context, string name, object? value, Action? configure = default) => + public static Variable SetVariable(this ActivityExecutionContext context, string name, object? value, Action? configure = null) => context.ExpressionExecutionContext.SetVariable(name, value, configure); /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 044abe9b2..6745c6004 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -88,7 +88,7 @@ public static class WorkflowExecutionContextExtensions : WorkflowExecutionContext.Noop; // Store the bookmark to resume in the context. - workflowExecutionContext.ResumedBookmarkContext = new ResumedBookmarkContext(bookmark); + workflowExecutionContext.ResumedBookmarkContext = new(bookmark); logger.LogDebug("Scheduled activity {ActivityId} to resume from bookmark {BookmarkId}", bookmarkedActivity.Id, bookmark.Id); return workItem; diff --git a/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs b/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs index 9d062b336..e2ba868f8 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs @@ -26,13 +26,13 @@ public record Bookmark( string? ActivityInstanceId, DateTimeOffset CreatedAt, bool AutoBurn = true, - string? CallbackMethodName = default, + string? CallbackMethodName = null, bool AutoComplete = true, - IDictionary? Metadata = default) + IDictionary? Metadata = null) { /// [JsonConstructor] - public Bookmark() : this("", "", "", null, "", "", "", default, default) + public Bookmark() : this("", "", "", null, "", "", "", default, false) { } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs b/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs index 7ded4d3a3..039bb6b05 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs @@ -38,7 +38,7 @@ public class StimulusSender( var resumed = await ResumeExistingWorkflowsAsync(stimulusHash, metadata, cancellationToken); responses.AddRange(resumed); - return new SendStimulusResult(responses); + return new(responses); } private async Task> TriggerNewWorkflowsAsync(string stimulusHash, StimulusMetadata? metadata = null, CancellationToken cancellationToken = default) @@ -129,7 +129,7 @@ public class StimulusSender( WorkflowInstanceId = workflowInstanceId, BookmarkId = metadata?.BookmarkId, StimulusHash = stimulusHash, - Options = new ResumeBookmarkOptions + Options = new() { Input = input, Properties = properties From 0b5f580b0daf8aae4bc3aa113cd3af72af7a6404 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 15 Jan 2025 18:40:24 +0100 Subject: [PATCH 063/166] Reorder tenant services initialization for proper execution. Ensure `RunStartupTasks` is added before background and recurring tasks as order is critical for correct startup and tenant activation behavior. This prevents potential misalignment in task execution during tenant lifecycle events. Fixes #6269 --- src/modules/Elsa.Common/Features/MultitenancyFeature.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Common/Features/MultitenancyFeature.cs b/src/modules/Elsa.Common/Features/MultitenancyFeature.cs index 89bbb7d66..640e6183e 100644 --- a/src/modules/Elsa.Common/Features/MultitenancyFeature.cs +++ b/src/modules/Elsa.Common/Features/MultitenancyFeature.cs @@ -40,6 +40,9 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module) .AddSingleton() .AddSingleton() + // Order is important: Startup task first, then background and recurring tasks. + .AddSingleton() + .AddSingleton() .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton(sp => sp.GetRequiredService()) @@ -48,7 +51,6 @@ public class MultitenancyFeature(IModule module) : FeatureBase(module) .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton(sp => sp.GetRequiredService()) - .AddSingleton() .AddSingleton() .AddSingleton() .AddScoped() From 9043af1726c1fd86964600ba6f9c77de0dbed7c1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 16 Jan 2025 20:41:12 +0100 Subject: [PATCH 064/166] Regenerate 3_3 Runtime module migrations --- .../Elsa.Dapper.Migrations/Runtime/V3_3.cs | 2 + ...ner.cs => 20250116192733_V3_3.Designer.cs} | 8 +-- ...1124504_V3_3.cs => 20250116192733_V3_3.cs} | 66 +++---------------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- ...ner.cs => 20250116193207_V3_3.Designer.cs} | 8 +-- ...1124811_V3_3.cs => 20250116193207_V3_3.cs} | 1 - .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- ...ner.cs => 20250116192950_V3_3.Designer.cs} | 8 +-- ...1124728_V3_3.cs => 20250116192950_V3_3.cs} | 43 +++--------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- ...ner.cs => 20250116192837_V3_3.Designer.cs} | 8 +-- ...1124643_V3_3.cs => 20250116192837_V3_3.cs} | 61 +++-------------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- ...ner.cs => 20250116192907_V3_3.Designer.cs} | 8 +-- ...1122946_V3_3.cs => 20250116192907_V3_3.cs} | 37 +++-------- .../RuntimeElsaDbContextModelSnapshot.cs | 6 +- .../Modules/Runtime/Configurations.cs | 1 + 17 files changed, 57 insertions(+), 224 deletions(-) rename src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/{20241231124504_V3_3.Designer.cs => 20250116192733_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/{20241231124504_V3_3.cs => 20250116192733_V3_3.cs} (84%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20241231124811_V3_3.Designer.cs => 20250116193207_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20241231124811_V3_3.cs => 20250116193207_V3_3.cs} (99%) rename src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/{20241231124728_V3_3.Designer.cs => 20250116192950_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/{20241231124728_V3_3.cs => 20250116192950_V3_3.cs} (90%) rename src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/{20241231124643_V3_3.Designer.cs => 20250116192837_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/{20241231124643_V3_3.cs => 20250116192837_V3_3.cs} (85%) rename src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/{20241231122946_V3_3.Designer.cs => 20250116192907_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/{20241231122946_V3_3.cs => 20250116192907_V3_3.cs} (90%) diff --git a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs index 2cbb9a16f..653346ed5 100644 --- a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs +++ b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs @@ -20,6 +20,7 @@ public class V3_3 : Migration Alter.Table("WorkflowExecutionLogRecords").AddColumn("TenantId").AsString().Nullable(); Alter.Table("ActivityExecutionRecords").AddColumn("TenantId").AsString().Nullable(); Alter.Table("KeyValuePairs").AddColumn("TenantId").AsString().Nullable(); + Rename.Column("Key").OnTable("KeyValuePairs").To("Id"); IfDatabase("SqlServer", "Oracle", "MySql", "Postgres") .Create @@ -56,6 +57,7 @@ public class V3_3 : Migration Delete.Column("TenantId").FromTable("WorkflowExecutionLogRecords"); Delete.Column("TenantId").FromTable("ActivityExecutionRecords"); Delete.Column("TenantId").FromTable("KeyValuePairs"); + Rename.Column("Id").OnTable("KeyValuePairs").To("Key"); IfDatabase("SqlServer", "Oracle", "MySql", "Postgres") .Create diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.Designer.cs index 66b24b533..cd2cae34e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241231124504_V3_3")] + [Migration("20250116192733_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -31,10 +31,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime b.Property("Id") .HasColumnType("varchar(255)"); - b.Property("Key") - .IsRequired() - .HasColumnType("longtext"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("longtext"); diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.cs similarity index 84% rename from src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.cs index ab100e254..d1dc13b38 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20241231124504_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/20250116192733_V3_3.cs @@ -19,10 +19,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", + migrationBuilder.RenameColumn( + name: "Key", schema: _schema.Schema, - table: "KeyValuePairs"); + table: "KeyValuePairs", + newName: "Id"); migrationBuilder.RenameColumn( name: "BookmarkId", @@ -54,26 +55,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime nullable: true) .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AlterColumn( - name: "Key", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "longtext", - nullable: false, - oldClrType: typeof(string), - oldType: "varchar(255)") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "varchar(255)", - nullable: false, - defaultValue: "") - .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -98,12 +79,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime nullable: true) .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -231,11 +206,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropIndex( name: "IX_SerializedKeyValuePair_TenantId", schema: _schema.Schema, @@ -266,11 +236,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -286,28 +251,17 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "KeyValuePairs", + newName: "Key"); + migrationBuilder.RenameColumn( name: "Id", schema: _schema.Schema, table: "Bookmarks", newName: "BookmarkId"); - - migrationBuilder.AlterColumn( - name: "Key", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "varchar(255)", - nullable: false, - oldClrType: typeof(string), - oldType: "longtext") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Key"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 5a4d7a6af..e384a181b 100644 --- a/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -28,10 +28,6 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime b.Property("Id") .HasColumnType("varchar(255)"); - b.Property("Key") - .IsRequired() - .HasColumnType("longtext"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("longtext"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs index f554d55de..3abc3e068 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241231124811_V3_3")] + [Migration("20250116193207_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -31,10 +31,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime b.Property("Id") .HasColumnType("NVARCHAR2(450)"); - b.Property("Key") - .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs similarity index 99% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs index 435afcca0..a026586c5 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20241231124811_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs @@ -99,7 +99,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime columns: table => new { Id = table.Column(type: "NVARCHAR2(450)", nullable: false), - Key = table.Column(type: "NVARCHAR2(2000)", nullable: false), SerializedValue = table.Column(type: "NVARCHAR2(2000)", nullable: false), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 41ef0f940..5e1520286 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -28,10 +28,6 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime b.Property("Id") .HasColumnType("NVARCHAR2(450)"); - b.Property("Key") - .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.Designer.cs index c86657426..05fe77430 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241231124728_V3_3")] + [Migration("20250116192950_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -31,10 +31,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime b.Property("Id") .HasColumnType("text"); - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("text"); diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.cs similarity index 90% rename from src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.cs index 9f8654a83..f9b793865 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20241231124728_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/20250116192950_V3_3.cs @@ -19,10 +19,11 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", + migrationBuilder.RenameColumn( + name: "Key", schema: _schema.Schema, - table: "KeyValuePairs"); + table: "KeyValuePairs", + newName: "Id"); migrationBuilder.RenameColumn( name: "BookmarkId", @@ -51,14 +52,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime type: "text", nullable: true); - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "text", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -80,12 +73,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime type: "text", nullable: true); - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -203,11 +190,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropIndex( name: "IX_SerializedKeyValuePair_TenantId", schema: _schema.Schema, @@ -238,11 +220,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -258,17 +235,17 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "KeyValuePairs", + newName: "Key"); + migrationBuilder.RenameColumn( name: "Id", schema: _schema.Schema, table: "Bookmarks", newName: "BookmarkId"); - - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Key"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 643dc2474..01066d7e1 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -28,10 +28,6 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime b.Property("Id") .HasColumnType("text"); - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("text"); diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.Designer.cs index fdc1b9e23..6f59df17b 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241231124643_V3_3")] + [Migration("20250116192837_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -31,10 +31,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime b.Property("Id") .HasColumnType("nvarchar(450)"); - b.Property("Key") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.cs similarity index 85% rename from src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.cs index c8a162b3e..ea7495e61 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20241231124643_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/20250116192837_V3_3.cs @@ -19,10 +19,11 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", + migrationBuilder.RenameColumn( + name: "Key", schema: _schema.Schema, - table: "KeyValuePairs"); + table: "KeyValuePairs", + newName: "Id"); migrationBuilder.RenameColumn( name: "BookmarkId", @@ -51,23 +52,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime type: "nvarchar(450)", nullable: true); - migrationBuilder.AlterColumn( - name: "Key", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(450)"); - - migrationBuilder.AddColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "nvarchar(450)", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", schema: _schema.Schema, @@ -89,12 +73,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime type: "nvarchar(450)", nullable: true); - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", schema: _schema.Schema, @@ -212,11 +190,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropIndex( name: "IX_SerializedKeyValuePair_TenantId", schema: _schema.Schema, @@ -247,11 +220,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "Triggers"); - migrationBuilder.DropColumn( - name: "Id", - schema: _schema.Schema, - table: "KeyValuePairs"); - migrationBuilder.DropColumn( name: "TenantId", schema: _schema.Schema, @@ -267,26 +235,17 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime schema: _schema.Schema, table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + schema: _schema.Schema, + table: "KeyValuePairs", + newName: "Key"); + migrationBuilder.RenameColumn( name: "Id", schema: _schema.Schema, table: "Bookmarks", newName: "BookmarkId"); - - migrationBuilder.AlterColumn( - name: "Key", - schema: _schema.Schema, - table: "KeyValuePairs", - type: "nvarchar(450)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - schema: _schema.Schema, - table: "KeyValuePairs", - column: "Key"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 663fb6f45..2d936a1c8 100644 --- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -28,10 +28,6 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime b.Property("Id") .HasColumnType("nvarchar(450)"); - b.Property("Key") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.Designer.cs index 904170343..d92a8513f 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.Designer.cs @@ -11,24 +11,20 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20241231122946_V3_3")] + [Migration("20250116192907_V3_3")] partial class V3_3 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { b.Property("Id") .HasColumnType("TEXT"); - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("TEXT"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.cs similarity index 90% rename from src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.cs index 4490f83bd..4316a2459 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20241231122946_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/20250116192907_V3_3.cs @@ -19,9 +19,10 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", - table: "KeyValuePairs"); + migrationBuilder.RenameColumn( + name: "Key", + table: "KeyValuePairs", + newName: "Id"); migrationBuilder.RenameColumn( name: "BookmarkId", @@ -46,13 +47,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime type: "TEXT", nullable: true); - migrationBuilder.AddColumn( - name: "Id", - table: "KeyValuePairs", - type: "TEXT", - nullable: false, - defaultValue: ""); - migrationBuilder.AddColumn( name: "TenantId", table: "KeyValuePairs", @@ -71,11 +65,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime type: "TEXT", nullable: true); - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - table: "KeyValuePairs", - column: "Id"); - migrationBuilder.CreateTable( name: "BookmarkQueueItems", columns: table => new @@ -176,10 +165,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "IX_StoredTrigger_TenantId", table: "Triggers"); - migrationBuilder.DropPrimaryKey( - name: "PK_KeyValuePairs", - table: "KeyValuePairs"); - migrationBuilder.DropIndex( name: "IX_SerializedKeyValuePair_TenantId", table: "KeyValuePairs"); @@ -204,10 +189,6 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "TenantId", table: "Triggers"); - migrationBuilder.DropColumn( - name: "Id", - table: "KeyValuePairs"); - migrationBuilder.DropColumn( name: "TenantId", table: "KeyValuePairs"); @@ -220,15 +201,15 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime name: "TenantId", table: "ActivityExecutionRecords"); + migrationBuilder.RenameColumn( + name: "Id", + table: "KeyValuePairs", + newName: "Key"); + migrationBuilder.RenameColumn( name: "Id", table: "Bookmarks", newName: "BookmarkId"); - - migrationBuilder.AddPrimaryKey( - name: "PK_KeyValuePairs", - table: "KeyValuePairs", - column: "Key"); } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 21a5c53c9..de19a039a 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -15,17 +15,13 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => { b.Property("Id") .HasColumnType("TEXT"); - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("SerializedValue") .IsRequired() .HasColumnType("TEXT"); diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Configurations.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Configurations.cs index 1e097aefa..140e84dab 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Configurations.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/Configurations.cs @@ -68,6 +68,7 @@ public class Configurations : public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + builder.Ignore(x => x.Key); builder.HasIndex(x => x.TenantId, $"IX_{nameof(SerializedKeyValuePair)}_{nameof(SerializedKeyValuePair.TenantId)}"); } From 95a5986b527f25ea8434c4a0fe240d8ffe2830fc Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 16 Jan 2025 20:45:00 +0100 Subject: [PATCH 065/166] Create dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..d8b5df869 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "NuGet" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 93aa3a7835fa4c9d9972420e018434c8af0ef309 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 17 Jan 2025 08:56:21 +0100 Subject: [PATCH 066/166] Remove WorkflowInboxMessages table and associated logic. This change eliminates the previously defined WorkflowInboxMessages table and its creation logic for all databases. Cleanup simplifies the database schema and maintains focus on essential entities. --- .../Elsa.Dapper.Migrations/Runtime/V3_3.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs index 653346ed5..8c91daf2b 100644 --- a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs +++ b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs @@ -14,7 +14,6 @@ public class V3_3 : Migration /// public override void Up() { - Delete.Table("WorkflowInboxMessages"); Alter.Table("Triggers").AddColumn("TenantId").AsString().Nullable(); Alter.Table("Bookmarks").AddColumn("TenantId").AsString().Nullable(); Alter.Table("WorkflowExecutionLogRecords").AddColumn("TenantId").AsString().Nullable(); @@ -74,21 +73,6 @@ public class V3_3 : Migration .WithColumn("ExpiresAt").AsDateTimeOffset().Indexed() ; - IfDatabase("Sqlite") - .Create - .Table("WorkflowInboxMessages") - .WithColumn("Id").AsString().PrimaryKey() - .WithColumn("ActivityTypeName").AsString().NotNullable().Indexed() - .WithColumn("WorkflowInstanceId").AsString().Nullable().Indexed() - .WithColumn("ActivityInstanceId").AsString().Nullable().Indexed() - .WithColumn("CorrelationId").AsString().Nullable().Indexed() - .WithColumn("Hash").AsString().NotNullable().Indexed() - .WithColumn("SerializedBookmarkPayload").AsString(MaxValue) - .WithColumn("SerializedInput").AsString(MaxValue).Nullable() - .WithColumn("CreatedAt").AsDateTime2().Indexed() - .WithColumn("ExpiresAt").AsDateTime2().Indexed() - ; - Delete.Table("BookmarkQueueItems"); } } \ No newline at end of file From 35d21a69ed7070254c4642456753ddeaa8d029ab Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 17 Jan 2025 08:57:22 +0100 Subject: [PATCH 067/166] Remove creation of WorkflowInboxMessages table. This commit deletes the code responsible for creating the WorkflowInboxMessages table. The change simplifies the migration script by removing unused or unnecessary table definitions and ensures consistency in database schema evolution. --- .../Elsa.Dapper.Migrations/Runtime/V3_3.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs index 8c91daf2b..bbe82ae9f 100644 --- a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs +++ b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_3.cs @@ -57,22 +57,6 @@ public class V3_3 : Migration Delete.Column("TenantId").FromTable("ActivityExecutionRecords"); Delete.Column("TenantId").FromTable("KeyValuePairs"); Rename.Column("Id").OnTable("KeyValuePairs").To("Key"); - - IfDatabase("SqlServer", "Oracle", "MySql", "Postgres") - .Create - .Table("WorkflowInboxMessages") - .WithColumn("Id").AsString().PrimaryKey() - .WithColumn("ActivityTypeName").AsString().NotNullable().Indexed() - .WithColumn("WorkflowInstanceId").AsString().Nullable().Indexed() - .WithColumn("ActivityInstanceId").AsString().Nullable().Indexed() - .WithColumn("CorrelationId").AsString().Nullable().Indexed() - .WithColumn("Hash").AsString().NotNullable().Indexed() - .WithColumn("SerializedBookmarkPayload").AsString(MaxValue) - .WithColumn("SerializedInput").AsString(MaxValue).Nullable() - .WithColumn("CreatedAt").AsDateTimeOffset().Indexed() - .WithColumn("ExpiresAt").AsDateTimeOffset().Indexed() - ; - Delete.Table("BookmarkQueueItems"); } } \ No newline at end of file From ad8eeecdced5cf6e92b54aa20ebdb713de9ac41c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Jan 2025 00:24:01 +0100 Subject: [PATCH 068/166] Refactor workflow management for improved modularization Reorganized workflow handling to enhance clarity and maintainability, including introducing dedicated request handlers, refactoring middleware, and improving definition publishing. Deprecated `ValidateWorkflowResponse` and updated relevant namespaces for notifications handlers. --- .../Elsa.Mediator/Contracts/IRequestSender.cs | 2 +- .../RequestHandlerInvokerMiddleware.cs | 50 +++---- .../Middleware/Request/RequestContext.cs | 4 +- .../Elsa.Mediator/Services/DefaultMediator.cs | 13 +- src/modules/Elsa.Http/Elsa.Http.csproj | 1 - src/modules/Elsa.Http/Features/HttpFeature.cs | 3 +- .../ValidateWorkflowRequestHandler.cs | 25 ++-- .../GetByDefinitionId/Endpoint.cs | 20 +-- .../Extensions/ModuleExtensions.cs | 10 +- .../CachingWorkflowDefinitionsFeature.cs | 1 + .../Features/WorkflowDefinitionsFeature.cs | 10 +- .../Features/WorkflowManagementFeature.cs | 1 + .../DeleteWorkflowInstances.cs | 2 +- .../EvictWorkflowDefinitionServiceCache.cs | 2 +- .../RefreshActivityRegistry.cs | 2 +- .../UpdateConsumingWorkflows.cs | 3 +- .../Request/FindWorkflowDefinitionHandler.cs | 23 +++ .../Mappers/WorkflowDefinitionMapper.cs | 51 ++++++- .../Materializers/JsonWorkflowMaterializer.cs | 2 +- .../Models/WorkflowValidationError.cs | 2 +- .../ValidateWorkflowRequest.cs | 6 +- .../Requests/FindWorkflowDefinitionRequest.cs | 12 ++ .../Responses/ValidateWorkflowResponse.cs | 10 -- .../Services/WorkflowDefinitionPublisher.cs | 141 +++++++++--------- .../Services/WorkflowValidator.cs | 19 +-- 25 files changed, 226 insertions(+), 189 deletions(-) rename src/modules/Elsa.Workflows.Management/Handlers/{ => Notification}/DeleteWorkflowInstances.cs (97%) rename src/modules/Elsa.Workflows.Management/Handlers/{ => Notification}/EvictWorkflowDefinitionServiceCache.cs (97%) rename src/modules/Elsa.Workflows.Management/Handlers/{ => Notification}/RefreshActivityRegistry.cs (98%) rename src/modules/Elsa.Workflows.Management/Handlers/{ => Notification}/UpdateConsumingWorkflows.cs (89%) create mode 100644 src/modules/Elsa.Workflows.Management/Handlers/Request/FindWorkflowDefinitionHandler.cs rename src/modules/Elsa.Workflows.Management/{Requests => Notifications}/ValidateWorkflowRequest.cs (50%) create mode 100644 src/modules/Elsa.Workflows.Management/Requests/FindWorkflowDefinitionRequest.cs delete mode 100644 src/modules/Elsa.Workflows.Management/Responses/ValidateWorkflowResponse.cs diff --git a/src/common/Elsa.Mediator/Contracts/IRequestSender.cs b/src/common/Elsa.Mediator/Contracts/IRequestSender.cs index 37d4288f9..de23cd440 100644 --- a/src/common/Elsa.Mediator/Contracts/IRequestSender.cs +++ b/src/common/Elsa.Mediator/Contracts/IRequestSender.cs @@ -12,5 +12,5 @@ public interface IRequestSender /// The cancellation token. /// The type of the response. /// The response. - Task> SendAsync(IRequest request, CancellationToken cancellationToken = default); + Task SendAsync(IRequest request, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/common/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs b/src/common/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs index 930d324f9..2055515f1 100644 --- a/src/common/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs +++ b/src/common/Elsa.Mediator/Middleware/Request/Components/RequestHandlerInvokerMiddleware.cs @@ -6,24 +6,10 @@ namespace Elsa.Mediator.Middleware.Request.Components; /// /// A middleware component that invokes request handlers. /// -public class RequestHandlerInvokerMiddleware : IRequestMiddleware +public class RequestHandlerInvokerMiddleware( + RequestMiddlewareDelegate next, + IEnumerable requestHandlers) : IRequestMiddleware { - private readonly RequestMiddlewareDelegate _next; - private readonly IEnumerable _requestHandlers; - - /// - /// Initializes a new instance of the class. - /// - /// The next middleware in the pipeline. - /// - public RequestHandlerInvokerMiddleware( - RequestMiddlewareDelegate next, - IEnumerable requestHandlers) - { - _next = next; - _requestHandlers = requestHandlers; - } - /// public async ValueTask InvokeAsync(RequestContext context) { @@ -33,22 +19,26 @@ public class RequestHandlerInvokerMiddleware : IRequestMiddleware var requestType = request.GetType(); var responseType = context.ResponseType; var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, responseType); - var handlers = _requestHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray(); + var handlers = requestHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray(); + + if (handlers.Length == 0) + throw new InvalidOperationException($"There is no handler to handle the {requestType.FullName} request"); - foreach (var handler in handlers) - { - var handleMethod = handlerType.GetMethod("HandleAsync")!; - var cancellationToken = context.CancellationToken; - var task = (Task)handleMethod.Invoke(handler, new object?[] { request, cancellationToken })!; - await task; + if (handlers.Length > 1) + throw new InvalidOperationException($"Multiple handlers were found to handle the {requestType.FullName} request"); - // Get result of task. - var taskWithReturnType = typeof(Task<>).MakeGenericType(responseType); - var resultProperty = taskWithReturnType.GetProperty(nameof(Task.Result))!; - context.Responses.Add(resultProperty.GetValue(task)!); - } + var handler = handlers.First(); + var handleMethod = handlerType.GetMethod("HandleAsync")!; + var cancellationToken = context.CancellationToken; + var task = (Task)handleMethod.Invoke(handler, [request, cancellationToken])!; + await task; + + // Get result of task. + var taskWithReturnType = typeof(Task<>).MakeGenericType(responseType); + var resultProperty = taskWithReturnType.GetProperty(nameof(Task.Result))!; + context.Response = resultProperty.GetValue(task)!; // Invoke next middleware. - await _next(context); + await next(context); } } \ No newline at end of file diff --git a/src/common/Elsa.Mediator/Middleware/Request/RequestContext.cs b/src/common/Elsa.Mediator/Middleware/Request/RequestContext.cs index 8aa4a5232..b6093cb68 100644 --- a/src/common/Elsa.Mediator/Middleware/Request/RequestContext.cs +++ b/src/common/Elsa.Mediator/Middleware/Request/RequestContext.cs @@ -36,7 +36,7 @@ public class RequestContext public CancellationToken CancellationToken { get; init; } /// - /// Gets the responses from each request handler. + /// Gets the response the request handler. /// - public ICollection Responses { get; set; } = new List(); + public object? Response { get; set; } } \ No newline at end of file diff --git a/src/common/Elsa.Mediator/Services/DefaultMediator.cs b/src/common/Elsa.Mediator/Services/DefaultMediator.cs index 7665624e6..21b3c36c0 100644 --- a/src/common/Elsa.Mediator/Services/DefaultMediator.cs +++ b/src/common/Elsa.Mediator/Services/DefaultMediator.cs @@ -41,23 +41,20 @@ public class DefaultMediator : IMediator } /// - public async Task> SendAsync(IRequest request, CancellationToken cancellationToken = default) + public async Task SendAsync(IRequest request, CancellationToken cancellationToken = default) { var responseType = typeof(T); var context = new RequestContext(request, responseType, cancellationToken); await _requestPipeline.ExecuteAsync(context); - if (context.Responses.All(x => x is T)) - return context.Responses.Cast().AsEnumerable(); - - throw new InvalidCastException($"Unable to cast objects in Responses property to type {typeof(T)}"); + return (T?)context.Response; } /// public async Task SendAsync(ICommand command, CancellationToken cancellationToken = default) => await SendAsync(command, _defaultCommandStrategy, cancellationToken); /// - public async Task SendAsync(ICommand command, ICommandStrategy? strategy = default, CancellationToken cancellationToken = default) + public async Task SendAsync(ICommand command, ICommandStrategy? strategy = null, CancellationToken cancellationToken = default) { var resultType = typeof(Unit); strategy ??= _defaultCommandStrategy; @@ -69,7 +66,7 @@ public class DefaultMediator : IMediator public async Task SendAsync(ICommand command, CancellationToken cancellationToken = default) => await SendAsync(command, _defaultCommandStrategy, cancellationToken); /// - public async Task SendAsync(ICommand command, ICommandStrategy strategy, CancellationToken cancellationToken = default) + public async Task SendAsync(ICommand command, ICommandStrategy? strategy, CancellationToken cancellationToken = default) { var resultType = typeof(T); strategy ??= _defaultCommandStrategy; @@ -83,7 +80,7 @@ public class DefaultMediator : IMediator public async Task SendAsync(INotification notification, CancellationToken cancellationToken = default) => await SendAsync(notification, _defaultPublishingStrategy, cancellationToken); /// - public async Task SendAsync(INotification notification, IEventPublishingStrategy? strategy = default, CancellationToken cancellationToken = default) + public async Task SendAsync(INotification notification, IEventPublishingStrategy? strategy = null, CancellationToken cancellationToken = default) { strategy ??= _defaultPublishingStrategy; var context = new NotificationContext(notification, strategy, cancellationToken); diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj index 9f1f9fda3..aaa37a25c 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj +++ b/src/modules/Elsa.Http/Elsa.Http.csproj @@ -13,7 +13,6 @@ - diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 3115b1681..65c0e42d8 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -16,7 +16,6 @@ using Elsa.Http.Tasks; using Elsa.Http.UIHints; using Elsa.Workflows; using Elsa.Workflows.Management.Requests; -using Elsa.Workflows.Management.Responses; using FluentStorage; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; @@ -168,7 +167,7 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddHttpContextAccessor() // Handlers. - .AddRequestHandler() + .AddNotificationHandler() .AddNotificationHandler() // Content parsers. diff --git a/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs b/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs index b1f706ef4..fb51064e7 100644 --- a/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs +++ b/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs @@ -2,9 +2,7 @@ using Elsa.Extensions; using Elsa.Http.Bookmarks; using Elsa.Mediator.Contracts; using Elsa.Workflows.Helpers; -using Elsa.Workflows.Management.Models; -using Elsa.Workflows.Management.Requests; -using Elsa.Workflows.Management.Responses; +using Elsa.Workflows.Management.Notifications; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Filters; using JetBrains.Annotations; @@ -15,7 +13,7 @@ namespace Elsa.Http.Handlers; /// A handler that validates a workflow path and return any errors. /// [UsedImplicitly] -public class ValidateWorkflowRequestHandler : IRequestHandler +public class ValidateWorkflowRequestHandler : INotificationHandler { private readonly ITriggerStore _triggerStore; private readonly ITriggerIndexer _triggerIndexer; @@ -28,14 +26,17 @@ public class ValidateWorkflowRequestHandler : IRequestHandler - public async Task HandleAsync(ValidateWorkflowRequest request, CancellationToken cancellationToken) + + public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken) { - var workflow = request.Workflow; + var workflow = notification.Workflow; var httpEndpointTriggers = (await _triggerIndexer.GetTriggersAsync(workflow, cancellationToken)).Where(x => x.Payload is HttpEndpointBookmarkPayload).ToList(); - var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(new TriggerFilter { Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint)) }, cancellationToken)).ToList(); - var validationErrors = new List(); + var filter = new TriggerFilter + { + Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint)) + }; + var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(filter, cancellationToken)).ToList(); + var validationErrors = notification.ValidationErrors; foreach (var httpEndpointTrigger in httpEndpointTriggers) { @@ -53,9 +54,7 @@ public class ValidateWorkflowRequestHandler : IRequestHandler +internal class GetByDefinitionId(IMediator mediator, IWorkflowDefinitionLinker linker) : ElsaEndpoint { public override void Configure() { @@ -19,16 +18,9 @@ internal class GetByDefinitionId(IWorkflowDefinitionStore store, IWorkflowDefini public override async Task HandleAsync(Request request, CancellationToken cancellationToken) { var versionOptions = request.VersionOptions != null ? VersionOptions.FromString(request.VersionOptions) : VersionOptions.Latest; - - var filter = new WorkflowDefinitionFilter - { - DefinitionId = request.DefinitionId, - VersionOptions = versionOptions - }; - - var order = new WorkflowDefinitionOrder(x => x.Version, OrderDirection.Descending); - var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault(); - + var findRequest = new FindWorkflowDefinitionRequest(request.DefinitionId, versionOptions); + var definition = await mediator.SendAsync(findRequest, cancellationToken); + if (definition == null) { await SendNotFoundAsync(cancellationToken); diff --git a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs index 6c7306f07..dc6a1dc46 100644 --- a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs @@ -14,7 +14,7 @@ public static class ModuleExtensions /// /// Adds the workflow management feature to the specified module. /// - public static IModule UseWorkflowManagement(this IModule module, Action? configure = default) + public static IModule UseWorkflowManagement(this IModule module, Action? configure = null) { module.Configure(management => { @@ -27,7 +27,7 @@ public static class ModuleExtensions /// /// Adds the default workflow management feature to the specified module. /// - public static WorkflowManagementFeature UseWorkflowDefinitions(this WorkflowManagementFeature feature, Action? configure = default) + public static WorkflowManagementFeature UseWorkflowDefinitions(this WorkflowManagementFeature feature, Action? configure = null) { feature.Module.Configure(configure); return feature; @@ -36,7 +36,7 @@ public static class ModuleExtensions /// /// Adds the workflow instance feature to workflow management module. /// - public static WorkflowManagementFeature UseWorkflowInstances(this WorkflowManagementFeature feature, Action? configure = default) + public static WorkflowManagementFeature UseWorkflowInstances(this WorkflowManagementFeature feature, Action? configure = null) { feature.Module.Configure(configure); return feature; @@ -45,7 +45,7 @@ public static class ModuleExtensions /// /// Adds the Elsa DSL integration feature. /// - public static WorkflowManagementFeature UseDslIntegration(this WorkflowManagementFeature feature, Action? configure = default) + public static WorkflowManagementFeature UseDslIntegration(this WorkflowManagementFeature feature, Action? configure = null) { feature.Module.Configure(configure); return feature; @@ -84,7 +84,7 @@ public static class ModuleExtensions /// /// Adds caching stores feature to the workflow management feature. /// - public static WorkflowManagementFeature UseCache(this WorkflowManagementFeature feature, Action? configure = default) + public static WorkflowManagementFeature UseCache(this WorkflowManagementFeature feature, Action? configure = null) { feature.Module.Configure(configure); return feature; diff --git a/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs b/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs index a36ed798c..d9d666b75 100644 --- a/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs @@ -1,6 +1,7 @@ using Elsa.Features.Abstractions; using Elsa.Features.Services; using Elsa.Workflows.Management.Handlers; +using Elsa.Workflows.Management.Handlers.Notification; using Elsa.Workflows.Management.Services; using Elsa.Workflows.Management.Stores; using Microsoft.Extensions.DependencyInjection; diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowDefinitionsFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowDefinitionsFeature.cs index a29d9ed1f..916ad3c67 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowDefinitionsFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowDefinitionsFeature.cs @@ -1,5 +1,9 @@ using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Handlers.Request; +using Elsa.Workflows.Management.Requests; using Elsa.Workflows.Management.Stores; using Microsoft.Extensions.DependencyInjection; @@ -19,10 +23,14 @@ public class WorkflowDefinitionsFeature : FeatureBase /// The factory to create new instances of . /// public Func WorkflowDefinitionStore { get; set; } = sp => sp.GetRequiredService(); + public Func FindWorkflowDefinitionHandler { get; set; } = () => typeof(FindWorkflowDefinitionHandler); /// public override void Apply() { - Services.AddScoped(WorkflowDefinitionStore); + Services + .AddScoped(WorkflowDefinitionStore) + .AddScoped(typeof(IRequestHandler), FindWorkflowDefinitionHandler()) + ; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index a81d8fd47..a46f191ed 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -18,6 +18,7 @@ using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Handlers; +using Elsa.Workflows.Management.Handlers.Notification; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Management.Models; diff --git a/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notification/DeleteWorkflowInstances.cs similarity index 97% rename from src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs rename to src/modules/Elsa.Workflows.Management/Handlers/Notification/DeleteWorkflowInstances.cs index 01cd75881..01429d33a 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notification/DeleteWorkflowInstances.cs @@ -3,7 +3,7 @@ using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Management.Notifications; using JetBrains.Annotations; -namespace Elsa.Workflows.Management.Handlers; +namespace Elsa.Workflows.Management.Handlers.Notification; /// /// Deletes workflow instances when a workflow definition or version is deleted. diff --git a/src/modules/Elsa.Workflows.Management/Handlers/EvictWorkflowDefinitionServiceCache.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notification/EvictWorkflowDefinitionServiceCache.cs similarity index 97% rename from src/modules/Elsa.Workflows.Management/Handlers/EvictWorkflowDefinitionServiceCache.cs rename to src/modules/Elsa.Workflows.Management/Handlers/Notification/EvictWorkflowDefinitionServiceCache.cs index 5c1e4dec9..3cc7f15da 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/EvictWorkflowDefinitionServiceCache.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notification/EvictWorkflowDefinitionServiceCache.cs @@ -2,7 +2,7 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Management.Notifications; using JetBrains.Annotations; -namespace Elsa.Workflows.Management.Handlers; +namespace Elsa.Workflows.Management.Handlers.Notification; /// /// A workflow definition notifications handler for evicting the cache of the workflow definition service. diff --git a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notification/RefreshActivityRegistry.cs similarity index 98% rename from src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs rename to src/modules/Elsa.Workflows.Management/Handlers/Notification/RefreshActivityRegistry.cs index f6fe2b7bd..bad83d829 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/RefreshActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notification/RefreshActivityRegistry.cs @@ -5,7 +5,7 @@ using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Notifications; using JetBrains.Annotations; -namespace Elsa.Workflows.Management.Handlers; +namespace Elsa.Workflows.Management.Handlers.Notification; /// /// Refreshes the for the provider whenever an is published, retracted or deleted. diff --git a/src/modules/Elsa.Workflows.Management/Handlers/UpdateConsumingWorkflows.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notification/UpdateConsumingWorkflows.cs similarity index 89% rename from src/modules/Elsa.Workflows.Management/Handlers/UpdateConsumingWorkflows.cs rename to src/modules/Elsa.Workflows.Management/Handlers/Notification/UpdateConsumingWorkflows.cs index 39082829c..1e3819d4b 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/UpdateConsumingWorkflows.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notification/UpdateConsumingWorkflows.cs @@ -1,9 +1,8 @@ using Elsa.Extensions; using Elsa.Mediator.Contracts; -using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Notifications; -namespace Elsa.Workflows.Management.Handlers; +namespace Elsa.Workflows.Management.Handlers.Notification; /// /// Updates consuming workflows when a workflow definition is published. diff --git a/src/modules/Elsa.Workflows.Management/Handlers/Request/FindWorkflowDefinitionHandler.cs b/src/modules/Elsa.Workflows.Management/Handlers/Request/FindWorkflowDefinitionHandler.cs new file mode 100644 index 000000000..0febd0442 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Handlers/Request/FindWorkflowDefinitionHandler.cs @@ -0,0 +1,23 @@ +using Elsa.Common.Entities; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Management.Requests; + +namespace Elsa.Workflows.Management.Handlers.Request; + +public class FindWorkflowDefinitionHandler(IWorkflowDefinitionStore store) : IRequestHandler +{ + public async Task HandleAsync(FindWorkflowDefinitionRequest request, CancellationToken cancellationToken) + { + var filter = new WorkflowDefinitionFilter + { + DefinitionId = request.DefinitionId, + VersionOptions = request.VersionOptions + }; + + var order = new WorkflowDefinitionOrder(x => x.Version, OrderDirection.Descending); + var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault(); + return definition; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs index bc941ce70..cd22cc0a4 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowDefinitionMapper.cs @@ -1,5 +1,6 @@ using Elsa.Workflows.Activities; using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Management.Models; using Elsa.Workflows.Models; @@ -34,9 +35,9 @@ public class WorkflowDefinitionMapper var root = _activitySerializer.Deserialize(source.StringData!); return new( - new WorkflowIdentity(source.DefinitionId, source.Version, source.Id, source.TenantId), - new WorkflowPublication(source.IsLatest, source.IsPublished), - new WorkflowMetadata(source.Name, source.Description, source.CreatedAt, source.ToolVersion), + new(source.DefinitionId, source.Version, source.Id, source.TenantId), + new(source.IsLatest, source.IsPublished), + new(source.Name, source.Description, source.CreatedAt, source.ToolVersion), source.Options, root, source.Variables, @@ -66,9 +67,9 @@ public class WorkflowDefinitionMapper #pragma warning restore CS0618 return new( - new WorkflowIdentity(source.DefinitionId, source.Version, source.Id, source.TenantId), - new WorkflowPublication(source.IsLatest, source.IsPublished), - new WorkflowMetadata(source.Name, source.Description, source.CreatedAt, source.ToolVersion), + new(source.DefinitionId, source.Version, source.Id, source.TenantId), + new(source.IsLatest, source.IsPublished), + new(source.Name, source.Description, source.CreatedAt, source.ToolVersion), options, root, variables, @@ -79,6 +80,38 @@ public class WorkflowDefinitionMapper source.IsReadonly, source.IsSystem); } + + public WorkflowDefinition MapToWorkflowDefinition(WorkflowDefinitionModel source) + { + var root = source.Root!; + var variables = _variableDefinitionMapper.Map(source.Variables).ToList(); + var options = source.Options ?? new WorkflowOptions(); + var stringData = _activitySerializer.Serialize(root); + + return new() + { + IsPublished = source.IsPublished, + Description = source.Description, + Id = source.Id, + Inputs = source.Inputs ?? [], + Name = source.Name, + Options = options, + Outcomes = source.Outcomes ?? [], + Outputs = source.Outputs ?? [], + Variables = variables, + Version = source.Version, + CreatedAt = source.CreatedAt, + CustomProperties = source.CustomProperties ?? new Dictionary(), + DefinitionId = source.DefinitionId, + IsLatest = source.IsLatest, + IsReadonly = source.IsReadonly, + IsSystem = source.IsSystem, + TenantId = source.TenantId, + ToolVersion = source.ToolVersion, + StringData = stringData, + MaterializerName = JsonWorkflowMaterializer.MaterializerName + }; + } /// /// Maps many s to many s. @@ -86,8 +119,10 @@ public class WorkflowDefinitionMapper /// The source s. /// An optional cancellation token. /// The mapped s. - public async Task> MapAsync(IEnumerable source, CancellationToken cancellationToken = default) => - await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken))); + public async Task> MapAsync(IEnumerable source, CancellationToken cancellationToken = default) + { + return await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken))); + } /// /// Maps a to a . diff --git a/src/modules/Elsa.Workflows.Management/Materializers/JsonWorkflowMaterializer.cs b/src/modules/Elsa.Workflows.Management/Materializers/JsonWorkflowMaterializer.cs index 0d24fccf5..29477d1e0 100644 --- a/src/modules/Elsa.Workflows.Management/Materializers/JsonWorkflowMaterializer.cs +++ b/src/modules/Elsa.Workflows.Management/Materializers/JsonWorkflowMaterializer.cs @@ -31,7 +31,7 @@ public class JsonWorkflowMaterializer : IWorkflowMaterializer public ValueTask MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken) { var workflow = ToWorkflow(definition); - return new ValueTask(workflow); + return new(workflow); } private Workflow ToWorkflow(WorkflowDefinition definition) => _workflowDefinitionMapper.Map(definition); diff --git a/src/modules/Elsa.Workflows.Management/Models/WorkflowValidationError.cs b/src/modules/Elsa.Workflows.Management/Models/WorkflowValidationError.cs index b33564cbf..108090c20 100644 --- a/src/modules/Elsa.Workflows.Management/Models/WorkflowValidationError.cs +++ b/src/modules/Elsa.Workflows.Management/Models/WorkflowValidationError.cs @@ -5,4 +5,4 @@ namespace Elsa.Workflows.Management.Models; /// /// The error message. /// The Id of the activity that caused the error, if any. -public record WorkflowValidationError(string Message, string? ActivityId = default); \ No newline at end of file +public record WorkflowValidationError(string Message, string? ActivityId = null); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Requests/ValidateWorkflowRequest.cs b/src/modules/Elsa.Workflows.Management/Notifications/ValidateWorkflowRequest.cs similarity index 50% rename from src/modules/Elsa.Workflows.Management/Requests/ValidateWorkflowRequest.cs rename to src/modules/Elsa.Workflows.Management/Notifications/ValidateWorkflowRequest.cs index 13eeb3899..967c56686 100644 --- a/src/modules/Elsa.Workflows.Management/Requests/ValidateWorkflowRequest.cs +++ b/src/modules/Elsa.Workflows.Management/Notifications/ValidateWorkflowRequest.cs @@ -1,11 +1,11 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities; -using Elsa.Workflows.Management.Responses; +using Elsa.Workflows.Management.Models; -namespace Elsa.Workflows.Management.Requests; +namespace Elsa.Workflows.Management.Notifications; /// /// A request to validate a workflow definition. /// /// The workflow materialized from the definition. -public record ValidateWorkflowRequest(Workflow Workflow) : IRequest; \ No newline at end of file +public record WorkflowDefinitionValidating(Workflow Workflow, ICollection ValidationErrors) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Requests/FindWorkflowDefinitionRequest.cs b/src/modules/Elsa.Workflows.Management/Requests/FindWorkflowDefinitionRequest.cs new file mode 100644 index 000000000..a539d5aa5 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Requests/FindWorkflowDefinitionRequest.cs @@ -0,0 +1,12 @@ +using Elsa.Common.Models; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Entities; + +namespace Elsa.Workflows.Management.Requests; + +/// +/// A request to find a workflow definition. +/// +/// The ID of the workflow definition. +/// The version options. +public record FindWorkflowDefinitionRequest(string DefinitionId, VersionOptions VersionOptions) : IRequest; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Responses/ValidateWorkflowResponse.cs b/src/modules/Elsa.Workflows.Management/Responses/ValidateWorkflowResponse.cs deleted file mode 100644 index c87d447af..000000000 --- a/src/modules/Elsa.Workflows.Management/Responses/ValidateWorkflowResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Elsa.Workflows.Management.Models; -using Elsa.Workflows.Management.Requests; - -namespace Elsa.Workflows.Management.Responses; - -/// -/// Provides a response to a . -/// -/// The validation errors, if any. -public record ValidateWorkflowResponse(ICollection ValidationErrors); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs index 0cdc9491e..ab60f6558 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs @@ -14,54 +14,33 @@ using Elsa.Workflows.Management.Requests; namespace Elsa.Workflows.Management.Services; /// -public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher +public class WorkflowDefinitionPublisher( + IWorkflowDefinitionService workflowDefinitionService, + IWorkflowDefinitionStore workflowDefinitionStore, + IWorkflowValidator workflowValidator, + INotificationSender notificationSender, + IIdentityGenerator identityGenerator, + IActivitySerializer activitySerializer, + ISystemClock systemClock) + : IWorkflowDefinitionPublisher { - private readonly IWorkflowDefinitionService _workflowDefinitionService; - private readonly IWorkflowDefinitionStore _workflowDefinitionStore; - private readonly INotificationSender _notificationSender; - private readonly IIdentityGenerator _identityGenerator; - private readonly IActivitySerializer _activitySerializer; - private readonly IRequestSender _requestSender; - private readonly ISystemClock _systemClock; - - /// - /// Constructor. - /// - public WorkflowDefinitionPublisher( - IWorkflowDefinitionService workflowDefinitionService, - IWorkflowDefinitionStore workflowDefinitionStore, - INotificationSender notificationSender, - IIdentityGenerator identityGenerator, - IActivitySerializer activitySerializer, - IRequestSender requestSender, - ISystemClock systemClock) - { - _workflowDefinitionService = workflowDefinitionService; - _workflowDefinitionStore = workflowDefinitionStore; - _notificationSender = notificationSender; - _identityGenerator = identityGenerator; - _activitySerializer = activitySerializer; - _requestSender = requestSender; - _systemClock = systemClock; - } - /// public WorkflowDefinition New(IActivity? root = null) { root ??= new Sequence(); - var id = _identityGenerator.GenerateId(); - var definitionId = _identityGenerator.GenerateId(); + var id = identityGenerator.GenerateId(); + var definitionId = identityGenerator.GenerateId(); const int version = 1; - return new WorkflowDefinition + return new() { Id = id, DefinitionId = definitionId, Version = version, IsLatest = true, IsPublished = false, - CreatedAt = _systemClock.UtcNow, - StringData = _activitySerializer.Serialize(root), + CreatedAt = systemClock.UtcNow, + StringData = activitySerializer.Serialize(root), MaterializerName = JsonWorkflowMaterializer.MaterializerName }; } @@ -69,14 +48,18 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher /// public async Task PublishAsync(string definitionId, CancellationToken cancellationToken = default) { - var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.Latest }; - var definition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.Latest + }; + var definition = await workflowDefinitionStore.FindAsync(filter, cancellationToken); if (definition == null) - return new PublishWorkflowDefinitionResult(false, new List + return new(false, new List { new("Workflow definition not found.") - }, null); + }, new([])); return await PublishAsync(definition, cancellationToken); } @@ -84,47 +67,53 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher /// public async Task PublishAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) { - var workflowGraph = await _workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken); - var responses = await _requestSender.SendAsync(new ValidateWorkflowRequest(workflowGraph.Workflow), cancellationToken); - var validationErrors = responses.SelectMany(r => r.ValidationErrors).ToList(); + var workflowGraph = await workflowDefinitionService.MaterializeWorkflowAsync(definition, cancellationToken); + var validationErrors = (await workflowValidator.ValidateAsync(workflowGraph.Workflow, cancellationToken)).ToList(); if (validationErrors.Any()) - return new PublishWorkflowDefinitionResult(false, validationErrors, null); - - await _notificationSender.SendAsync(new WorkflowDefinitionPublishing(definition), cancellationToken); + return new(false, validationErrors, new([])); + await notificationSender.SendAsync(new WorkflowDefinitionPublishing(definition), cancellationToken); var definitionId = definition.DefinitionId; // Reset current latest and published definitions. - var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.LatestOrPublished }; - var publishedWorkflows = await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.LatestOrPublished + }; + var publishedWorkflows = await workflowDefinitionStore.FindManyAsync(filter, cancellationToken); foreach (var publishedAndOrLatestWorkflow in publishedWorkflows) { - var isPublished = publishedAndOrLatestWorkflow.IsPublished; + var isPublished = publishedAndOrLatestWorkflow.IsPublished; publishedAndOrLatestWorkflow.IsPublished = false; publishedAndOrLatestWorkflow.IsLatest = false; - await _workflowDefinitionStore.SaveAsync(publishedAndOrLatestWorkflow, cancellationToken); - + await workflowDefinitionStore.SaveAsync(publishedAndOrLatestWorkflow, cancellationToken); + if (isPublished) - await _notificationSender.SendAsync(new WorkflowDefinitionVersionRetracted(publishedAndOrLatestWorkflow), cancellationToken); + await notificationSender.SendAsync(new WorkflowDefinitionVersionRetracted(publishedAndOrLatestWorkflow), cancellationToken); } // Save the new published definition. definition.IsPublished = true; definition = Initialize(definition); - await _workflowDefinitionStore.SaveAsync(definition, cancellationToken); + await workflowDefinitionStore.SaveAsync(definition, cancellationToken); var affectedWorkflows = new AffectedWorkflows(new List()); - await _notificationSender.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), cancellationToken); - return new PublishWorkflowDefinitionResult(true, validationErrors, affectedWorkflows); + await notificationSender.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), cancellationToken); + return new(true, validationErrors, affectedWorkflows); } /// public async Task RetractAsync(string definitionId, CancellationToken cancellationToken = default) { - var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.Published }; - var definition = await _workflowDefinitionStore.FindAsync(filter, cancellationToken); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.Published + }; + var definition = await workflowDefinitionStore.FindAsync(filter, cancellationToken); if (definition == null) return null; @@ -140,19 +129,26 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher definition.IsPublished = false; - await _notificationSender.SendAsync(new WorkflowDefinitionRetracting(definition), cancellationToken); - await _workflowDefinitionStore.SaveAsync(definition, cancellationToken); - await _notificationSender.SendAsync(new WorkflowDefinitionRetracted(definition), cancellationToken); + await notificationSender.SendAsync(new WorkflowDefinitionRetracting(definition), cancellationToken); + await workflowDefinitionStore.SaveAsync(definition, cancellationToken); + await notificationSender.SendAsync(new WorkflowDefinitionRetracted(definition), cancellationToken); return definition; } /// public async Task GetDraftAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default) { - var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = versionOptions }; + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = versionOptions + }; var order = new WorkflowDefinitionOrder(x => x.Version, OrderDirection.Descending); - var lastVersion = await _workflowDefinitionStore.FindLastVersionAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId }, cancellationToken); - var definition = await _workflowDefinitionStore.FindAsync(filter, order, cancellationToken) ?? lastVersion; + var lastVersion = await workflowDefinitionStore.FindLastVersionAsync(new() + { + DefinitionId = definitionId + }, cancellationToken); + var definition = await workflowDefinitionStore.FindAsync(filter, order, cancellationToken) ?? lastVersion; if (definition == null!) return null; @@ -163,8 +159,8 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher var draft = definition.ShallowClone(); draft.Version = lastVersion?.Version + 1 ?? 1; - draft.CreatedAt = _systemClock.UtcNow; - draft.Id = _identityGenerator.GenerateId(); + draft.CreatedAt = systemClock.UtcNow; + draft.Id = identityGenerator.GenerateId(); draft.IsLatest = true; draft.IsPublished = false; @@ -176,24 +172,27 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher { var draft = definition; var definitionId = definition.DefinitionId; - var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId }; - var lastVersion = await _workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken); + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId + }; + var lastVersion = await workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken); draft.Version = draft.Id == lastVersion?.Id ? lastVersion.Version : lastVersion?.Version + 1 ?? 1; draft.IsLatest = true; draft = Initialize(draft); - await _workflowDefinitionStore.SaveAsync(draft, cancellationToken); + await workflowDefinitionStore.SaveAsync(draft, cancellationToken); if (lastVersion is null) { - await _notificationSender.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken); + await notificationSender.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken); } if (lastVersion is { IsPublished: true, IsLatest: true }) { lastVersion.IsLatest = false; - await _workflowDefinitionStore.SaveAsync(lastVersion, cancellationToken); + await workflowDefinitionStore.SaveAsync(lastVersion, cancellationToken); } return draft; @@ -202,10 +201,10 @@ public class WorkflowDefinitionPublisher : IWorkflowDefinitionPublisher private WorkflowDefinition Initialize(WorkflowDefinition definition) { if (definition.Id == null!) - definition.Id = _identityGenerator.GenerateId(); + definition.Id = identityGenerator.GenerateId(); if (definition.DefinitionId == null!) - definition.DefinitionId = _identityGenerator.GenerateId(); + definition.DefinitionId = identityGenerator.GenerateId(); if (definition.Version == 0) definition.Version = 1; diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs index b8c4b7efa..ed43c2bad 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs @@ -1,27 +1,20 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities; using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Notifications; using Elsa.Workflows.Management.Requests; namespace Elsa.Workflows.Management.Services; /// -public class WorkflowValidator : IWorkflowValidator +public class WorkflowValidator(INotificationSender notificationSender) : IWorkflowValidator { - private readonly IRequestSender _requestSender; - - /// - /// Initializes a new instance of the class. - /// - public WorkflowValidator(IRequestSender requestSender) - { - _requestSender = requestSender; - } - /// public async Task> ValidateAsync(Workflow workflow, CancellationToken cancellationToken = default) { - var responses = await _requestSender.SendAsync(new ValidateWorkflowRequest(workflow), cancellationToken); - return responses.SelectMany(r => r.ValidationErrors).ToList(); + var validationErrors = new List(); + var notification = new WorkflowDefinitionValidating(workflow, validationErrors); + await notificationSender.SendAsync(notification, cancellationToken); + return validationErrors; } } \ No newline at end of file From a9800223514874d0aece504b871a2d3fc40b8e97 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Jan 2025 13:58:11 +0100 Subject: [PATCH 069/166] Refactor exception handling and activity status logic Refactored `ExceptionHandlingMiddleware` to improve readability and modularity by splitting responsibilities into smaller methods. Replaced `GetAggregateStatus` with direct use of `source.Status` for significant performance improvement when a large number of activity instances are involved. Updated null-checks for clarity and fixed inconsistent usage of default values. --- .../Activities/ExceptionHandlingMiddleware.cs | 63 ++++++++++--------- .../DefaultActivityExecutionMapper.cs | 19 ++---- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs index b26210782..38d5963a2 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs @@ -1,4 +1,5 @@ using Elsa.Common; +using Elsa.Extensions; using Elsa.Workflows.Models; using Elsa.Workflows.Pipelines.ActivityExecution; using Elsa.Workflows.State; @@ -20,45 +21,47 @@ public static class ExceptionHandlingMiddlewareExtensions /// /// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state. /// -public class ExceptionHandlingMiddleware : IActivityExecutionMiddleware +public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncidentStrategyResolver incidentStrategyResolver, ISystemClock systemClock, ILogger logger) + : IActivityExecutionMiddleware { - private readonly ActivityMiddlewareDelegate _next; - private readonly IIncidentStrategyResolver _incidentStrategyResolver; - private readonly ISystemClock _systemClock; - private readonly ILogger _logger; - - /// - /// Constructor. - /// - public ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncidentStrategyResolver incidentStrategyResolver, ISystemClock systemClock, ILogger logger) - { - _next = next; - _incidentStrategyResolver = incidentStrategyResolver; - _systemClock = systemClock; - _logger = logger; - } - /// public async ValueTask InvokeAsync(ActivityExecutionContext context) { try { - await _next(context); + await next(context); } catch (Exception e) { - _logger.LogWarning(e, "An exception was caught from a downstream middleware component"); - context.Exception = e; - context.TransitionTo(ActivityStatus.Faulted); - - var activity = context.Activity; - var exceptionState = ExceptionState.FromException(e); - var now = _systemClock.UtcNow; - var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now); - context.WorkflowExecutionContext.Incidents.Add(incident); - - var strategy = await _incidentStrategyResolver.ResolveStrategyAsync(context); - strategy.HandleIncident(context); + logger.LogWarning(e, "An exception was caught from a downstream middleware component"); + LogExceptionAndTransition(context, e); + FaultAncestors(context); + await HandleIncidentAsync(context); } } + + private void LogExceptionAndTransition(ActivityExecutionContext context, Exception e) + { + context.Exception = e; + context.TransitionTo(ActivityStatus.Faulted); + var activity = context.Activity; + var exceptionState = ExceptionState.FromException(e); + var now = systemClock.UtcNow; + var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now); + context.WorkflowExecutionContext.Incidents.Add(incident); + } + + private async Task HandleIncidentAsync(ActivityExecutionContext context) + { + var strategy = await incidentStrategyResolver.ResolveStrategyAsync(context); + strategy.HandleIncident(context); + } + + private static void FaultAncestors(ActivityExecutionContext context) + { + var ancestors = context.GetAncestors(); + + foreach (var ancestor in ancestors) + ancestor.TransitionTo(ActivityStatus.Faulted); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs index 0e1dd85de..7278e4fa1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs @@ -115,7 +115,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper ActivityTypeVersion = source.Activity.Version, StartedAt = source.StartedAt, HasBookmarks = source.Bookmarks.Any(), - Status = GetAggregateStatus(source), + Status = source.Status, CompletedAt = source.CompletedAt }; } @@ -220,20 +220,9 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper } } - private static ActivityStatus GetAggregateStatus(ActivityExecutionContext context) - { - // If any child activity is faulted, the aggregate status is faulted. - var descendantContexts = context.GetDescendants().ToList(); - - if (descendantContexts.Any(x => x.Status == ActivityStatus.Faulted)) - return ActivityStatus.Faulted; - - return context.Status; - } - private static IDictionary GetPayload(ActivityExecutionContext source) { - var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default; + var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : null; var payload = new Dictionary(); if (outcomes != null) @@ -256,13 +245,13 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper var cachedValue = activity.GetOutput(expressionExecutionContext, x.Name); - if (cachedValue != default) + if (cachedValue != null) return cachedValue; if (x.ValueGetter(activity) is Output output && source.TryGet(output.MemoryBlockReference(), out var outputValue)) return outputValue; - return default; + return null; }); return outputs; From 7e058ddf15a3f43327a61dd05f15ea2ebebd2e37 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Jan 2025 17:44:31 +0100 Subject: [PATCH 070/166] Refactor workflow context and execution handling. Optimized activity execution context management by introducing parent-child relationships and improving immutability. Adjusted several APIs to enhance clarity, performance, and maintainability, including the use of `AsReadOnly` collections and removal of redundant code. --- .../Flowchart/Activities/Flowchart.cs | 12 +++---- .../ActivityExecutionContext.Complete.cs | 6 ++-- ...ivityExecutionContext.ExecutionLogEntry.cs | 2 +- .../Contexts/ActivityExecutionContext.cs | 15 +++++++-- .../Contexts/WorkflowExecutionContext.cs | 2 +- .../ActivityExecutionContextExtensions.cs | 2 +- .../Models/ActivityNode.cs | 32 +++++++++++++++---- .../Services/ActivityVisitor.cs | 6 ++-- .../Services/QueueBasedActivityScheduler.cs | 2 +- 9 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 4ee602a37..5824451ac 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -31,9 +31,7 @@ public class Flowchart : Container /// /// The activity to execute when the flowchart starts. /// - [Port] - [Browsable(false)] - public IActivity? Start { get; set; } + [Port] [Browsable(false)] public IActivity? Start { get; set; } /// /// A list of connections between activities. @@ -98,8 +96,8 @@ public class Flowchart : Container { var workflowExecutionContext = context.WorkflowExecutionContext; var activityIds = Activities.Select(x => x.Id).ToList(); - var descendantContexts = context.GetDescendents().Where(x => x.ParentActivityExecutionContext == context).ToList(); - var activityExecutionContexts = descendantContexts.Where(x => activityIds.Contains(x.Activity.Id)).ToList(); + var descendantContexts = context.GetDescendents().Where(x => x.ParentActivityExecutionContext == context); + var hasRunningActivityInstances = descendantContexts.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem => { @@ -117,8 +115,6 @@ public class Flowchart : Container return ancestors.Any(x => x == context); }); - var hasRunningActivityInstances = activityExecutionContexts.Any(x => x.Status == ActivityStatus.Running); - return hasRunningActivityInstances || hasPendingWork; } @@ -186,7 +182,7 @@ public class Flowchart : Container var executionCount = scope.GetExecutionCount(activity); var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount); - if (haveInboundActivitiesExecuted) + if (haveInboundActivitiesExecuted) await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync); } else diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs index 978c64d91..1a278d1db 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs @@ -9,7 +9,7 @@ public partial class ActivityExecutionContext /// /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. /// - public async ValueTask CompleteActivityAsync(object? result = default) + public async ValueTask CompleteActivityAsync(object? result = null) { var outcomes = result as Outcomes; @@ -28,8 +28,8 @@ public partial class ActivityExecutionContext return; // Cancel any non-completed child activities. - var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this && x.CanCancelActivity()).ToList(); - + var childContexts = Children.Where(x => x.CanCancelActivity()).ToList(); + foreach (var childContext in childContexts) await childContext.CancelActivityAsync(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs index e05a262c6..260733811 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.ExecutionLogEntry.cs @@ -13,7 +13,7 @@ public partial class ActivityExecutionContext /// 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. /// Returns the created . - public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = default, string? source = default, object? payload = default) + public WorkflowExecutionLogEntry AddExecutionLogEntry(string eventName, string? message = null, string? source = null, object? payload = null) { var logEntry = new WorkflowExecutionLogEntry( Id, diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 5393a9f80..099f71c40 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -21,6 +21,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable private readonly ISystemClock _systemClock; private readonly List _bookmarks = []; private long _executionCount; + private ActivityExecutionContext? _parentActivityExecutionContext; /// /// Initializes a new instance of the class. @@ -39,7 +40,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable { _systemClock = systemClock; WorkflowExecutionContext = workflowExecutionContext; - ParentActivityExecutionContext = parentActivityExecutionContext; + _parentActivityExecutionContext = parentActivityExecutionContext; ExpressionExecutionContext = expressionExecutionContext; Activity = activity; ActivityDescriptor = activityDescriptor; @@ -84,7 +85,15 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// The parent activity execution context, if any. /// - public ActivityExecutionContext? ParentActivityExecutionContext { get; internal set; } + public ActivityExecutionContext? ParentActivityExecutionContext + { + get => _parentActivityExecutionContext; + internal set + { + _parentActivityExecutionContext = value; + _parentActivityExecutionContext?.Children.Add(this); + } + } /// /// The expression execution context. @@ -159,6 +168,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// As of tool version 3.0, all activity Ids are already unique, so there's no need to construct a hierarchical ID public string NodeId => ActivityNode.NodeId; + + public ISet Children { get; } = new HashSet(); /// /// A list of bookmarks created by the current activity. diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 545ba3bbc..56788e661 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -346,7 +346,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// public IReadOnlyCollection ActivityExecutionContexts { - get => _activityExecutionContexts.ToList(); + get => _activityExecutionContexts.AsReadOnly(); internal set => _activityExecutionContexts = value.ToList(); } diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 090cff8b8..edc7dafd9 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -202,7 +202,7 @@ public static partial class ActivityExecutionContextExtensions /// public static IEnumerable GetDescendents(this ActivityExecutionContext context) { - var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); + var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); foreach (var child in children) { diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs index 44e39ac00..3768857cb 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs @@ -5,6 +5,10 @@ namespace Elsa.Workflows.Models; /// public class ActivityNode { + private readonly List _parents = new(); + private readonly List _children = new(); + private string? _nodeId; + /// /// Initializes a new instance of the class. /// @@ -23,8 +27,13 @@ public class ActivityNode { get { - var ancestorIds = Ancestors().Reverse().Select(x => x.Activity.Id).ToList(); - return ancestorIds.Any() ? $"{string.Join(":", ancestorIds)}:{Activity.Id}" : Activity.Id; + if (_nodeId == null) + { + var ancestorIds = Ancestors().Reverse().Select(x => x.Activity.Id).ToList(); + _nodeId = ancestorIds.Any() ? $"{string.Join(":", ancestorIds)}:{Activity.Id}" : Activity.Id; + } + + return _nodeId; } } @@ -41,12 +50,23 @@ public class ActivityNode /// /// Gets the parents of this node. /// - public ICollection Parents { get; set; } = new List(); - + public IReadOnlyCollection Parents => _parents.AsReadOnly(); + /// /// Gets the children of this node. /// - public ICollection Children { get; set; } = new List(); + public ICollection Children => _children.AsReadOnly(); + + public void AddParent(ActivityNode parent) + { + _parents.Add(parent); + _nodeId = null; + } + + public void AddChild(ActivityNode child) + { + _children.Add(child); + } /// /// Gets the descendants of this node. @@ -85,7 +105,7 @@ public class ActivityNode /// Gets the siblings of this node. /// public IEnumerable Siblings() => Parents.SelectMany(parent => parent.Children); - + /// /// Gets the siblings and cousins of this node. /// diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityVisitor.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityVisitor.cs index fcdb66519..c95c7beec 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityVisitor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityVisitor.cs @@ -71,12 +71,12 @@ public class ActivityVisitor : IActivityVisitor if (childNode == null) { - childNode = new ActivityNode(activity, activityPort.PortName); + childNode = new(activity, activityPort.PortName); collectedNodes.Add(childNode); } - childNode.Parents.Add(pair.Node); - pair.Node.Children.Add(childNode); + childNode.AddParent(pair.Node); + pair.Node.AddChild(childNode); collectedActivities.Add(activity); await VisitRecursiveAsync((childNode, activity), visitorContext, cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Core/Services/QueueBasedActivityScheduler.cs b/src/modules/Elsa.Workflows.Core/Services/QueueBasedActivityScheduler.cs index b13584b05..47b56e7fb 100644 --- a/src/modules/Elsa.Workflows.Core/Services/QueueBasedActivityScheduler.cs +++ b/src/modules/Elsa.Workflows.Core/Services/QueueBasedActivityScheduler.cs @@ -21,7 +21,7 @@ public class QueueBasedActivityScheduler : IActivityScheduler public ActivityWorkItem Take() => _queue.Dequeue(); /// - public IEnumerable List() => _queue.ToList(); + public IEnumerable List() => _queue; /// public bool Any(Func predicate) => _queue.Any(predicate); From a49bf0c05fb467de6460c81df70eee49f5ac5eb2 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Jan 2025 18:07:14 +0100 Subject: [PATCH 071/166] Refactor descendant context check in Flowchart activity. Replaced usage of `GetDescendents` with `Children` property for better clarity and efficiency when checking running activity instances. This simplifies the logic and aligns with the existing structure of activity context handling. --- .../Activities/Flowchart/Activities/Flowchart.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 5824451ac..022ff7a9e 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -96,8 +96,8 @@ public class Flowchart : Container { var workflowExecutionContext = context.WorkflowExecutionContext; var activityIds = Activities.Select(x => x.Id).ToList(); - var descendantContexts = context.GetDescendents().Where(x => x.ParentActivityExecutionContext == context); - var hasRunningActivityInstances = descendantContexts.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); + var children = context.Children; + var hasRunningActivityInstances = children.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem => { From d31661926d00a302dd4d6639ea345e82988303dc Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Jan 2025 18:29:13 +0100 Subject: [PATCH 072/166] Simplify activity context handling in workflows. Replaced `GetActiveChildren` with a direct `Children` property in `Flowchart.cs` to streamline logic. Removed redundant `GetDescendents`, `GetActiveChildren`, and `GetChildren` methods from `ActivityExecutionContextExtensions.cs`. Also updated GitHub workflows to allow performance-related branches. --- .github/workflows/packages.yml | 1 + .../Flowchart/Activities/Flowchart.cs | 2 +- .../ActivityExecutionContextExtensions.cs | 44 ------------------- 3 files changed, 2 insertions(+), 45 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index f594544c1..df4fbec7c 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -5,6 +5,7 @@ on: branches: - 'main' - 'bug/*' + - 'perf/*' release: types: [ prereleased, published ] env: diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 022ff7a9e..3fa0921ea 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -227,7 +227,7 @@ public class Flowchart : Container if (!hasPendingWork) { - var hasFaultedActivities = context.GetActiveChildren().Any(x => x.Status == ActivityStatus.Faulted); + var hasFaultedActivities = context.Children.Any(x => x.Status == ActivityStatus.Faulted); if (!hasFaultedActivities) { diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index edc7dafd9..739b0d8eb 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -197,50 +197,6 @@ public static partial class ActivityExecutionContextExtensions } } - /// - /// Returns a flattened list of the current context's descendants. - /// - public static IEnumerable GetDescendents(this ActivityExecutionContext context) - { - var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); - - foreach (var child in children) - { - yield return child; - - foreach (var descendent in GetDescendents(child)) - yield return descendent; - } - } - - /// - /// Returns a flattened list of the current context's immediate active children. - /// - public static IEnumerable GetActiveChildren(this ActivityExecutionContext context) => - context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); - - /// - /// Returns a flattened list of the current context's immediate children. - /// - public static IEnumerable GetChildren(this ActivityExecutionContext context) => - context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); - - /// - /// Returns a flattened list of the current context's descendants. - /// - public static IEnumerable GetDescendants(this ActivityExecutionContext context) - { - var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); - - foreach (var child in children) - { - yield return child; - - foreach (var descendant in child.GetDescendants()) - yield return descendant; - } - } - /// /// Send a signal up the current hierarchy of ancestors. /// From a7360ed9223187115174124ed17d0c56df78c674 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 19:11:31 +0100 Subject: [PATCH 073/166] Add option to disable variable copying in Jint engine Introduce a `DisableVariableCopying` option to improve performance by preventing workflow variables from being copied into the Jint engine or back into the workflow context. Updated related logic to honor this setting and ensure compatibility with existing behavior. --- src/apps/Elsa.Server.Web/Program.cs | 2 ++ .../Extensions/EngineExtensions.cs | 14 +++++----- .../Handlers/ConfigureEngineWithVariables.cs | 27 +++++++++++++++---- .../Elsa.JavaScript/Options/JintOptions.cs | 6 +++++ .../ExpressionExecutionContextExtensions.cs | 8 +++--- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 3f7f5f733..1e4778c0a 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -89,6 +89,7 @@ const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; +const bool disableVariableCopying = true; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; @@ -355,6 +356,7 @@ services { options.AllowClrAccess = true; options.DisableWrappers = disableVariableWrappers; + options.DisableVariableCopying = disableVariableCopying; options.RegisterType(); options.ConfigureEngine(engine => { diff --git a/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs b/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs index b12b9f449..7219e79d5 100644 --- a/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs +++ b/src/modules/Elsa.JavaScript/Extensions/EngineExtensions.cs @@ -24,12 +24,12 @@ public static class EngineExtensions internal static void SyncVariablesContainer(this Engine engine, IOptions options, string name, object? value) { - if (!options.Value.DisableWrappers) - { - // To ensure both variable accessor syntaxes work, we need to update the variables container in the engine as well as the context to keep them in sync. - var variablesContainer = (IDictionary)engine.GetValue("variables").ToObject()!; - variablesContainer[name] = ObjectConverterHelper.ProcessVariableValue(engine, value); - engine.SetValue("variables", variablesContainer); - } + if (options.Value.DisableWrappers || options.Value.DisableVariableCopying) + return; + + // To ensure both variable accessor syntaxes work, we need to update the variables container in the engine as well as the context to keep them in sync. + var variablesContainer = (IDictionary)engine.GetValue("variables").ToObject()!; + variablesContainer[name] = ObjectConverterHelper.ProcessVariableValue(engine, value); + engine.SetValue("variables", variablesContainer); } } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs index 417c4f5eb..3e1d4f6c9 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariables.cs @@ -1,4 +1,5 @@ using System.Dynamic; +using System.Text.RegularExpressions; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Extensions; @@ -8,7 +9,6 @@ using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities; using JetBrains.Annotations; -using Jint; using Jint.Native; using Microsoft.Extensions.Options; @@ -18,12 +18,14 @@ namespace Elsa.JavaScript.Handlers; /// A handler that configures the Jint engine with workflow variables. /// [UsedImplicitly] -public class ConfigureEngineWithVariables(IOptions options) : INotificationHandler, INotificationHandler +public partial class ConfigureEngineWithVariables(IOptions options) : INotificationHandler, INotificationHandler { + private bool IsEnabled => options.Value is { DisableWrappers: false, DisableVariableCopying: false }; + /// public Task HandleAsync(EvaluatingJavaScript notification, CancellationToken cancellationToken) { - if (options.Value.DisableWrappers) + if (!IsEnabled) return Task.CompletedTask; CopyVariablesIntoEngine(notification); @@ -32,7 +34,7 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti public Task HandleAsync(EvaluatedJavaScript notification, CancellationToken cancellationToken) { - if (options.Value.DisableWrappers) + if (!IsEnabled) return Task.CompletedTask; CopyVariablesIntoWorkflowExecutionContext(notification); @@ -60,7 +62,8 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti { var engine = notification.Engine; var context = notification.Context; - var variableNames = context.GetVariableNamesInScope().FilterInvalidVariableNames().ToList(); + var expression = notification.Expression; + var variableNames = GetUsedVariableNames(context, expression).ToList(); var variablesContainer = (IDictionary)new ExpandoObject(); foreach (var variableName in variableNames) @@ -73,6 +76,17 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti engine.SetValue("variables", variablesContainer); } + private IEnumerable GetUsedVariableNames(ExpressionExecutionContext context, string expression) + { + var variableNames = context.GetVariableNamesInScope().FilterInvalidVariableNames(); + + var variableNamesInScript = ExtractVariableNamesRegex().Matches(expression) + .Select(m => m.Groups[1].Value) + .ToList(); + + return variableNames.Where(x => variableNamesInScript.Contains(x)); + } + private IEnumerable GetInputNames(ExpressionExecutionContext context) { var activityExecutionContext = context.TryGetActivityExecutionContext(out var aec) ? aec : null; @@ -95,4 +109,7 @@ public class ConfigureEngineWithVariables(IOptions options) : INoti activityExecutionContext = activityExecutionContext.ParentActivityExecutionContext; } } + + [GeneratedRegex(@"variables\.(\w+)(?:\.\w+)*")] + private static partial Regex ExtractVariableNamesRegex(); } \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Options/JintOptions.cs b/src/modules/Elsa.JavaScript/Options/JintOptions.cs index 2cff888b7..ad611cc72 100644 --- a/src/modules/Elsa.JavaScript/Options/JintOptions.cs +++ b/src/modules/Elsa.JavaScript/Options/JintOptions.cs @@ -47,6 +47,12 @@ public class JintOptions /// public bool DisableWrappers { get; set; } + /// + /// Disables copying workflow variables into the Jint engine and copying them back into the workflow execution context. + /// Disabling this option will increase performance but will also prevent you from accessing workflow variables from within JavaScript expressions using the variables.MyVariable syntax. + /// + public bool DisableVariableCopying { get; set; } + /// /// Configures the Jint engine options. /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index a88e84498..7382b84b9 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -143,7 +143,7 @@ public static class ExpressionExecutionContextExtensions var existingVariable = context.GetVariable(name, localScopeOnly: true); if (existingVariable != null) - throw new Exception($"Variable {name} already exists in the context."); + throw new($"Variable {name} already exists in the context."); var variable = new Variable(name, value) { @@ -464,7 +464,7 @@ public static class ExpressionExecutionContextExtensions foreach (var output in activityDescriptor.Outputs) { var outputPascalName = output.Name.Pascalize(); - yield return new ActivityOutputs(activity.Id, activityIdPascalName, [ + yield return new(activity.Id, activityIdPascalName, [ outputPascalName ]); } @@ -508,7 +508,7 @@ public static class ExpressionExecutionContextExtensions { var inputPascalName = inputEntry.Key.Pascalize(); var inputValue = inputEntry.Value; - yield return new WorkflowInput(inputPascalName, inputValue); + yield return new(inputPascalName, inputValue); } } else @@ -522,7 +522,7 @@ public static class ExpressionExecutionContextExtensions var variable = variableBlockMetadata.Variable; var variablePascalName = variable.Name.Pascalize(); - yield return new WorkflowInput(variablePascalName, block.Value); + yield return new(variablePascalName, block.Value); } } } From 3262e7493fe1d7e8b751291415d7ed0ad9eceece Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 19:29:56 +0100 Subject: [PATCH 074/166] Enable variable copying in Elsa.Server.Web configuration Updated the `disableVariableCopying` flag to `false` in `Program.cs`. This change allows variables to be copied, potentially addressing scenarios where variable duplication is needed. Ensure to test for any side effects this might introduce. --- src/apps/Elsa.Server.Web/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 1e4778c0a..1f1c393d4 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -89,7 +89,7 @@ const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; -const bool disableVariableCopying = true; +const bool disableVariableCopying = false; var builder = WebApplication.CreateBuilder(args); var services = builder.Services; From 33a2fb7afe9e2654c57a9d2a5780e870a8236c2a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 19:52:30 +0100 Subject: [PATCH 075/166] Optimize ActivityOutputRegister. Enhanced `ActivityOutputRegister` with dictionary-based lookups for improved performance and added unique key generation methods to efficiently retrieve outputs. --- .../Models/ActivityOutputRegister.cs | 29 +++++++++++++------ .../Models/PropertyDescriptor.cs | 8 ++--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index 8b120af83..b6cd8b8fd 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -5,7 +5,9 @@ namespace Elsa.Workflows.Models; /// public class ActivityOutputRegister { - private readonly ICollection _records = new List(); + private readonly List _records = new(); + private readonly Dictionary _recordsByActivityIdAndOutputName = new(); + private readonly Dictionary _recordsByActivityInstanceIdAndOutputName = new(); /// /// The default output name. @@ -19,7 +21,7 @@ public class ActivityOutputRegister /// The output value. public void Record(ActivityExecutionContext activityExecutionContext, object? outputValue) { - Record(activityExecutionContext, default, outputValue); + Record(activityExecutionContext, null, outputValue); } /// @@ -39,13 +41,15 @@ public class ActivityOutputRegister // Inspect the output descriptor to see if the specified output name matches any PropertyInfo's name. // If so, use that descriptor's name instead. var outputDescriptor = activityExecutionContext.ActivityDescriptor.Outputs.FirstOrDefault(x => x.PropertyInfo?.Name == outputName); - + if (outputDescriptor != null) outputName = outputDescriptor.Name; var record = new ActivityOutputRecord(containerId, activityId, activityInstanceId, outputName, outputValue); _records.Add(record); + _recordsByActivityIdAndOutputName[CreateActivityIdLookupKey(activityId, outputName)] = record; + _recordsByActivityInstanceIdAndOutputName[CreateActivityInstanceIdLookupKey(activityInstanceId, outputName)] = record; } /// @@ -59,10 +63,12 @@ public class ActivityOutputRegister /// The activity ID. /// Name of the output. /// The output value. - public object? FindOutputByActivityId(string activityId, string? outputName = default) + public object? FindOutputByActivityId(string activityId, string? outputName = null) { - var record = _records.LastOrDefault(x => x.ActivityId == activityId && x.OutputName == (outputName ?? DefaultOutputName)); - return record?.Value; + var key = CreateActivityIdLookupKey(activityId, outputName ?? DefaultOutputName); + return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var record) + ? null + : record.Value; } /// @@ -71,9 +77,14 @@ public class ActivityOutputRegister /// The activity instance ID. /// /// The output value. - public object? FindOutputByActivityInstanceId(string activityInstanceId, string? outputName = default) + public object? FindOutputByActivityInstanceId(string activityInstanceId, string? outputName = null) { - var record = _records.LastOrDefault(x => x.ActivityInstanceId == activityInstanceId && x.OutputName == (outputName ?? DefaultOutputName)); - return record?.Value; + var key = $"{activityInstanceId}:{outputName ?? DefaultOutputName}"; + return !_recordsByActivityInstanceIdAndOutputName.TryGetValue(key, out var record) + ? null + : record.Value; } + + private string CreateActivityIdLookupKey(string activityId, string outputName) => $"{activityId}:{outputName}"; + private string CreateActivityInstanceIdLookupKey(string activityInstanceId, string? outputName) => $"{activityInstanceId}:{outputName ?? DefaultOutputName}"; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs index dc6e8ec54..24ed114b9 100644 --- a/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/PropertyDescriptor.cs @@ -11,13 +11,13 @@ public abstract class PropertyDescriptor /// /// The name. /// - public string Name { get; set; } = default!; + public string Name { get; set; } = null!; /// /// The .NET type. /// [JsonPropertyName("typeName")] - public Type Type { get; set; } = default!; + public Type Type { get; set; } = null!; /// /// The user friendly name of the input. Used by UI tools. @@ -53,13 +53,13 @@ public abstract class PropertyDescriptor /// Returns the value of the input property for the specified activity. /// [JsonIgnore] - public Func ValueGetter { get; set; } = default!; + public Func ValueGetter { get; set; } = null!; /// /// Sets the value of the input property for the specified activity. /// [JsonIgnore] - public Action ValueSetter { get; set; } = default!; + public Action ValueSetter { get; set; } = null!; /// /// The source of the property, if any. From dd39ca8d847912ad789b01e9c1de8bf4569f7e17 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 20:10:54 +0100 Subject: [PATCH 076/166] Refactor variable handling and activity output registration. Replaced default value assignments with null for clarity and simplicity. Refactored ActivityOutputRegister to optimize record storage and retrieval using grouped dictionary entries instead of flat lists. Adjusted related methods to improve performance and maintain consistency. --- .../ExpressionExecutionContextExtensions.cs | 16 ++++----- .../Models/ActivityOutputRegister.cs | 35 ++++++++++++------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index 7382b84b9..70f5381d2 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -119,7 +119,7 @@ public static class ExpressionExecutionContextExtensions public static Variable? GetVariable(this ExpressionExecutionContext context, string name, bool localScopeOnly = false) { var block = context.GetVariableBlock(name, localScopeOnly); - return block?.Metadata is VariableBlockMetadata metadata ? metadata.Variable : default; + return block?.Metadata is VariableBlockMetadata metadata ? metadata.Variable : null; } private static MemoryBlock? GetVariableBlock(this ExpressionExecutionContext context, string name, bool localScopeOnly = false) @@ -138,7 +138,7 @@ public static class ExpressionExecutionContextExtensions /// Creates a named variable in the context. /// public static Variable CreateVariable(this ExpressionExecutionContext context, string name, T? value, Type? storageDriverType = null, - Action? configure = default) + Action? configure = null) { var existingVariable = context.GetVariable(name, localScopeOnly: true); @@ -173,7 +173,7 @@ public static class ExpressionExecutionContextExtensions /// /// Sets the value of a named variable in the context. /// - public static Variable SetVariable(this ExpressionExecutionContext context, string name, T? value, Action? configure = default) + public static Variable SetVariable(this ExpressionExecutionContext context, string name, T? value, Action? configure = null) { var variable = context.GetVariable(name); @@ -193,7 +193,7 @@ public static class ExpressionExecutionContextExtensions /// /// Sets the output to the specified value. /// - public static void Set(this ExpressionExecutionContext context, Output? output, object? value, Action? configure = default) + public static void Set(this ExpressionExecutionContext context, Output? output, object? value, Action? configure = null) { if (output != null) { @@ -412,11 +412,11 @@ public static class ExpressionExecutionContextExtensions // Otherwise, return the input. var workflowExecutionContext = context.GetWorkflowExecutionContext(); var input = workflowExecutionContext.Input; - return input.TryGetValue(name, out var value) ? value : default; + return input.TryGetValue(name, out var value) ? value : null; } /// - /// Returns the value of the specified input. + /// Returns the value of the specified output. /// /// /// The ID or name of the activity. @@ -433,9 +433,9 @@ public static class ExpressionExecutionContextExtensions throw new InvalidOperationException("Activity not found."); var outputRegister = workflowExecutionContext.GetActivityOutputRegister(); - var outputRecordCandidates = outputRegister.FindMany(x => x.ActivityId == activity.Id && x.OutputName == outputName).ToList(); + var outputRecordCandidates = outputRegister.FindMany(activity.Id, outputName); var containerIds = activityExecutionContext.GetAncestors().Select(x => x.Id).ToList(); - var filteredOutputRecordCandidates = outputRecordCandidates.Where(x => containerIds.Contains(x.ContainerId)).ToList(); + var filteredOutputRecordCandidates = outputRecordCandidates.Where(x => containerIds.Contains(x.ContainerId)); var outputRecord = filteredOutputRecordCandidates.FirstOrDefault(); return outputRecord?.Value; } diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index b6cd8b8fd..5a3be3937 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -5,8 +5,7 @@ namespace Elsa.Workflows.Models; /// public class ActivityOutputRegister { - private readonly List _records = new(); - private readonly Dictionary _recordsByActivityIdAndOutputName = new(); + private readonly Dictionary> _recordsByActivityIdAndOutputName = new(); private readonly Dictionary _recordsByActivityInstanceIdAndOutputName = new(); /// @@ -46,16 +45,28 @@ public class ActivityOutputRegister outputName = outputDescriptor.Name; var record = new ActivityOutputRecord(containerId, activityId, activityInstanceId, outputName, outputValue); - - _records.Add(record); - _recordsByActivityIdAndOutputName[CreateActivityIdLookupKey(activityId, outputName)] = record; + _recordsByActivityInstanceIdAndOutputName[CreateActivityInstanceIdLookupKey(activityInstanceId, outputName)] = record; + + var scopedRecordsKey = CreateActivityIdLookupKey(activityId, outputName); + + if(!_recordsByActivityIdAndOutputName.TryGetValue(scopedRecordsKey, out var scopedRecords)) + { + scopedRecords = new(); + _recordsByActivityIdAndOutputName[scopedRecordsKey] = scopedRecords; + } + + scopedRecords.Add(record); } /// - /// Finds all output records matching the specified predicate. + /// Finds all output records for the specified activity ID and output name. /// - public IEnumerable FindMany(Func predicate) => _records.Where(predicate); + public IEnumerable FindMany(string activityId, string? outputName = null) + { + var key = CreateActivityIdLookupKey(activityId, outputName); + return _recordsByActivityIdAndOutputName.TryGetValue(key, out var records) ? records : Enumerable.Empty(); + } /// /// Gets the output value for the specified activity ID. @@ -65,10 +76,10 @@ public class ActivityOutputRegister /// The output value. public object? FindOutputByActivityId(string activityId, string? outputName = null) { - var key = CreateActivityIdLookupKey(activityId, outputName ?? DefaultOutputName); - return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var record) + var key = CreateActivityIdLookupKey(activityId, outputName); + return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var records) ? null - : record.Value; + : records.FirstOrDefault(); } /// @@ -79,12 +90,12 @@ public class ActivityOutputRegister /// The output value. public object? FindOutputByActivityInstanceId(string activityInstanceId, string? outputName = null) { - var key = $"{activityInstanceId}:{outputName ?? DefaultOutputName}"; + var key = CreateActivityInstanceIdLookupKey(activityInstanceId, outputName); return !_recordsByActivityInstanceIdAndOutputName.TryGetValue(key, out var record) ? null : record.Value; } - private string CreateActivityIdLookupKey(string activityId, string outputName) => $"{activityId}:{outputName}"; + private string CreateActivityIdLookupKey(string activityId, string? outputName) => $"{activityId}:{outputName ?? DefaultOutputName}"; private string CreateActivityInstanceIdLookupKey(string activityInstanceId, string? outputName) => $"{activityInstanceId}:{outputName ?? DefaultOutputName}"; } \ No newline at end of file From 1ca8f2a4237d8ae6ab038204170a6dda3c36315e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 22:53:08 +0100 Subject: [PATCH 077/166] Fix null reference issue in ActivityOutputRegister. Updated the method to safely access the `Value` property when retrieving the first record, preventing potential null reference exceptions. This ensures more robust and error-free behavior when querying output records. --- .../Elsa.Workflows.Core/Models/ActivityOutputRegister.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index 5a3be3937..d4845cfea 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -79,7 +79,7 @@ public class ActivityOutputRegister var key = CreateActivityIdLookupKey(activityId, outputName); return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var records) ? null - : records.FirstOrDefault(); + : records.FirstOrDefault()?.Value; } /// From 0bb183f1aa1e227ece3cef5f8ab98347c106a8b3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Jan 2025 22:57:19 +0100 Subject: [PATCH 078/166] Fix spacing inconsistencies in ActivityOutputRegister.cs Resolved unnecessary whitespace issues and adjusted spacing around conditional statements to improve code readability and maintain consistency. These changes do not alter functionality but enhance the code's clarity and professional formatting. --- .../Models/ActivityOutputRegister.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index d4845cfea..ac72cf660 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -45,17 +45,17 @@ public class ActivityOutputRegister outputName = outputDescriptor.Name; var record = new ActivityOutputRecord(containerId, activityId, activityInstanceId, outputName, outputValue); - + _recordsByActivityInstanceIdAndOutputName[CreateActivityInstanceIdLookupKey(activityInstanceId, outputName)] = record; - + var scopedRecordsKey = CreateActivityIdLookupKey(activityId, outputName); - if(!_recordsByActivityIdAndOutputName.TryGetValue(scopedRecordsKey, out var scopedRecords)) + if (!_recordsByActivityIdAndOutputName.TryGetValue(scopedRecordsKey, out var scopedRecords)) { scopedRecords = new(); _recordsByActivityIdAndOutputName[scopedRecordsKey] = scopedRecords; } - + scopedRecords.Add(record); } @@ -77,8 +77,8 @@ public class ActivityOutputRegister public object? FindOutputByActivityId(string activityId, string? outputName = null) { var key = CreateActivityIdLookupKey(activityId, outputName); - return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var records) - ? null + return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var records) + ? null : records.FirstOrDefault()?.Value; } @@ -91,11 +91,11 @@ public class ActivityOutputRegister public object? FindOutputByActivityInstanceId(string activityInstanceId, string? outputName = null) { var key = CreateActivityInstanceIdLookupKey(activityInstanceId, outputName); - return !_recordsByActivityInstanceIdAndOutputName.TryGetValue(key, out var record) - ? null + return !_recordsByActivityInstanceIdAndOutputName.TryGetValue(key, out var record) + ? null : record.Value; } - + private string CreateActivityIdLookupKey(string activityId, string? outputName) => $"{activityId}:{outputName ?? DefaultOutputName}"; private string CreateActivityInstanceIdLookupKey(string activityInstanceId, string? outputName) => $"{activityInstanceId}:{outputName ?? DefaultOutputName}"; } \ No newline at end of file From fbd16f2d747a9e6c239d12fd32d3bdcfb5c92d4c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 13:54:37 +0100 Subject: [PATCH 079/166] Refactor workflow execution handling to improve performance --- .../Activities/BulkDispatchWorkflows.cs | 49 ++++++++++++------- .../Activities/DispatchWorkflow.cs | 35 ++++++++----- .../Activities/ExecuteWorkflow.cs | 40 +++++++++------ .../ResumeBulkDispatchWorkflowActivity.cs | 9 ++-- .../ResumeDispatchWorkflowActivity.cs | 18 +++++-- .../Handlers/ResumeExecuteWorkflowActivity.cs | 14 ++++-- 6 files changed, 109 insertions(+), 56 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index df335a24d..ca1ccd2ae 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -104,19 +104,11 @@ public class BulkDispatchWorkflows : Activity protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var waitForCompletion = WaitForCompletion.GetOrDefault(context); - var items = context.GetItemSource(Items); - var dispatchedInstancesCount = 0; - - await foreach (var item in items) - { - await DispatchChildWorkflowAsync(context, item); - dispatchedInstancesCount++; - } - - context.SetProperty(DispatchedInstancesCountKey, dispatchedInstancesCount); + var items = await context.GetItemSource(Items).ToListAsync(context.CancellationToken); + var count = items.Count; // If we need to wait for the child workflows to complete (if any), create a bookmark. - if (waitForCompletion && dispatchedInstancesCount > 0) + if (waitForCompletion && count > 0) { var workflowInstanceId = context.WorkflowExecutionContext.Id; var bookmarkOptions = new CreateBookmarkArgs @@ -125,23 +117,45 @@ public class BulkDispatchWorkflows : Activity Stimulus = new BulkDispatchWorkflowsStimulus(workflowInstanceId) { ParentInstanceId = context.WorkflowExecutionContext.Id, - ScheduledInstanceIdsCount = dispatchedInstancesCount + ScheduledInstanceIdsCount = count }, IncludeActivityInstanceId = false, AutoBurn = false, }; + + // Create bookmarks first. context.CreateBookmark(bookmarkOptions); + + // Dispatch workflows afterwards. + await DispatchWorkflowsAsync(); } else { // Otherwise, we can complete immediately. + await DispatchWorkflowsAsync(); await context.CompleteActivityWithOutcomesAsync("Done"); } + + return; + + async Task DispatchWorkflowsAsync() + { + foreach (var item in items) + await DispatchChildWorkflowAsync(context, item, waitForCompletion); + + context.SetProperty(DispatchedInstancesCountKey, count); + } } - private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context, object item) + private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context, object item, bool waitForCompletion) { var workflowDefinitionId = WorkflowDefinitionId.Get(context); + var workflowDefinitionService = context.GetRequiredService(); + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published); + + if (workflowGraph == null) + throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); + var parentInstanceId = context.WorkflowExecutionContext.Id; var input = Input.GetOrDefault(context) ?? new Dictionary(); var channelName = ChannelName.GetOrDefault(context); @@ -150,6 +164,9 @@ public class BulkDispatchWorkflows : Activity { ["ParentInstanceId"] = parentInstanceId }; + + if(waitForCompletion) + properties["WaitForCompletion"] = true; var itemDictionary = new Dictionary { @@ -168,12 +185,6 @@ public class BulkDispatchWorkflows : Activity var workflowDispatcher = context.GetRequiredService(); var identityGenerator = context.GetRequiredService(); var evaluator = context.GetRequiredService(); - var workflowDefinitionService = context.GetRequiredService(); - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published); - - if (workflowGraph == null) - throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found."); - var correlationId = CorrelationIdFunction != null ? await evaluator.EvaluateAsync(CorrelationIdFunction!, context.ExpressionExecutionContext, evaluatorOptions) : null; var instanceId = identityGenerator.GenerateId(); var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id) diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs index fa424c91d..4e579d60c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs @@ -72,7 +72,7 @@ public class DispatchWorkflow : Activity var waitForCompletion = WaitForCompletion.GetOrDefault(context); // Dispatch the child workflow. - var instanceId = await DispatchChildWorkflowAsync(context); + var instanceId = await DispatchChildWorkflowAsync(context, waitForCompletion); // If we need to wait for the child workflow to complete, create a bookmark. if (waitForCompletion) @@ -92,28 +92,39 @@ public class DispatchWorkflow : Activity } } - private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context) + private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context, bool waitForCompletion) { var workflowDefinitionId = WorkflowDefinitionId.Get(context); - var input = Input.GetOrDefault(context) ?? new Dictionary(); - var channelName = ChannelName.GetOrDefault(context); - - input["ParentInstanceId"] = context.WorkflowExecutionContext.Id; - - var correlationId = CorrelationId.GetOrDefault(context); - var workflowDispatcher = context.GetRequiredService(); - var identityGenerator = context.GetRequiredService(); var workflowDefinitionService = context.GetRequiredService(); var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken); if (workflowGraph == null) - throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found."); + throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); + + var input = Input.GetOrDefault(context) ?? new Dictionary(); + var channelName = ChannelName.GetOrDefault(context); + var parentInstanceId = context.WorkflowExecutionContext.Id; + var properties = new Dictionary + { + ["ParentInstanceId"] = parentInstanceId + }; + + if(waitForCompletion) + properties["WaitForCompletion"] = true; + + input["ParentInstanceId"] = parentInstanceId; + + var correlationId = CorrelationId.GetOrDefault(context); + var workflowDispatcher = context.GetRequiredService(); + var identityGenerator = context.GetRequiredService(); + var instanceId = identityGenerator.GenerateId(); var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id) { - ParentWorkflowInstanceId = context.WorkflowExecutionContext.Id, + ParentWorkflowInstanceId = parentInstanceId, Input = input, + Properties = properties, CorrelationId = correlationId, InstanceId = instanceId, }; diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs index 7aaf05a5b..cba602166 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs @@ -19,7 +19,7 @@ namespace Elsa.Workflows.Runtime.Activities; public class ExecuteWorkflow : Activity { /// - public ExecuteWorkflow([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public ExecuteWorkflow([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } @@ -31,7 +31,7 @@ public class ExecuteWorkflow : Activity Description = "The definition ID of the workflow to execute.", UIHint = InputUIHints.WorkflowDefinitionPicker )] - public Input WorkflowDefinitionId { get; set; } = default!; + public Input WorkflowDefinitionId { get; set; } = null!; /// /// The correlation ID to associate the workflow with. @@ -40,25 +40,25 @@ public class ExecuteWorkflow : Activity DisplayName = "Correlation ID", Description = "The correlation ID to associate the workflow with." )] - public Input CorrelationId { get; set; } = default!; + public Input CorrelationId { get; set; } = null!; /// /// The input to send to the workflow. /// [Input(Description = "The input to send to the workflow.")] - public Input?> Input { get; set; } = default!; + public Input?> Input { get; set; } = null!; /// /// True to wait for the child workflow to complete before completing this activity. If not set, the child workflow will be executed until it either completes or goes idle before this activity completes. /// [Input(Description = "Wait for the child workflow to complete before completing this activity.")] - public Input WaitForCompletion { get; set; } = default!; + public Input WaitForCompletion { get; set; } = null!; /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { - var result = await ExecuteWorkflowAsync(context); var waitForCompletion = WaitForCompletion.Get(context); + var result = await ExecuteWorkflowAsync(context, waitForCompletion); if(!waitForCompletion || result.Status == WorkflowStatus.Finished) { @@ -77,23 +77,35 @@ public class ExecuteWorkflow : Activity context.CreateBookmark(bookmarkOptions); } - private async ValueTask ExecuteWorkflowAsync(ActivityExecutionContext context) + private async ValueTask ExecuteWorkflowAsync(ActivityExecutionContext context, bool waitForCompletion) { var workflowDefinitionId = WorkflowDefinitionId.Get(context); - var input = Input.GetOrDefault(context) ?? new Dictionary(); - var correlationId = CorrelationId.GetOrDefault(context); - var workflowInvoker = context.GetRequiredService(); - var identityGenerator = context.GetRequiredService(); var workflowDefinitionService = context.GetRequiredService(); var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken); if (workflowGraph == null) - throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found."); - + throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); + + var parentInstanceId = context.WorkflowExecutionContext.Id; + var input = Input.GetOrDefault(context) ?? new Dictionary(); + var correlationId = CorrelationId.GetOrDefault(context); + var workflowInvoker = context.GetRequiredService(); + var identityGenerator = context.GetRequiredService(); + var properties = new Dictionary + { + ["ParentInstanceId"] = parentInstanceId + }; + + if(waitForCompletion) + properties["WaitForCompletion"] = true; + + input["ParentInstanceId"] = parentInstanceId; + var options = new RunWorkflowOptions { - ParentWorkflowInstanceId = context.WorkflowExecutionContext.Id, + ParentWorkflowInstanceId = parentInstanceId, Input = input, + Properties = properties, CorrelationId = correlationId, WorkflowInstanceId = identityGenerator.GenerateId() }; diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeBulkDispatchWorkflowActivity.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeBulkDispatchWorkflowActivity.cs index 6b683305b..7532d376e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeBulkDispatchWorkflowActivity.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeBulkDispatchWorkflowActivity.cs @@ -21,11 +21,12 @@ internal class ResumeBulkDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, if (workflowState.Status != WorkflowStatus.Finished) return; - var parentInstanceId = workflowState.Properties.TryGetValue("ParentInstanceId", out var parentInstanceIdValue) ? parentInstanceIdValue.ToString() : default; - - if (string.IsNullOrWhiteSpace(parentInstanceId)) + var waitForCompletion = workflowState.Properties.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue; + + if (!waitForCompletion) return; - + + var parentInstanceId = (string)workflowState.Properties["ParentInstanceId"]; var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(); var stimulus = new BulkDispatchWorkflowsStimulus(parentInstanceId); var stimulusHash = stimulusHasher.Hash(activityTypeName, stimulus); diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs index 607edf1f6..f1b52b181 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs @@ -2,7 +2,6 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Helpers; using Elsa.Workflows.Notifications; using Elsa.Workflows.Runtime.Activities; -using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stimuli; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -22,21 +21,32 @@ internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, ISti var workflowState = notification.WorkflowState; logger.LogDebug("Handling workflow executed notification for workflow {WorkflowInstanceId}", notification.WorkflowState.Id); - + if (workflowState.Status != WorkflowStatus.Finished) { logger.LogDebug("Workflow {WorkflowInstanceId} is not in a finished state. Skipping resumption of any blocking DispatchWorkflow activities", notification.WorkflowState.Id); return; } + var props = workflowState.Properties; + var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue; + + if (!waitForCompletion) + { + logger.LogDebug("Workflow {WorkflowInstanceId} does not have a WaitForCompletion property set to true. Skipping resumption of any blocking DispatchWorkflow activities", notification.WorkflowState.Id); + return; + } + + var parentInstanceId = (string) props["ParentInstanceId"]; var stimulus = new DispatchWorkflowStimulus(notification.WorkflowState.Id); var input = workflowState.Output; - + var bookmarkQueueItem = new NewBookmarkQueueItem { + WorkflowInstanceId = parentInstanceId, ActivityTypeName = ActivityTypeName, StimulusHash = stimulusHasher.Hash(ActivityTypeName, stimulus), - Options = new ResumeBookmarkOptions + Options = new() { Input = input } diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs index 256621e0b..d4bb203a6 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs @@ -2,7 +2,6 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Helpers; using Elsa.Workflows.Notifications; using Elsa.Workflows.Runtime.Activities; -using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stimuli; using JetBrains.Annotations; @@ -11,7 +10,7 @@ namespace Elsa.Workflows.Runtime.Handlers; /// /// Resumes any blocking activities when its child workflow completes. /// -[PublicAPI] +[UsedImplicitly] internal class ResumeExecuteWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler { private static readonly string ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); @@ -23,14 +22,23 @@ internal class ResumeExecuteWorkflowActivity(IBookmarkQueue bookmarkQueue, IStim if (workflowState.Status != WorkflowStatus.Finished) return; + var props = workflowState.Properties; + + var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue; + + if (!waitForCompletion) + return; + + var parentInstanceId = (string)props["ParentInstanceId"]; var stimulus = new ExecuteWorkflowStimulus(notification.WorkflowState.Id); var input = workflowState.Output; var bookmarkQueueItem = new NewBookmarkQueueItem { + WorkflowInstanceId = parentInstanceId, ActivityTypeName = ActivityTypeName, StimulusHash = stimulusHasher.Hash(ActivityTypeName, stimulus), - Options = new ResumeBookmarkOptions + Options = new() { Input = input } From 74a5c3cd87c6551464e1f9bc440d925358aa5398 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 13:58:50 +0100 Subject: [PATCH 080/166] Refactor workflow dispatching for clarity and efficiency Refactored the handling of child workflow execution, ensuring better readability and streamlining logic for dispatch operations. Added explicit tracking of dispatched instances and improved comments to support maintainability. --- .../Activities/BulkDispatchWorkflows.cs | 30 +++++++------------ .../Activities/DispatchWorkflow.cs | 11 ++++--- .../Activities/ExecuteWorkflow.cs | 21 ++++++------- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index ca1ccd2ae..8074be822 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -107,6 +107,13 @@ public class BulkDispatchWorkflows : Activity var items = await context.GetItemSource(Items).ToListAsync(context.CancellationToken); var count = items.Count; + // Dispatch the child workflows. + foreach (var item in items) + await DispatchChildWorkflowAsync(context, item, waitForCompletion); + + // Store the number of dispatched instances for tracking. + context.SetProperty(DispatchedInstancesCountKey, count); + // If we need to wait for the child workflows to complete (if any), create a bookmark. if (waitForCompletion && count > 0) { @@ -122,29 +129,14 @@ public class BulkDispatchWorkflows : Activity IncludeActivityInstanceId = false, AutoBurn = false, }; - - // Create bookmarks first. + context.CreateBookmark(bookmarkOptions); - - // Dispatch workflows afterwards. - await DispatchWorkflowsAsync(); } else { // Otherwise, we can complete immediately. - await DispatchWorkflowsAsync(); await context.CompleteActivityWithOutcomesAsync("Done"); } - - return; - - async Task DispatchWorkflowsAsync() - { - foreach (var item in items) - await DispatchChildWorkflowAsync(context, item, waitForCompletion); - - context.SetProperty(DispatchedInstancesCountKey, count); - } } private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionContext context, object item, bool waitForCompletion) @@ -155,7 +147,7 @@ public class BulkDispatchWorkflows : Activity if (workflowGraph == null) throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); - + var parentInstanceId = context.WorkflowExecutionContext.Id; var input = Input.GetOrDefault(context) ?? new Dictionary(); var channelName = ChannelName.GetOrDefault(context); @@ -164,8 +156,8 @@ public class BulkDispatchWorkflows : Activity { ["ParentInstanceId"] = parentInstanceId }; - - if(waitForCompletion) + + if (waitForCompletion) properties["WaitForCompletion"] = true; var itemDictionary = new Dictionary diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs index 4e579d60c..566c79937 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs @@ -97,10 +97,10 @@ public class DispatchWorkflow : Activity var workflowDefinitionId = WorkflowDefinitionId.Get(context); var workflowDefinitionService = context.GetRequiredService(); var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken); - + if (workflowGraph == null) throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); - + var input = Input.GetOrDefault(context) ?? new Dictionary(); var channelName = ChannelName.GetOrDefault(context); var parentInstanceId = context.WorkflowExecutionContext.Id; @@ -108,8 +108,9 @@ public class DispatchWorkflow : Activity { ["ParentInstanceId"] = parentInstanceId }; - - if(waitForCompletion) + + // If we need to wait for the child workflow to complete, set the property. This will be used by the ResumeDispatchWorkflowActivity handler. + if (waitForCompletion) properties["WaitForCompletion"] = true; input["ParentInstanceId"] = parentInstanceId; @@ -117,8 +118,6 @@ public class DispatchWorkflow : Activity var correlationId = CorrelationId.GetOrDefault(context); var workflowDispatcher = context.GetRequiredService(); var identityGenerator = context.GetRequiredService(); - - var instanceId = identityGenerator.GenerateId(); var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id) { diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs index cba602166..4fda5e880 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs @@ -47,7 +47,7 @@ public class ExecuteWorkflow : Activity /// [Input(Description = "The input to send to the workflow.")] public Input?> Input { get; set; } = null!; - + /// /// True to wait for the child workflow to complete before completing this activity. If not set, the child workflow will be executed until it either completes or goes idle before this activity completes. /// @@ -59,14 +59,14 @@ public class ExecuteWorkflow : Activity { var waitForCompletion = WaitForCompletion.Get(context); var result = await ExecuteWorkflowAsync(context, waitForCompletion); - - if(!waitForCompletion || result.Status == WorkflowStatus.Finished) + + if (!waitForCompletion || result.Status == WorkflowStatus.Finished) { context.SetResult(result); await context.CompleteActivityAsync(); return; } - + // Since the child workflow is still running, we need to wait for it to complete using a bookmark. var bookmarkOptions = new CreateBookmarkArgs { @@ -85,7 +85,7 @@ public class ExecuteWorkflow : Activity if (workflowGraph == null) throw new($"No published version of workflow definition with ID {workflowDefinitionId} found."); - + var parentInstanceId = context.WorkflowExecutionContext.Id; var input = Input.GetOrDefault(context) ?? new Dictionary(); var correlationId = CorrelationId.GetOrDefault(context); @@ -95,12 +95,13 @@ public class ExecuteWorkflow : Activity { ["ParentInstanceId"] = parentInstanceId }; - - if(waitForCompletion) + + // If we need to wait for the child workflow to complete, set the property. This will be used by the ResumeExecuteWorkflowActivity to resume the parent workflow. + if (waitForCompletion) properties["WaitForCompletion"] = true; - + input["ParentInstanceId"] = parentInstanceId; - + var options = new RunWorkflowOptions { ParentWorkflowInstanceId = parentInstanceId, @@ -121,7 +122,7 @@ public class ExecuteWorkflow : Activity return info; } - + private async ValueTask OnChildWorkflowCompletedAsync(ActivityExecutionContext context) { var input = context.WorkflowInput; From 95e04eac1726889298b55349f8da01d61cfe3c14 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 14:01:19 +0100 Subject: [PATCH 081/166] Refactor to remove redundant debug logs and clean up code. Eliminated unnecessary debug log statements from `ResumeDispatchWorkflowActivity` and removed redundant whitespace across the affected files. These changes streamline the code for better readability and maintainability --- .../ResumeDispatchWorkflowActivity.cs | 23 ++++++------------- .../Handlers/ResumeExecuteWorkflowActivity.cs | 3 +-- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs index f1b52b181..37b95583a 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeDispatchWorkflowActivity.cs @@ -15,32 +15,24 @@ namespace Elsa.Workflows.Runtime.Handlers; internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher, ILogger logger) : INotificationHandler { private static readonly string ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(); - + public async Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken) { var workflowState = notification.WorkflowState; - - logger.LogDebug("Handling workflow executed notification for workflow {WorkflowInstanceId}", notification.WorkflowState.Id); - + if (workflowState.Status != WorkflowStatus.Finished) - { - logger.LogDebug("Workflow {WorkflowInstanceId} is not in a finished state. Skipping resumption of any blocking DispatchWorkflow activities", notification.WorkflowState.Id); return; - } var props = workflowState.Properties; var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue; - + if (!waitForCompletion) - { - logger.LogDebug("Workflow {WorkflowInstanceId} does not have a WaitForCompletion property set to true. Skipping resumption of any blocking DispatchWorkflow activities", notification.WorkflowState.Id); return; - } - - var parentInstanceId = (string) props["ParentInstanceId"]; + + var parentInstanceId = (string)props["ParentInstanceId"]; var stimulus = new DispatchWorkflowStimulus(notification.WorkflowState.Id); var input = workflowState.Output; - + var bookmarkQueueItem = new NewBookmarkQueueItem { WorkflowInstanceId = parentInstanceId, @@ -51,8 +43,7 @@ internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, ISti Input = input } }; - - logger.LogDebug("Resuming any blocking DispatchWorkflow activities for workflow {WorkflowInstanceId} using stimulus hash {StimulusHash}", notification.WorkflowState.Id, bookmarkQueueItem.StimulusHash); + await bookmarkQueue.EnqueueAsync(bookmarkQueueItem, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs index d4bb203a6..da9ffefd9 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ResumeExecuteWorkflowActivity.cs @@ -23,9 +23,8 @@ internal class ResumeExecuteWorkflowActivity(IBookmarkQueue bookmarkQueue, IStim return; var props = workflowState.Properties; - var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue; - + if (!waitForCompletion) return; From 184b159c6fd09d7fda5dd350531076be0127ec5e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 14:22:16 +0100 Subject: [PATCH 082/166] Bump base version to 3.3.1 in workflow configuration --- .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 df4fbec7c..05ae4fb20 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -9,7 +9,7 @@ on: release: types: [ prereleased, published ] env: - base_version: '3.3.0' + base_version: '3.3.1' feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json' nuget_feed_source: 'https://api.nuget.org/v3/index.json' From c48592b53acf4e97f0a042bc3f2432087d59579e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 18:40:38 +0100 Subject: [PATCH 083/166] Update package dependencies to latest versions Updated various NuGet package dependencies to their latest available versions for improved stability, performance, and compatibility. The changes span multiple libraries, including Azure, FastEndpoints, and Microsoft.EntityFrameworkCore packages. This ensures the project remains up-to-date with the latest enhancements and bug fixes. --- Directory.Packages.props | 150 +++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 891e11d74..1d53a5dc7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,8 +11,8 @@ - - + + @@ -20,33 +20,33 @@ - - - + + + - - - - - - - - + + + + + + + + - + - + - + @@ -55,15 +55,15 @@ - + - + - + @@ -72,7 +72,7 @@ - + @@ -90,13 +90,13 @@ - + - + @@ -105,27 +105,27 @@ - + - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -136,7 +136,7 @@ - + @@ -144,8 +144,8 @@ - - + + @@ -153,43 +153,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + \ No newline at end of file From 3e9181bb9e1d0ab27d979754a6c7b6f803972928 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 18:40:45 +0100 Subject: [PATCH 084/166] Add 'constants' to NamespaceFoldersToSkip settings This update modifies the .DotSettings file to include 'constants' as a NamespaceFoldersToSkip entry. This ensures that folders named 'constants' are excluded from namespace inspections, improving clarity and compliance with project preferences. --- .../Elsa.Workflows.Runtime.csproj.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings index 9b4d77e2f..41b655e3e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings @@ -1,4 +1,5 @@  + True True True True From fa4a7ce031173d8726b44cdc6af021fb9db519e4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 18:40:59 +0100 Subject: [PATCH 085/166] Refactor to use target-typed new expressions. Simplified object instantiations by replacing explicit type declarations with target-typed `new` expressions. This improves code readability and reduces redundancy in object initialization. --- .../Helpers/ObjectConverter.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 04bfa40e7..f59e9861c 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -40,11 +40,11 @@ public static class ObjectConverter try { var convertedValue = value.ConvertTo(targetType, converterOptions); - return new Result(true, convertedValue, null); + return new(true, convertedValue, null); } catch (Exception e) { - return new Result(false, null, e); + return new(false, null, e); } } @@ -56,7 +56,7 @@ public static class ObjectConverter private static JsonSerializerOptions? _defaultSerializerOptions; private static JsonSerializerOptions? _internalSerializerOptions; - private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new JsonSerializerOptions + private static JsonSerializerOptions DefaultSerializerOptions => _defaultSerializerOptions ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, @@ -68,7 +68,7 @@ public static class ObjectConverter Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; - private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new JsonSerializerOptions + private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; @@ -274,20 +274,20 @@ public static class ObjectConverter { DateTime dateTime => dateTime, DateTimeOffset dateTimeOffset => dateTimeOffset.DateTime, - DateOnly date => new DateTime(date.Year, date.Month, date.Day), + DateOnly date => new(date.Year, date.Month, date.Day), _ => throw new ArgumentException("Invalid value type.") }, { } t when t == typeof(DateTimeOffset) => value switch { - DateTime dateTime => new DateTimeOffset(dateTime), + DateTime dateTime => new(dateTime), DateTimeOffset dateTimeOffset => dateTimeOffset, - DateOnly date => new DateTimeOffset(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero), + DateOnly date => new(date.Year, date.Month, date.Day, 0, 0, 0, TimeSpan.Zero), _ => throw new ArgumentException("Invalid value type.") }, { } t when t == typeof(DateOnly) => value switch { - DateTime dateTime => new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day), - DateTimeOffset dateTimeOffset => new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day), + DateTime dateTime => new(dateTime.Year, dateTime.Month, dateTime.Day), + DateTimeOffset dateTimeOffset => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day), DateOnly date => date, _ => throw new ArgumentException("Invalid value type.") }, From 868370d222c920ea360ba638b0efc8e072ff71b9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 18:54:25 +0100 Subject: [PATCH 086/166] Refactor VariableDefinitionMapper to include logging Refactored the `VariableDefinitionMapper` to utilize dependency injection for `IWellKnownTypeRegistry` and added `ILogger` for improved error handling. Updated value conversion logic to log warnings when conversion fails, ensuring better traceability. Simplified return statements and replaced redundant collections with modern syntax. --- .../Mappers/VariableDefinitionMapper.cs | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 2d741dbc6..6821f23c1 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -4,30 +4,21 @@ using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; +using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Management.Mappers; /// /// Maps s to s and vice versa. /// -public class VariableDefinitionMapper +public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry, ILogger logger) { - private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry; - - /// - /// Constructor. - /// - public VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegistry) - { - _wellKnownTypeRegistry = wellKnownTypeRegistry; - } - /// /// Maps a to a . /// public Variable? Map(VariableDefinition source) { - if (!_wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type)) + if (!wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type)) return null; var valueType = source.IsArray ? type.MakeArrayType() : type; @@ -38,8 +29,14 @@ public class VariableDefinitionMapper variable.Id = source.Id; variable.Name = source.Name; - variable.Value = source.Value.ConvertTo(valueType); - variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : default; + source.Value?.TryConvertTo(valueType).OnSuccess(value => + { + variable.Value = value; + }).OnFailure(ex => + { + logger.LogWarning(ex, "Failed to convert variable value."); + }); + variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null; return variable; } @@ -52,7 +49,7 @@ public class VariableDefinitionMapper .Select(Map) .Where(x => x != null) .Select(x => x!) - ?? Enumerable.Empty(); + ?? []; /// /// Maps a to a . @@ -64,16 +61,15 @@ public class VariableDefinitionMapper var isArray = valueType.IsCollectionType(); var elementValueType = isArray ? valueType.GenericTypeArguments[0] : valueType; var value = source.Value; - - var valueTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); + var valueTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); var serializedValue = value.Format(); - return new VariableDefinition(source.Id, source.Name, valueTypeAlias, isArray, serializedValue, storageDriverTypeName); + return new(source.Id, source.Name, valueTypeAlias, isArray, serializedValue, storageDriverTypeName); } /// /// Maps a list of s to a list of s. /// - public IEnumerable Map(IEnumerable? source) => source?.Select(Map) ?? Enumerable.Empty(); + public IEnumerable Map(IEnumerable? source) => source?.Select(Map) ?? []; } \ No newline at end of file From 3920d420a9d7a21e6bea877b4562d986a5d59167 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 18:57:40 +0100 Subject: [PATCH 087/166] Update log message for variable conversion failure Improved the log message to include specific details about the failed conversion, such as the default value, variable name, and type. This enhances debugging by providing more context about the error. --- .../Mappers/VariableDefinitionMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 6821f23c1..7f1bbe591 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -34,7 +34,7 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist variable.Value = value; }).OnFailure(ex => { - logger.LogWarning(ex, "Failed to convert variable value."); + logger.LogWarning(ex, "Failed to convert the default value {DefaultValue} of variable {VariableName} to its type {VariableType}. Default value will not be set.", source.Value, source.Name, valueType); }); variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null; From 327a49d8c0e451ce1e5064eaa54f0c2517ef509c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 19:16:15 +0100 Subject: [PATCH 088/166] Refactor VariableDefinitionMapper to improve type handling for collections and arrays --- .../Mappers/VariableDefinitionMapper.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 7f1bbe591..1d0e0e934 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -58,8 +58,9 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist { var variableType = source.GetType(); var valueType = variableType.IsConstructedGenericType ? variableType.GetGenericArguments().FirstOrDefault() ?? typeof(object) : typeof(object); - var isArray = valueType.IsCollectionType(); - var elementValueType = isArray ? valueType.GenericTypeArguments[0] : valueType; + var isArray = valueType.IsArray; + var isCollection = valueType.IsCollectionType(); + var elementValueType = isArray ? valueType.GetElementType() : isCollection ? valueType.GenericTypeArguments[0] : valueType; var value = source.Value; var valueTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); From cd2d0efef55194b581c1cd734491dd551e0dd2a6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 19:50:02 +0100 Subject: [PATCH 089/166] Refactor MongoDbStore to use EmptyToNull for TenantId assignments and add StringExtensions for null handling --- src/modules/Elsa.MongoDb/Common/MongoDbStore.cs | 6 +++--- src/modules/Elsa.MongoDb/Extensions/StringExtensions.cs | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/modules/Elsa.MongoDb/Extensions/StringExtensions.cs diff --git a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs index c070fd188..9c5c3b674 100644 --- a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs @@ -405,7 +405,7 @@ public class MongoDbStore(IMongoCollection collection, ITe if (typeof(Entity).IsAssignableFrom(typeof(TDocument))) { var tenant = tenantAccessor.Tenant; - var tenantId = tenant?.Id; + var tenantId = tenant?.Id.EmptyToNull(); queryable = queryable.Where(x => (x as Entity)!.TenantId == tenantId); } @@ -418,7 +418,7 @@ public class MongoDbStore(IMongoCollection collection, ITe var tenantId = tenant?.Id; if (document is Entity tenantDocument) - tenantDocument.TenantId = tenantId; + tenantDocument.TenantId = tenantId.EmptyToNull(); } private void ApplyTenantId(IEnumerable documents) @@ -429,7 +429,7 @@ public class MongoDbStore(IMongoCollection collection, ITe foreach (var document in documents) { if (document is Entity tenantDocument) - tenantDocument.TenantId = tenantId; + tenantDocument.TenantId = tenantId.EmptyToNull(); } } } \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/Extensions/StringExtensions.cs b/src/modules/Elsa.MongoDb/Extensions/StringExtensions.cs new file mode 100644 index 000000000..cc806f04a --- /dev/null +++ b/src/modules/Elsa.MongoDb/Extensions/StringExtensions.cs @@ -0,0 +1,6 @@ +namespace Elsa.MongoDb.Extensions; + +public static class StringExtensions +{ + public static string? EmptyToNull(this string? value) => string.IsNullOrWhiteSpace(value) ? null : value; +} \ No newline at end of file From 658630078be9b36d8ee9696122c81c7ee2621397 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:06:29 +0100 Subject: [PATCH 090/166] Add SMTP server configuration and refactor email activity for improved error handling and attachment processing --- docker/docker-compose.yml | 12 +++ .../Elsa.Email/Activities/SendEmail.cs | 84 ++++++++++++------- .../Mappers/VariableDefinitionMapper.cs | 27 ++++-- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 76d7d7b40..5340c7c54 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -72,6 +72,18 @@ environment: - PlantUml__RemoteUrl= - ConnectionStrings__DefaultConnection=USER ID=tracelens;PASSWORD=tracelenspass;HOST=postgres;PORT=5432;DATABASE=tracelens;POOLING=true; + + smtp4dev: # Mock SMTP server + image: rnwood/smtp4dev + container_name: smtp4dev + restart: always + ports: + - "3000:80" # Web interface + - "2525:25" # SMTP port + environment: + - ASPNETCORE_URLS=http://+:80 + - Logging__LogLevel__Default=Information + elsa-server: build: diff --git a/src/modules/Elsa.Email/Activities/SendEmail.cs b/src/modules/Elsa.Email/Activities/SendEmail.cs index c1e88afcb..4ec9ca02d 100644 --- a/src/modules/Elsa.Email/Activities/SendEmail.cs +++ b/src/modules/Elsa.Email/Activities/SendEmail.cs @@ -84,8 +84,7 @@ public class SendEmail : Activity /// /// The activity to execute when an error occurs while trying to send the email. /// - [Port] - public IActivity? Error { get; set; } + [Port] public IActivity? Error { get; set; } /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) @@ -99,7 +98,10 @@ public class SendEmail : Activity message.From.Add(MailboxAddress.Parse(from)); message.Subject = Subject.GetOrDefault(context) ?? ""; - var bodyBuilder = new BodyBuilder { HtmlBody = Body.GetOrDefault(context) }; + var bodyBuilder = new BodyBuilder + { + HtmlBody = Body.GetOrDefault(context) + }; await AddAttachmentsAsync(context, bodyBuilder, cancellationToken); message.Body = bodyBuilder.ToMessageBody(); @@ -119,7 +121,10 @@ public class SendEmail : Activity catch (Exception e) { logger.LogWarning(e, "Error while sending email message"); - context.AddExecutionLogEntry("Error", e.Message, payload: new { e.StackTrace }); + context.AddExecutionLogEntry("Error", e.Message, payload: new + { + e.StackTrace + }); await context.ScheduleActivityAsync(Error, OnErrorCompletedAsync); } } @@ -155,38 +160,38 @@ public class SendEmail : Activity await AttachLocalFileAsync(bodyBuilder, path, cancellationToken); break; case byte[] bytes: - { - var fileName = $"Attachment-{++index}"; - bodyBuilder.Attachments.Add(fileName, bytes, ContentType.Parse("application/binary")); - break; - } + { + var fileName = $"Attachment-{++index}"; + bodyBuilder.Attachments.Add(fileName, bytes, ContentType.Parse("application/binary")); + break; + } case Stream stream: - { - var fileName = $"Attachment-{++index}"; - await bodyBuilder.Attachments.AddAsync(fileName, stream, ContentType.Parse("application/binary"), cancellationToken); - break; - } + { + var fileName = $"Attachment-{++index}"; + await bodyBuilder.Attachments.AddAsync(fileName, stream, ContentType.Parse("application/binary"), cancellationToken); + break; + } case EmailAttachment emailAttachment: - { - var fileName = emailAttachment.FileName ?? $"Attachment-{++index}"; - var contentType = emailAttachment.ContentType ?? "application/binary"; - var parsedContentType = ContentType.Parse(contentType); + { + var fileName = emailAttachment.FileName ?? $"Attachment-{++index}"; + var contentType = emailAttachment.ContentType ?? "application/binary"; + var parsedContentType = ContentType.Parse(contentType); - if (emailAttachment.Content is byte[] bytes) - bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType); + if (emailAttachment.Content is byte[] bytes) + bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType); - else if (emailAttachment.Content is Stream stream) - await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); + else if (emailAttachment.Content is Stream stream) + await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); - break; - } + break; + } default: - { - var json = JsonSerializer.Serialize(attachmentObject); - var fileName = $"Attachment-{++index}"; - bodyBuilder.Attachments.Add(fileName, Encoding.UTF8.GetBytes(json), ContentType.Parse("application/json")); - break; - } + { + var json = JsonSerializer.Serialize(attachmentObject); + var fileName = $"Attachment-{++index}"; + bodyBuilder.Attachments.Add(fileName, Encoding.UTF8.GetBytes(json), ContentType.Parse("application/json")); + break; + } } } } @@ -203,7 +208,24 @@ public class SendEmail : Activity await bodyBuilder.Attachments.AddAsync(fileName, contentStream, ContentType.Parse(contentType), cancellationToken); } - private IEnumerable InterpretAttachmentsModel(object attachments) => attachments is string text ? new[] { text } : attachments is IEnumerable enumerable ? enumerable : new[] { attachments }; + private IEnumerable InterpretAttachmentsModel(object attachments) + { + if (attachments is byte[] bytes) + return new[] + { + bytes + }; + + return attachments is string text + ? new[] + { + text + } + : attachments as IEnumerable ?? new[] + { + attachments + }; + } private void SetRecipientsEmailAddresses(InternetAddressList list, IEnumerable? addresses) { diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 1d0e0e934..48c481de0 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -18,10 +18,16 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist /// public Variable? Map(VariableDefinition source) { - if (!wellKnownTypeRegistry.TryGetTypeOrDefault(source.TypeName, out var type)) + var aliasedType = wellKnownTypeRegistry.TryGetType(source.TypeName, out var aliasedTypeValue) ? aliasedTypeValue : null; + var type = aliasedType ?? Type.GetType(source.TypeName); + + if(type == null) + { + logger.LogWarning("Failed to resolve the type {TypeName} of variable {VariableName}. Variable will not be mapped.", source.TypeName, source.Name); return null; + } - var valueType = source.IsArray ? type.MakeArrayType() : type; + var valueType = aliasedType ?? (source.IsArray ? type.MakeArrayType() : type); var variableGenericType = typeof(Variable<>).MakeGenericType(valueType); var variable = (Variable)Activator.CreateInstance(variableGenericType)!; @@ -58,15 +64,20 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist { var variableType = source.GetType(); var valueType = variableType.IsConstructedGenericType ? variableType.GetGenericArguments().FirstOrDefault() ?? typeof(object) : typeof(object); + var valueTypeAlias = wellKnownTypeRegistry.TryGetAlias(valueType, out var alias) ? alias : null; + var value = source.Value; + var serializedValue = value.Format(); + var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); + + if(valueTypeAlias != null) + return new(source.Id, source.Name, valueTypeAlias, false, serializedValue, storageDriverTypeName); + var isArray = valueType.IsArray; var isCollection = valueType.IsCollectionType(); var elementValueType = isArray ? valueType.GetElementType() : isCollection ? valueType.GenericTypeArguments[0] : valueType; - var value = source.Value; - var valueTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); - var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); - var serializedValue = value.Format(); - - return new(source.Id, source.Name, valueTypeAlias, isArray, serializedValue, storageDriverTypeName); + var elementTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); + + return new(source.Id, source.Name, elementTypeAlias, isArray, serializedValue, storageDriverTypeName); } /// From 48bd06ecc74f6bb078741a8565893db7d0b45f01 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:08:42 +0100 Subject: [PATCH 091/166] Update base version to 3.3.2 in GitHub workflow Incremented the `base_version` from 3.3.1 to 3.3.2 in the GitHub Actions workflow configuration. This ensures alignment with the updated versioning for release management. --- .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 05ae4fb20..189b2242c 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -9,7 +9,7 @@ on: release: types: [ prereleased, published ] env: - base_version: '3.3.1' + base_version: '3.3.2' feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json' nuget_feed_source: 'https://api.nuget.org/v3/index.json' From 2459b56fd3770c621213058cf352b618d6efdc41 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:17:50 +0100 Subject: [PATCH 092/166] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c7878d823..ed068921d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,8 +1,8 @@ --- name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: bug +about: Create a bug report to help us improve +title: "[BUG] " +type: Bug assignees: '' --- From 941ce429f1393d7da9e5788eee402e3f69c21681 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:18:42 +0100 Subject: [PATCH 093/166] Update enhancement.md --- .github/ISSUE_TEMPLATE/enhancement.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md index 72c1f00d2..44a3228d5 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -1,8 +1,8 @@ --- name: Enhancement about: Suggest an enhancement to an existing feature for this project -title: "[ENH] " -labels: enhancement +title: "" +type: Enhancement assignees: '' --- From 1b022d34c89e2b7fdf4cb8f88fe5ca89dae06743 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:18:57 +0100 Subject: [PATCH 094/166] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ed068921d..78a907e38 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a bug report to help us improve -title: "[BUG] " +title: "" type: Bug assignees: '' From 8d443aed625429be6de20ac48ab00e3b6de8739e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:19:15 +0100 Subject: [PATCH 095/166] Update chore.md --- .github/ISSUE_TEMPLATE/chore.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/chore.md b/.github/ISSUE_TEMPLATE/chore.md index b6d83591a..641f6cedf 100644 --- a/.github/ISSUE_TEMPLATE/chore.md +++ b/.github/ISSUE_TEMPLATE/chore.md @@ -1,8 +1,8 @@ --- name: Maintenance Task about: Suggest a maintenance task for this project -title: "[CHORE] " -labels: maintenance +title: "" +type: Maintenance assignees: '' --- From 8e468e343c5852ddfa83009eafb03ce48475b0ff Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:19:30 +0100 Subject: [PATCH 096/166] Update doc.md --- .github/ISSUE_TEMPLATE/doc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md index 46aca55d7..83f634186 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -1,8 +1,8 @@ --- name: Documentation Improvement about: Suggest improvements to the documentation for this project -title: "[DOC] " -labels: documentation +title: "" +Type: Documentation assignees: '' --- From b17240bafe5229811d1bdcc95757a8b2aebefec6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:20:10 +0100 Subject: [PATCH 097/166] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bd29571db..9a4ea5fb3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,8 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEAT]" -labels: enhancement +title: "" +type: Feature assignees: '' --- From 157927dc9577a8726c8cb81797275567cb9f9027 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:20:26 +0100 Subject: [PATCH 098/166] Update performance.md --- .github/ISSUE_TEMPLATE/performance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/performance.md b/.github/ISSUE_TEMPLATE/performance.md index 34ee8f5a4..332cf2002 100644 --- a/.github/ISSUE_TEMPLATE/performance.md +++ b/.github/ISSUE_TEMPLATE/performance.md @@ -1,8 +1,8 @@ --- name: Performance Improvement about: Suggest a performance enhancement for this project -title: "[PERF] " -labels: performance +title: "" +type: Performance assignees: '' --- From 6b6b081b661b1737dba006d16c7f43d3065c6542 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:21:12 +0100 Subject: [PATCH 099/166] Update and rename chore.md to task.md --- .github/ISSUE_TEMPLATE/{chore.md => task.md} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename .github/ISSUE_TEMPLATE/{chore.md => task.md} (91%) diff --git a/.github/ISSUE_TEMPLATE/chore.md b/.github/ISSUE_TEMPLATE/task.md similarity index 91% rename from .github/ISSUE_TEMPLATE/chore.md rename to .github/ISSUE_TEMPLATE/task.md index 641f6cedf..430bf669c 100644 --- a/.github/ISSUE_TEMPLATE/chore.md +++ b/.github/ISSUE_TEMPLATE/task.md @@ -1,13 +1,13 @@ --- -name: Maintenance Task -about: Suggest a maintenance task for this project +name: Task +about: Suggest a task for this project title: "" -type: Maintenance +type: Task assignees: '' --- -## Maintenance Task Request +## Task Request ### Task Overview **What maintenance task do you propose? Please describe.** From 8f36d4c8712d5703f67afafca554f7fdc1dd42fc Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:21:42 +0100 Subject: [PATCH 100/166] Update test.md --- .github/ISSUE_TEMPLATE/test.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/test.md b/.github/ISSUE_TEMPLATE/test.md index d6f778748..1021547c1 100644 --- a/.github/ISSUE_TEMPLATE/test.md +++ b/.github/ISSUE_TEMPLATE/test.md @@ -1,8 +1,8 @@ --- name: Test Improvement about: Suggest improvements or additions to tests for this project -title: "[TEST] " -labels: test +title: "" +type: Test assignees: '' --- From bb2456595d504380fe96379697ef2f33cb329098 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:22:13 +0100 Subject: [PATCH 101/166] Update and rename enhancement.md to improvement.md --- .github/ISSUE_TEMPLATE/{enhancement.md => improvement.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/ISSUE_TEMPLATE/{enhancement.md => improvement.md} (98%) diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/improvement.md similarity index 98% rename from .github/ISSUE_TEMPLATE/enhancement.md rename to .github/ISSUE_TEMPLATE/improvement.md index 44a3228d5..e72b4864c 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ b/.github/ISSUE_TEMPLATE/improvement.md @@ -1,8 +1,8 @@ --- -name: Enhancement +name: Improvement about: Suggest an enhancement to an existing feature for this project title: "" -type: Enhancement +type: Improvement assignees: '' --- From cbe0cccbcd19fb111f43b68cbf53f99225ce636c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:25:30 +0100 Subject: [PATCH 102/166] Delete .github/ISSUE_TEMPLATE/test.md --- .github/ISSUE_TEMPLATE/test.md | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/test.md diff --git a/.github/ISSUE_TEMPLATE/test.md b/.github/ISSUE_TEMPLATE/test.md deleted file mode 100644 index 1021547c1..000000000 --- a/.github/ISSUE_TEMPLATE/test.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Test Improvement -about: Suggest improvements or additions to tests for this project -title: "" -type: Test -assignees: '' - ---- - -## Test Improvement Request - -### Test Issue Overview -**Is your test improvement related to a specific problem? Please describe.** -Provide a detailed explanation of the testing issue, such as missing coverage, flaky tests, or inefficient testing processes. For example, "We currently have no tests covering our user authentication flow..." - -### Proposed Test Improvements -**Describe the improvements you'd like** -Explain in detail what testing improvements or new tests you would like to see implemented. Describe how these tests should work and why they are necessary. Include any particular aspects like integration tests, unit tests, or end-to-end tests. - -### Existing Tests -**Describe any existing tests and their limitations** -Detail the current testing setup and its limitations. This could include gaps in coverage, outdated tests, or tests that frequently fail under certain conditions. - -### Impact of Improvements -**Explain the potential impact** -How would these test improvements benefit the development process, product reliability, or deployment cycles? Detail the anticipated benefits to help stakeholders understand the value of improving testing. - -### Additional Context -**Add any other context** -Include any other information that might help clarify your request. This could be technical considerations, links to failed builds, or any other details that could inform the improvement process. From d373b24d1521ce413356ed4a8e557b8f51d898df Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:28:55 +0100 Subject: [PATCH 103/166] Update performance.md --- .github/ISSUE_TEMPLATE/performance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/performance.md b/.github/ISSUE_TEMPLATE/performance.md index 332cf2002..f66241a5a 100644 --- a/.github/ISSUE_TEMPLATE/performance.md +++ b/.github/ISSUE_TEMPLATE/performance.md @@ -1,5 +1,5 @@ --- -name: Performance Improvement +name: Performance about: Suggest a performance enhancement for this project title: "" type: Performance From adea7fa666b711fa8b5681a8558f6774be88c9ca Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 24 Jan 2025 23:29:18 +0100 Subject: [PATCH 104/166] Update and rename doc.md to documentation.md --- .github/ISSUE_TEMPLATE/{doc.md => documentation.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/ISSUE_TEMPLATE/{doc.md => documentation.md} (97%) diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/documentation.md similarity index 97% rename from .github/ISSUE_TEMPLATE/doc.md rename to .github/ISSUE_TEMPLATE/documentation.md index 83f634186..96e21f160 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,5 +1,5 @@ --- -name: Documentation Improvement +name: Documentation about: Suggest improvements to the documentation for this project title: "" Type: Documentation From 9365328f0bceaeb975b89e3fb2d467d8bc8b6204 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 25 Jan 2025 23:26:37 +0100 Subject: [PATCH 105/166] Add commit state behavior support in workflows Introduced `ActivityCommitStateBehavior` and `WorkflowCommitStateOptions` to enable flexible state commit handling in workflows. Integrated commit logic into activity and workflow execution contexts and middleware. This improves control over when state is committed during workflow execution. --- .../Extensions/ActivityExtensions.cs | 11 +++ .../Extensions/JsonObjectExtensions.cs | 79 +++++++++++-------- .../Models/ActivityCommitStateBehavior.cs | 29 +++++++ .../Models/WorkflowCommitStateOptions.cs | 19 +++++ .../Models/WorkflowOptions.cs | 5 ++ .../Contexts/WorkflowExecutionContext.cs | 15 +++- .../Contracts/ICommitStateHandler.cs | 1 + .../Elsa.Workflows.Core.csproj.DotSettings | 2 + .../Enums/ActivityCommitStateBehavior.cs | 29 +++++++ .../Extensions/ActivityPropertyExtensions.cs | 15 +++- .../DefaultActivityInvokerMiddleware.cs | 44 +++++++++++ .../DefaultActivitySchedulerMiddleware.cs | 10 +++ .../Models/WorkflowCommitStateOptions.cs | 19 +++++ .../Models/WorkflowOptions.cs | 5 ++ .../Services/NoopCommitStateHandler.cs | 5 ++ .../Services/StoreCommitStateHandler.cs | 6 ++ 16 files changed, 258 insertions(+), 36 deletions(-) create mode 100644 src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs create mode 100644 src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs create mode 100644 src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs diff --git a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs index 6ae48eab8..f39fc90bc 100644 --- a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; using Elsa.Api.Client.Shared.Models; namespace Elsa.Api.Client.Extensions; @@ -182,4 +183,14 @@ public static class ActivityExtensions /// Sets a value indicating whether the specified activity can trigger the workflow. /// public static void SetRunAsynchronously(this JsonObject activity, bool value) => activity.SetProperty(JsonValue.Create(value), "customProperties", "runAsynchronously"); + + /// + /// Gets the commit state behavior for the specified activity. + /// + public static ActivityCommitStateBehavior GetCommitStateBehavior(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStateBehavior") ?? ActivityCommitStateBehavior.Default; + + /// + /// Sets the commit state behavior for the specified activity. + /// + public static void SetCommitStateBehavior(this JsonObject activity, ActivityCommitStateBehavior value) => activity.SetProperty(JsonValue.Create(value.ToString()), "customProperties", "commitStateBehavior"); } \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs index a1612f9c2..b65fcf394 100644 --- a/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/JsonObjectExtensions.cs @@ -15,52 +15,43 @@ public static class JsonObjectExtensions { return obj.ContainsKey("type") && obj.ContainsKey("id") && obj.ContainsKey("version"); } - + /// /// Serializes the specified value to a . /// /// The value to serialize. /// The to use. /// A representing the specified value. - public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = default) + public static JsonNode SerializeToNode(this object value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - + options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + return JsonSerializer.SerializeToNode(value, options)!; } - + /// /// Serializes the specified value to a . /// /// The value to serialize. /// The to use. /// A representing the specified value. - public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = default) + public static JsonArray SerializeToArray(this object value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - + options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + return JsonSerializer.SerializeToNode(value, options)!.AsArray(); } - + /// /// Serializes the specified value to a . /// /// The value to serialize. /// The to use. /// A representing the specified value. - public static JsonArray SerializeToArray(this IEnumerable value, JsonSerializerOptions? options = default) + public static JsonArray SerializeToArray(this IEnumerable value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - + options ??= new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + return JsonSerializer.SerializeToNode(value, options)!.AsArray(); } @@ -71,19 +62,23 @@ public static class JsonObjectExtensions /// The to use. /// The type to deserialize to. /// The deserialized value. - public static T Deserialize(this JsonNode value, JsonSerializerOptions? options = default) + public static T Deserialize(this JsonNode value, JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - + options ??= new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + if (value is JsonObject jsonObject) return JsonSerializer.Deserialize(jsonObject, options)!; if (value is JsonArray jsonArray) return JsonSerializer.Deserialize(jsonArray, options)!; + if (typeof(T).IsEnum || (Nullable.GetUnderlyingType(typeof(T))?.IsEnum ?? false)) + { + if (value.GetValueKind() == JsonValueKind.Null) + return default!; + return (T)Enum.Parse(Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T), value.ToString()); + } + if (value is JsonValue jsonValue) return jsonValue.GetValue(); @@ -101,7 +96,7 @@ public static class JsonObjectExtensions model = GetPropertyContainer(model, path); model[path.Last()] = value?.SerializeToNode(); } - + /// /// Sets the property value of the specified model. /// @@ -113,7 +108,7 @@ public static class JsonObjectExtensions model = GetPropertyContainer(model, path); model[path.Last()] = value?.SerializeToNode(); } - + /// /// Sets the property value of the specified model. /// @@ -125,7 +120,7 @@ public static class JsonObjectExtensions model = GetPropertyContainer(model, path); model[path.Last()] = new JsonArray(value.Select(x => x.SerializeToNode()).ToArray()); } - + /// /// Gets the property value of the specified model. /// @@ -139,7 +134,7 @@ public static class JsonObjectExtensions foreach (var prop in path.SkipLast(1)) { if (currentModel[prop] is not JsonObject value) - return default; + return null; currentModel = value; } @@ -147,6 +142,25 @@ public static class JsonObjectExtensions return currentModel[path.Last()]; } + /// + /// Gets the property value of the specified model. + /// + /// The model to get the property value from. + /// The path to the property. + /// The type to deserialize to. + /// The property value. + public static T? TryGetProperty(this JsonObject model, params string[] path) + { + try + { + return model.GetProperty(path); + } + catch (Exception e) + { + return default; + } + } + /// /// Gets the property value of the specified model. /// @@ -173,7 +187,7 @@ public static class JsonObjectExtensions var property = GetProperty(model, path); return property != null ? property.Deserialize(options) : default; } - + /// /// Returns the property container of the specified model. /// @@ -190,5 +204,4 @@ public static class JsonObjectExtensions return model; } - } \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs new file mode 100644 index 000000000..66d6dbdf6 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs @@ -0,0 +1,29 @@ +namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +public enum ActivityCommitStateBehavior +{ + /// + /// Never commit state, regardless of the workflow commit state options. + /// + Never, + + /// + /// Look at the workflow commit state options to determine if state should be committed. + /// + Default, + + /// + /// Commit state before the activity starts. + /// + Executing, + + /// + /// Commit state after the activity executes. + /// + Executed, + + /// + /// Commit state before the activity starts and after the activity executes. + /// + BeforeAndAfterExecution +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs new file mode 100644 index 000000000..8e14345c6 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowCommitStateOptions.cs @@ -0,0 +1,19 @@ +namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +public class WorkflowCommitStateOptions +{ + /// + /// Commit workflow state before the workflow starts. + /// + public bool Starting { get; set; } + + /// + /// Commit workflow state before an activity executes, unless the activity is configured to not commit state. + /// + public bool ActivityExecuting { get; set; } + + /// + /// Commit workflow state after an activity executes, unless the activity is configured to not commit state. + /// + public bool ActivityExecuted { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs index ec28ac197..c3f38f381 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs @@ -29,4 +29,9 @@ public class WorkflowOptions /// The type of IIncidentStrategy to use when a fault occurs in the workflow. /// public string? IncidentStrategyType { get; set; } + + /// + /// The options for committing workflow state. + /// + public WorkflowCommitStateOptions CommitStateOptions { get; set; } = new(); } \ 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 56788e661..df38d706d 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -37,6 +37,7 @@ public partial class WorkflowExecutionContext : IExecutionContext private readonly IList _completionCallbackEntries = new List(); private IList _activityExecutionContexts; private readonly IHasher _hasher; + private readonly ICommitStateHandler _commitStateHandler; /// /// Initializes a new instance of . @@ -61,6 +62,7 @@ public partial class WorkflowExecutionContext : IExecutionContext ActivityRegistry = serviceProvider.GetRequiredService(); ActivityRegistryLookup = serviceProvider.GetRequiredService(); _hasher = serviceProvider.GetRequiredService(); + _commitStateHandler = serviceProvider.GetRequiredService(); SubStatus = WorkflowSubStatus.Pending; Id = id; CorrelationId = correlationId; @@ -238,6 +240,11 @@ public partial class WorkflowExecutionContext : IExecutionContext /// The current sub status of the workflow. public WorkflowSubStatus SubStatus { get; internal set; } + /// + /// The previous sub status of the workflow. + /// + public WorkflowSubStatus PreviousSubStatus { get; internal set; } + /// The root associated with the execution context. public MemoryRegister MemoryRegister { get; private set; } = null!; @@ -510,8 +517,9 @@ public partial class WorkflowExecutionContext : IExecutionContext internal void TransitionTo(WorkflowSubStatus subStatus) { if (!ValidateStatusTransition()) - throw new Exception($"Cannot transition from {SubStatus} to {subStatus}"); + throw new($"Cannot transition from {SubStatus} to {subStatus}"); + PreviousSubStatus = SubStatus; SubStatus = subStatus; UpdatedAt = SystemClock.UtcNow; @@ -614,4 +622,9 @@ public partial class WorkflowExecutionContext : IExecutionContext var currentMainStatus = GetMainStatus(SubStatus); return currentMainStatus != WorkflowStatus.Finished; } + + public Task CommitAsync() + { + return _commitStateHandler.CommitAsync(this, CancellationToken); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs b/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs index 3d24ee6fd..a73659ea5 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs @@ -4,5 +4,6 @@ namespace Elsa.Workflows; public interface ICommitStateHandler { + Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken = default); Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings index f05e636df..300c67063 100644 --- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings @@ -7,6 +7,8 @@ True True True + True + True True True True diff --git a/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs b/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs new file mode 100644 index 000000000..3ae3e3062 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs @@ -0,0 +1,29 @@ +namespace Elsa.Workflows; + +public enum ActivityCommitStateBehavior +{ + /// + /// Never commit state, regardless of the workflow commit state options. + /// + Never, + + /// + /// Look at the workflow commit state options to determine if state should be committed. + /// + Default, + + /// + /// Commit state before the activity starts. + /// + Executing, + + /// + /// Commit state after the activity executes. + /// + Executed, + + /// + /// Commit state before the activity starts and after the activity executes. + /// + BeforeAndAfterExecution +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs index 4d657080a..b4c240157 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs @@ -11,6 +11,7 @@ public static class ActivityPropertyExtensions private static readonly string[] CanStartWorkflowPropertyName = ["canStartWorkflow", "CanStartWorkflow"]; private static readonly string[] RunAsynchronouslyPropertyName = ["runAsynchronously", "RunAsynchronously"]; private static readonly string[] SourcePropertyName = ["source", "Source"]; + private static readonly string[] CommitStateBehaviorName = ["commitStateBehavior", "CommitStateBehavior"]; /// /// Gets a flag indicating whether this activity can be used for starting a workflow. @@ -46,6 +47,16 @@ public static class ActivityPropertyExtensions /// Sets the source file and line number where this activity was instantiated, if any. /// public static void SetSource(this IActivity activity, string value) => activity.CustomProperties[SourcePropertyName[0]] = value; + + /// + /// Gets the commit state behavior for the specified activity. + /// + public static ActivityCommitStateBehavior GetCommitStateBehavior(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CommitStateBehaviorName, () => ActivityCommitStateBehavior.Default); + + /// + /// Sets the commit state behavior for the specified activity. + /// + public static void SetCommitStateBehavior(this IActivity activity, ActivityCommitStateBehavior value) => activity.CustomProperties[CommitStateBehaviorName[0]] = value; /// /// Sets the source file and line number where this activity was instantiated, if any. @@ -62,7 +73,7 @@ public static class ActivityPropertyExtensions /// /// Gets the display text for the specified activity. /// - public static string? GetDisplayText(this IActivity activity) => activity.Metadata.TryGetValue("displayText", out var value) ? value.ToString() : default; + public static string? GetDisplayText(this IActivity activity) => activity.Metadata.TryGetValue("displayText", out var value) ? value.ToString() : null; /// /// Sets the display text for the specified activity. @@ -72,7 +83,7 @@ public static class ActivityPropertyExtensions /// /// Gets the description for the specified activity. /// - public static string? GetDescription(this IActivity activity) => activity.Metadata.TryGetValue("description", out var value) ? value.ToString() : default; + public static string? GetDescription(this IActivity activity) => activity.Metadata.TryGetValue("description", out var value) ? value.ToString() : null; /// /// Sets the description for the specified activity. diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 65652ea04..a7bd74c6b 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -47,6 +47,10 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time"); return; } + + // Conditionally commit the workflow state. + if(ShouldCommitWhenStarting(context)) + await context.WorkflowExecutionContext.CommitAsync(); context.TransitionTo(ActivityStatus.Running); @@ -78,6 +82,10 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks); logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count); } + + // Conditionally commit the workflow state. + if(ShouldCommitWhenExecuted(context)) + await context.WorkflowExecutionContext.CommitAsync(); } /// @@ -111,4 +119,40 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I // Evaluate input properties. await context.EvaluateInputPropertiesAsync(); } + + private bool ShouldCommitWhenStarting(ActivityExecutionContext context) + { + var behavior = context.Activity.GetCommitStateBehavior(); + + if (behavior == ActivityCommitStateBehavior.Executing) + return true; + + if (behavior == ActivityCommitStateBehavior.Default) + { + var workflowOptions = context.WorkflowExecutionContext.Workflow.Options.CommitStateOptions; + + if(workflowOptions.ActivityExecuting) + return true; + } + + return false; + } + + private bool ShouldCommitWhenExecuted(ActivityExecutionContext context) + { + var behavior = context.Activity.GetCommitStateBehavior(); + + if (behavior == ActivityCommitStateBehavior.Executed) + return true; + + if (behavior == ActivityCommitStateBehavior.Default) + { + var workflowOptions = context.WorkflowExecutionContext.Workflow.Options.CommitStateOptions; + + if(workflowOptions.ActivityExecuted) + return true; + } + + return false; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs index f9d1645d9..078474b68 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs @@ -36,6 +36,8 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware context.TransitionTo(WorkflowSubStatus.Executing); + await ConditionallyCommitStateAsync(context); + while (scheduler.HasAny) { // Do not start a workflow if cancellation has been requested. @@ -65,4 +67,12 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware await _activityInvoker.InvokeAsync(context, workItem.Activity, options); } + + private async Task ConditionallyCommitStateAsync(WorkflowExecutionContext context) + { + var shouldCommit = context.Workflow.Options.CommitStateOptions.Starting; + + if (shouldCommit) + await context.CommitAsync(); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs new file mode 100644 index 000000000..514a255db --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs @@ -0,0 +1,19 @@ +namespace Elsa.Workflows.Models; + +public class WorkflowCommitStateOptions +{ + /// + /// Commit workflow state before the workflow starts. + /// + public bool Starting { get; set; } + + /// + /// Commit workflow state before an activity executes, unless the activity is configured to not commit state. + /// + public bool ActivityExecuting { get; set; } + + /// + /// Commit workflow state after an activity executes, unless the activity is configured to not commit state. + /// + public bool ActivityExecuted { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs index e4b82b116..f2ac78484 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs @@ -29,4 +29,9 @@ public class WorkflowOptions /// The type of to use when a fault occurs in the workflow. /// public Type? IncidentStrategyType { get; set; } + + /// + /// The options for committing workflow state. + /// + public WorkflowCommitStateOptions CommitStateOptions { get; set; } = new(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs b/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs index 8503bddb7..e0a6b7f81 100644 --- a/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs @@ -4,6 +4,11 @@ namespace Elsa.Workflows; public class NoopCommitStateHandler : ICommitStateHandler { + public Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + public Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, CancellationToken cancellationToken = default) { return Task.CompletedTask; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs index 8f3d33466..5db9391ba 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs @@ -5,6 +5,12 @@ namespace Elsa.Workflows.Runtime; public class StoreCommitStateHandler(IWorkflowInstanceManager workflowInstanceManager) : ICommitStateHandler { + public async Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken = default) + { + var workflowState = workflowInstanceManager.ExtractWorkflowState(workflowExecutionContext); + await CommitAsync(workflowExecutionContext, workflowState, cancellationToken); + } + public async Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, CancellationToken cancellationToken = default) { await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); From 6945eb191a9c648774f48be168a8763548b3b0fa Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jan 2025 19:29:19 +0200 Subject: [PATCH 106/166] Upgrade to NUKE 9.0.4 and fix automatic workflow generation --- .github/workflows/pr.yml | 7 ++-- .nuke/build.schema.json | 54 ++++++++++++++++-------------- build/Build.CI.GitHubActions.cs | 59 +++++++++++++++++++++++++++++++-- build/_build.csproj | 2 +- 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bb879d54b..d99b63e7f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,7 +5,7 @@ # # - To turn off auto-generation set: # -# [GitHubActions (AutoGenerate = false)] +# [CustomGitHubActions (AutoGenerate = false)] # # - To trigger manual generation invoke: # @@ -32,9 +32,10 @@ jobs: name: ubuntu-latest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.x + dotnet-version: | + 9.x + - uses: actions/checkout@v4 - name: 'Run: Compile, Test, Pack' run: ./build.cmd Compile Test Pack diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 64c8bccf1..839100960 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,28 +1,5 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "properties": { - "AnalyseCode": { - "type": "boolean" - }, - "Configuration": { - "type": "string", - "enum": [ - "Debug", - "Release" - ] - }, - "IgnoreFailedSources": { - "type": "boolean", - "description": "Ignore unreachable sources during Restore" - }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, - "Version": { - "type": "string" - } - }, "definitions": { "Host": { "type": "string", @@ -122,5 +99,34 @@ } } }, - "$ref": "#/definitions/NukeBuild" + "allOf": [ + { + "properties": { + "AnalyseCode": { + "type": "boolean" + }, + "Configuration": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + }, + "IgnoreFailedSources": { + "type": "boolean", + "description": "Ignore unreachable sources during Restore" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Version": { + "type": "string" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] } diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index 7b3097658..1d2847d54 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -1,7 +1,11 @@ +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; -[GitHubActions( +[CustomGitHubActions( "pr", GitHubActionsImage.UbuntuLatest, OnPullRequestBranches = ["main"], @@ -12,4 +16,55 @@ using Nuke.Components; ConcurrencyCancelInProgress = true ) ] -public partial class Build; \ No newline at end of file +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); + + // only need to list the ones that are missing from default image + newSteps.Insert(0, new GitHubActionsSetupDotNetStep(["9.x"])); + + job.Steps = newSteps.ToArray(); + return job; + } +} + +class GitHubActionsSetupDotNetStep : GitHubActionsStep +{ + public GitHubActionsSetupDotNetStep(string[] versions) + { + Versions = versions; + } + + string[] Versions { get; } + + public override void Write(CustomFileWriter writer) + { + writer.WriteLine("- uses: actions/setup-dotnet@v4"); + + using (writer.Indent()) + { + writer.WriteLine("with:"); + using (writer.Indent()) + { + writer.WriteLine("dotnet-version: |"); + using (writer.Indent()) + { + foreach (var version in Versions) + { + writer.WriteLine(version); + } + } + } + } + } +} \ No newline at end of file diff --git a/build/_build.csproj b/build/_build.csproj index 66629bd30..acca9bc0d 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -19,7 +19,7 @@ - + From e806b73f18a36505c7fb8087354df77c01d99df0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 08:59:02 +0100 Subject: [PATCH 107/166] Update Oracle.EntityFrameworkCore package to version 9.23.60 Upgraded the Oracle.EntityFrameworkCore package from version 8.23.70 to 9.23.60. This update ensures compatibility with newer features and resolves any potential issues fixed in the latest release. No changes were made to other package versions. --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d53a5dc7..75a8cc06a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -184,7 +184,7 @@ - + From c93b063106b3e2dd227e75ae2b213a36d9b529ba Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 11:29:02 +0100 Subject: [PATCH 108/166] Fix variable mapping and aliasing --- .../Mappers/VariableDefinitionMapper.cs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs index 48c481de0..79a7d8ec6 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/VariableDefinitionMapper.cs @@ -20,28 +20,33 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist { var aliasedType = wellKnownTypeRegistry.TryGetType(source.TypeName, out var aliasedTypeValue) ? aliasedTypeValue : null; var type = aliasedType ?? Type.GetType(source.TypeName); - - if(type == null) + + if (type == null) { logger.LogWarning("Failed to resolve the type {TypeName} of variable {VariableName}. Variable will not be mapped.", source.TypeName, source.Name); return null; } - var valueType = aliasedType ?? (source.IsArray ? type.MakeArrayType() : type); + var valueType = aliasedType is { IsArray: true } ? source.IsArray ? aliasedType.MakeArrayType() : aliasedType : source.IsArray ? type.MakeArrayType() : type; var variableGenericType = typeof(Variable<>).MakeGenericType(valueType); var variable = (Variable)Activator.CreateInstance(variableGenericType)!; - if(!string.IsNullOrEmpty(source.Id)) + if (!string.IsNullOrEmpty(source.Id)) variable.Id = source.Id; - + variable.Name = source.Name; - source.Value?.TryConvertTo(valueType).OnSuccess(value => + + if (!string.IsNullOrWhiteSpace(source.Value)) { - variable.Value = value; - }).OnFailure(ex => - { - logger.LogWarning(ex, "Failed to convert the default value {DefaultValue} of variable {VariableName} to its type {VariableType}. Default value will not be set.", source.Value, source.Name, valueType); - }); + source.Value?.TryConvertTo(valueType).OnSuccess(value => + { + variable.Value = value; + }).OnFailure(ex => + { + logger.LogWarning(ex, "Failed to convert the default value {DefaultValue} of variable {VariableName} to its type {VariableType}. Default value will not be set.", source.Value, source.Name, valueType); + }); + } + variable.StorageDriverType = !string.IsNullOrEmpty(source.StorageDriverTypeName) ? Type.GetType(source.StorageDriverTypeName) : null; return variable; @@ -68,15 +73,16 @@ public class VariableDefinitionMapper(IWellKnownTypeRegistry wellKnownTypeRegist var value = source.Value; var serializedValue = value.Format(); var storageDriverTypeName = source.StorageDriverType?.GetSimpleAssemblyQualifiedName(); - - if(valueTypeAlias != null) + + // Handles the case where an alias exists for an array or collection type. E.g. byte[] -> ByteArray. + if (valueTypeAlias != null && (valueType.IsArray || valueType.IsCollectionType())) return new(source.Id, source.Name, valueTypeAlias, false, serializedValue, storageDriverTypeName); - + var isArray = valueType.IsArray; var isCollection = valueType.IsCollectionType(); - var elementValueType = isArray ? valueType.GetElementType() : isCollection ? valueType.GenericTypeArguments[0] : valueType; + var elementValueType = isArray ? valueType.GetElementType() : isCollection ? valueType.GenericTypeArguments[0] : valueType; var elementTypeAlias = wellKnownTypeRegistry.GetAliasOrDefault(elementValueType); - + return new(source.Id, source.Name, elementTypeAlias, isArray, serializedValue, storageDriverTypeName); } From 7a21f09ae96c176ac80872b12687ccda16ecad7b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 15:49:25 +0100 Subject: [PATCH 109/166] Restore methods for traversing activity execution context hierarchy Reintroduced `GetAncestors` method and added new methods: `GetDescendants`, `GetActiveChildren`, and `GetChildren`. These methods enhance navigation through activity execution contexts by providing easy access to hierarchical relationships like ancestors, descendants, and children. --- .../ActivityExecutionContextExtensions.cs | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 739b0d8eb..59a69c68d 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -183,20 +183,6 @@ public static partial class ActivityExecutionContextExtensions return portProperty.GetCustomAttribute()?.Name ?? portProperty.Name; } - /// - /// Returns a flattened list of the current context's ancestors. - /// - public static IEnumerable GetAncestors(this ActivityExecutionContext context) - { - var current = context.ParentActivityExecutionContext; - - while (current != null) - { - yield return current; - current = current.ParentActivityExecutionContext; - } - } - /// /// Send a signal up the current hierarchy of ancestors. /// @@ -314,6 +300,68 @@ public static partial class ActivityExecutionContextExtensions return null; } + + /// + /// Returns a flattened list of the current context's ancestors. + /// + public static IEnumerable GetAncestors(this ActivityExecutionContext context) + { + var current = context.ParentActivityExecutionContext; + + while (current != null) + { + yield return current; + current = current.ParentActivityExecutionContext; + } + } + + /// + /// Returns a flattened list of the current context's descendants. + /// + public static IEnumerable GetDescendents(this ActivityExecutionContext context) + { + var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); + + foreach (var child in children) + { + yield return child; + + foreach (var descendent in GetDescendents(child)) + yield return descendent; + } + } + + /// + /// Returns a flattened list of the current context's immediate active children. + /// + public static IEnumerable GetActiveChildren(this ActivityExecutionContext context) + { + return context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); + } + + /// + /// Returns a flattened list of the current context's immediate children. + /// + public static IEnumerable GetChildren(this ActivityExecutionContext context) + { + return context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context); + } + + /// + /// Returns a flattened list of the current context's descendants. + /// + public static IEnumerable GetDescendants(this ActivityExecutionContext context) + { + var children = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); + + foreach (var child in children) + { + yield return child; + + foreach (var descendant in child.GetDescendants()) + yield return descendant; + } + } internal static bool GetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties.TryGetValue("HasEvaluatedProperties", out var value) && value; internal static void SetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties["HasEvaluatedProperties"] = true; From be94a51b8c35fc58abc4d48d84e4b95b258482bf Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 18:44:47 +0100 Subject: [PATCH 110/166] Add validation for unique input/output names in workflows Introduces a `ValidateWorkflow` notification handler to enforce validation of unique input and output names in workflows. Adds validation errors when duplicate names are detected, improving the integrity of workflow definitions. Integrates the handler into the workflow management feature. --- .../Features/WorkflowManagementFeature.cs | 1 + .../Handlers/Notification/ValidateWorkflow.cs | 31 +++++++++++++++++++ ...est.cs => WorkflowDefinitionValidating.cs} | 0 3 files changed, 32 insertions(+) create mode 100644 src/modules/Elsa.Workflows.Management/Handlers/Notification/ValidateWorkflow.cs rename src/modules/Elsa.Workflows.Management/Notifications/{ValidateWorkflowRequest.cs => WorkflowDefinitionValidating.cs} (100%) diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index a46f191ed..016bdb488 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -241,6 +241,7 @@ public class WorkflowManagementFeature : FeatureBase .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() + .AddNotificationHandler() ; Services.Configure(options => diff --git a/src/modules/Elsa.Workflows.Management/Handlers/Notification/ValidateWorkflow.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notification/ValidateWorkflow.cs new file mode 100644 index 000000000..4e3ed5258 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notification/ValidateWorkflow.cs @@ -0,0 +1,31 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Notifications; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.Management.Handlers.Notification; + +public class ValidateWorkflow : INotificationHandler +{ + public Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken) + { + var workflow = notification.Workflow; + var inputs = workflow.Inputs; + var outputs = workflow.Outputs; + + ValidateUniqueNames(inputs, "inputs", notification.ValidationErrors); + ValidateUniqueNames(outputs, "outputs", notification.ValidationErrors); + + return Task.CompletedTask; + } + + private void ValidateUniqueNames(IEnumerable variables, string variableType, ICollection validationErrors) + { + var duplicateNames = variables.GroupBy(x => x.Name).Where(x => x.Count() > 1).Select(x => x.Key).ToList(); + if (duplicateNames.Any()) + { + var message = $"The following {variableType} are defined more than once: {string.Join(", ", duplicateNames)}"; + validationErrors.Add(new(message)); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Notifications/ValidateWorkflowRequest.cs b/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionValidating.cs similarity index 100% rename from src/modules/Elsa.Workflows.Management/Notifications/ValidateWorkflowRequest.cs rename to src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionValidating.cs From ea8c3da169b26448b41a54fed64c6aa889c75d3f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 18:55:28 +0100 Subject: [PATCH 111/166] Refactor object initialization and remove unused import. Updated the object initialization syntax for cleaner code and removed an unused import in WorkflowValidator.cs to improve code readability and maintainability. --- .../Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs | 2 +- .../Elsa.Workflows.Management/Services/WorkflowValidator.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs index 1c36f7fb1..309ff4c9d 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/BulkPublish/Endpoint.cs @@ -70,6 +70,6 @@ internal class BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPu updatedConsumers.AddRange(result.AffectedWorkflows.WorkflowDefinitions.Select(x => x.DefinitionId)); } - return new Response(published, alreadyPublished, notFound, skipped, updatedConsumers); + return new(published, alreadyPublished, notFound, skipped, updatedConsumers); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs index ed43c2bad..666a60856 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowValidator.cs @@ -2,7 +2,6 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities; using Elsa.Workflows.Management.Models; using Elsa.Workflows.Management.Notifications; -using Elsa.Workflows.Management.Requests; namespace Elsa.Workflows.Management.Services; From 8cb43db070efc0ed3e2b354a5cf975ead5d14f2f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 20:07:23 +0100 Subject: [PATCH 112/166] Refactor workflow state persistence for improved consistency. Removed obsolete middleware for persisting bookmarks, execution logs, and variables, integrating their functionality into the commit state handler. Added early exit checks for empty collections in persistence methods. Updated activity invoker logic for better state commit handling during execution. --- .../Elsa.EntityFrameworkCore.Common/Store.cs | 12 ++++++++++-- .../DefaultActivityInvokerMiddleware.cs | 4 ++-- ...rkflowExecutionPipelineBuilderExtensions.cs | 6 +++--- .../PersistActivityExecutionLogMiddleware.cs | 8 +++++--- .../Workflows/PersistBookmarkMiddleware.cs | 18 ++++++------------ .../PersistWorkflowExecutionLogMiddleware.cs | 7 ++++--- .../Workflows/PersistentVariablesMiddleware.cs | 17 +++-------------- .../Services/BookmarkUpdater.cs | 18 ++++++++++++------ .../Services/StoreActivityExecutionLogSink.cs | 4 ++++ .../Services/StoreCommitStateHandler.cs | 15 ++++++++++++++- .../Services/StoreWorkflowExecutionLogSink.cs | 4 ++++ 11 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs index d5b8fdd6c..3387e912a 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Store.cs @@ -79,9 +79,13 @@ public class Store(IDbContextFactory dbContextF Func? onSaving = default, CancellationToken cancellationToken = default) { - await using var dbContext = await CreateDbContextAsync(cancellationToken); var entityList = entities.ToList(); + if (entityList.Count == 0) + return; + + await using var dbContext = await CreateDbContextAsync(cancellationToken); + if (onSaving != null) { var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, cancellationToken).AsTask()).ToList(); @@ -162,9 +166,13 @@ public class Store(IDbContextFactory dbContextF Func? onSaving = default, CancellationToken cancellationToken = default) { - await using var dbContext = await CreateDbContextAsync(cancellationToken); var entityList = entities.ToList(); + if (entityList.Count == 0) + return; + + await using var dbContext = await CreateDbContextAsync(cancellationToken); + if (onSaving != null) { var savingTasks = entityList.Select(entity => onSaving(dbContext, entity, cancellationToken).AsTask()).ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index a7bd74c6b..a1d884d10 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -49,7 +49,7 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I } // Conditionally commit the workflow state. - if(ShouldCommitWhenStarting(context)) + if(ShouldCommitWhenExecuting(context)) await context.WorkflowExecutionContext.CommitAsync(); context.TransitionTo(ActivityStatus.Running); @@ -120,7 +120,7 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I await context.EvaluateInputPropertiesAsync(); } - private bool ShouldCommitWhenStarting(ActivityExecutionContext context) + private bool ShouldCommitWhenExecuting(ActivityExecutionContext context) { var behavior = context.Activity.GetCommitStateBehavior(); diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs index cd48e66f6..9affb2058 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -18,9 +18,6 @@ public static class WorkflowExecutionPipelineBuilderExtensions pipelineBuilder .Reset() .UseEngineExceptionHandling() - .UseBookmarkPersistence() - .UseActivityExecutionLogPersistence() - .UseWorkflowExecutionLogPersistence() .UsePersistentVariables() .UseExceptionHandling() .UseDefaultActivityScheduler(); @@ -33,15 +30,18 @@ public static class WorkflowExecutionPipelineBuilderExtensions /// /// Installs middleware that persists bookmarks after workflow execution. /// + [Obsolete("This middleware is no longer used and will be removed in a future version. Bookmarks are now persisted through the commit state handler.")] public static IWorkflowExecutionPipelineBuilder UseBookmarkPersistence(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); /// /// Installs middleware that persists the workflow execution journal. /// + [Obsolete("This middleware is no longer used and will be removed in a future version. Execution logs are now persisted through the commit state handler.")] public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionLogPersistence(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); /// /// Installs middleware that persists activity execution records. /// + [Obsolete("This middleware is no longer used and will be removed in a future version. Activity state is now persisted through the commit state handler.")] public static IWorkflowExecutionPipelineBuilder UseActivityExecutionLogPersistence(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistActivityExecutionLogMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistActivityExecutionLogMiddleware.cs index e81f53fd7..b659c8635 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistActivityExecutionLogMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistActivityExecutionLogMiddleware.cs @@ -1,17 +1,19 @@ using Elsa.Workflows.Pipelines.WorkflowExecution; -using Elsa.Workflows.Runtime.Entities; namespace Elsa.Workflows.Runtime.Middleware.Workflows; /// /// Creates and updates activity execution records from activity execution contexts. /// -public class PersistActivityExecutionLogMiddleware(WorkflowMiddlewareDelegate next, ILogRecordSink sink) : WorkflowExecutionMiddleware(next) +[Obsolete("This middleware is no longer used and will be removed in a future version. Activity state is now persisted through the commit state handler")] +public class PersistActivityExecutionLogMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next) { /// public override async ValueTask InvokeAsync(WorkflowExecutionContext context) { await Next(context); - await sink.PersistExecutionLogsAsync(context); + + // Not used anymore. + //await sink.PersistExecutionLogsAsync(context); } } \ 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 0319baf07..c44b255ab 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistBookmarkMiddleware.cs @@ -1,26 +1,20 @@ using Elsa.Workflows.Pipelines.WorkflowExecution; -using Elsa.Workflows.Runtime.Requests; namespace Elsa.Workflows.Runtime.Middleware.Workflows; /// /// Takes care of loading and persisting bookmarks. /// -public class PersistBookmarkMiddleware : WorkflowExecutionMiddleware +[Obsolete("This middleware is no longer used and will be removed in a future version. Bookmarks are now persisted through the commit state handler")] +public class PersistBookmarkMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next) { - private readonly IBookmarksPersister _bookmarksPersister; - - /// - public PersistBookmarkMiddleware(WorkflowMiddlewareDelegate next, IBookmarksPersister bookmarksPersister) : base(next) - { - _bookmarksPersister = bookmarksPersister; - } - /// public override async ValueTask InvokeAsync(WorkflowExecutionContext context) { await Next(context); - var bookmarkRequest = new UpdateBookmarksRequest(context, context.BookmarksDiff, context.CorrelationId); - await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest); + + // Not used anymore. + // var bookmarkRequest = new UpdateBookmarksRequest(context, context.BookmarksDiff, context.CorrelationId); + // await _bookmarksPersister.PersistBookmarksAsync(bookmarkRequest); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistWorkflowExecutionLogMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistWorkflowExecutionLogMiddleware.cs index 25d863054..4c2d39534 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistWorkflowExecutionLogMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistWorkflowExecutionLogMiddleware.cs @@ -1,12 +1,12 @@ using Elsa.Workflows.Pipelines.WorkflowExecution; -using Elsa.Workflows.Runtime.Entities; namespace Elsa.Workflows.Runtime.Middleware.Workflows; /// /// Takes care of persisting workflow execution log entries. /// -public class PersistWorkflowExecutionLogMiddleware(WorkflowMiddlewareDelegate next, ILogRecordSink sink) : WorkflowExecutionMiddleware(next) +[Obsolete("This middleware is no longer used and will be removed in a future version. Execution logs are now persisted through the commit state handler.")] +public class PersistWorkflowExecutionLogMiddleware(WorkflowMiddlewareDelegate next) : WorkflowExecutionMiddleware(next) { /// public override async ValueTask InvokeAsync(WorkflowExecutionContext context) @@ -14,6 +14,7 @@ public class PersistWorkflowExecutionLogMiddleware(WorkflowMiddlewareDelegate ne // Invoke next middleware. await Next(context); - await sink.PersistExecutionLogsAsync(context); + // Not used anymore. + //await sink.PersistExecutionLogsAsync(context); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistentVariablesMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistentVariablesMiddleware.cs index b743983da..ab464717d 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistentVariablesMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Workflows/PersistentVariablesMiddleware.cs @@ -5,28 +5,17 @@ namespace Elsa.Workflows.Runtime.Middleware.Workflows; /// /// Takes care of loading and persisting workflow variables. /// -public class PersistentVariablesMiddleware : WorkflowExecutionMiddleware +public class PersistentVariablesMiddleware(WorkflowMiddlewareDelegate next, IVariablePersistenceManager variablePersistenceManager) : WorkflowExecutionMiddleware(next) { - private readonly IVariablePersistenceManager _variablePersistenceManager; - - /// - /// Constructor. - /// - public PersistentVariablesMiddleware(WorkflowMiddlewareDelegate next, IVariablePersistenceManager variablePersistenceManager) : base(next) - { - _variablePersistenceManager = variablePersistenceManager; - } - /// public override async ValueTask InvokeAsync(WorkflowExecutionContext context) { // Load variables into the workflow execution context. - await _variablePersistenceManager.LoadVariablesAsync(context); + await variablePersistenceManager.LoadVariablesAsync(context); // Invoke next middleware. await Next(context); - // Persist variables. - await _variablePersistenceManager.SaveVariablesAsync(context); + // Variables are persisted through the commit state handler. } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkUpdater.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkUpdater.cs index d42bfab61..d7a9be23e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkUpdater.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkUpdater.cs @@ -11,12 +11,15 @@ public class BookmarkUpdater(IBookmarkManager bookmarkManager, IBookmarkStore bo public async Task UpdateBookmarksAsync(UpdateBookmarksRequest request, CancellationToken cancellationToken = default) { var instanceId = request.WorkflowExecutionContext.Id; - await RemoveBookmarksAsync(instanceId, request.Diff.Removed, cancellationToken); - await StoreBookmarksAsync(request.WorkflowExecutionContext, request.Diff.Added, cancellationToken); + await RemoveBookmarksAsync(instanceId, request.Diff.Removed.ToList(), cancellationToken); + await StoreBookmarksAsync(request.WorkflowExecutionContext, request.Diff.Added.ToList(), cancellationToken); } - - private async Task RemoveBookmarksAsync(string workflowInstanceId, IEnumerable bookmarks, CancellationToken cancellationToken) + + private async Task RemoveBookmarksAsync(string workflowInstanceId, ICollection bookmarks, CancellationToken cancellationToken) { + if (bookmarks.Count == 0) + return; + var matchingIds = bookmarks.Select(x => x.Id).ToList(); var filter = new BookmarkFilter { @@ -25,9 +28,12 @@ public class BookmarkUpdater(IBookmarkManager bookmarkManager, IBookmarkStore bo }; await bookmarkManager.DeleteManyAsync(filter, cancellationToken); } - - private async Task StoreBookmarksAsync(WorkflowExecutionContext context, IEnumerable bookmarks, CancellationToken cancellationToken) + + private async Task StoreBookmarksAsync(WorkflowExecutionContext context, ICollection bookmarks, CancellationToken cancellationToken) { + if (bookmarks.Count == 0) + return; + foreach (var bookmark in bookmarks) { var storedBookmark = context.MapBookmark(bookmark); diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs index 2c280a22c..23cab65a3 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs @@ -15,6 +15,10 @@ public class StoreActivityExecutionLogSink(IActivityExecutionStore activityExecu public async Task PersistExecutionLogsAsync(WorkflowExecutionContext context, CancellationToken cancellationToken = default) { var records = await extractor.ExtractLogRecordsAsync(context).ToList(); + + if(records.Count == 0) + return; + await activityExecutionStore.SaveManyAsync(records, cancellationToken); await notificationSender.SendAsync(new ActivityExecutionLogUpdated(context, records), cancellationToken); } diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs index 5db9391ba..6479c0eb6 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs @@ -1,9 +1,16 @@ using Elsa.Workflows.Management; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.State; namespace Elsa.Workflows.Runtime; -public class StoreCommitStateHandler(IWorkflowInstanceManager workflowInstanceManager) : ICommitStateHandler +public class StoreCommitStateHandler( + IWorkflowInstanceManager workflowInstanceManager, + IBookmarksPersister bookmarkPersister, + IVariablePersistenceManager variablePersistenceManager, + ILogRecordSink activityExecutionLogRecordSink, + ILogRecordSink workflowExecutionLogRecordSink) : ICommitStateHandler { public async Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken = default) { @@ -13,7 +20,13 @@ public class StoreCommitStateHandler(IWorkflowInstanceManager workflowInstanceMa public async Task CommitAsync(WorkflowExecutionContext workflowExecutionContext, WorkflowState workflowState, CancellationToken cancellationToken = default) { + var updateBookmarksRequest = new UpdateBookmarksRequest(workflowExecutionContext, workflowExecutionContext.BookmarksDiff, workflowExecutionContext.CorrelationId); + await bookmarkPersister.PersistBookmarksAsync(updateBookmarksRequest); + await activityExecutionLogRecordSink.PersistExecutionLogsAsync(workflowExecutionContext, cancellationToken); + await workflowExecutionLogRecordSink.PersistExecutionLogsAsync(workflowExecutionContext, cancellationToken); + await variablePersistenceManager.SaveVariablesAsync(workflowExecutionContext); await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); + workflowExecutionContext.ExecutionLog.Clear(); await workflowExecutionContext.ExecuteDeferredTasksAsync(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs index a91667858..72bce2ebb 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreWorkflowExecutionLogSink.cs @@ -14,6 +14,10 @@ public class StoreWorkflowExecutionLogSink(IWorkflowExecutionLogStore store, ILo public async Task PersistExecutionLogsAsync(WorkflowExecutionContext context, CancellationToken cancellationToken) { var records = await extractor.ExtractLogRecordsAsync(context).ToList(); + + if(records.Count == 0) + return; + await store.AddManyAsync(records, context.CancellationToken); await notificationSender.SendAsync(new WorkflowExecutionLogUpdated(context), context.CancellationToken); } From 2238ffb5398740a83554615c788de477095a3f6d Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Jan 2025 19:39:03 +0000 Subject: [PATCH 113/166] Update BaseSqlClient.cs Remove unnecessary variables. --- src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs index 0298d5493..6ed7dbfd9 100644 --- a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; namespace Elsa.Sql.Client; @@ -12,11 +12,7 @@ public abstract class BaseSqlClient protected static DataSet ReadAsDataSet(IDataReader reader) { var dataSet = new DataSet("dataset"); - - var schematable = reader.GetSchemaTable(); - var data = new DataSet(); dataSet.Tables.Add(ReadAsDataTable(reader)); - return dataSet; } From 6b288e19905f2a0305f31bc851147487b534e82c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 21:13:49 +0100 Subject: [PATCH 114/166] Update SetCommitStateBehavior method for fluent convenience --- src/apps/Elsa.Server.Web/SampleWorkflow.cs | 33 +++++++++++++++++++ .../Extensions/ActivityPropertyExtensions.cs | 6 +++- .../Features/WorkflowRuntimeFeature.cs | 4 +-- ...andler.cs => DefaultCommitStateHandler.cs} | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/apps/Elsa.Server.Web/SampleWorkflow.cs rename src/modules/Elsa.Workflows.Runtime/Services/{StoreCommitStateHandler.cs => DefaultCommitStateHandler.cs} (97%) diff --git a/src/apps/Elsa.Server.Web/SampleWorkflow.cs b/src/apps/Elsa.Server.Web/SampleWorkflow.cs new file mode 100644 index 000000000..e8dbc6da0 --- /dev/null +++ b/src/apps/Elsa.Server.Web/SampleWorkflow.cs @@ -0,0 +1,33 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Extensions; + +namespace Elsa.Server.Web; + +public class SampleWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.WorkflowOptions.CommitStateOptions = new() + { + // Commit state before workflow starts executing. + Starting = true, + + // Commit state before every activity that is about to execute. + ActivityExecuted = true, + + // Commit state after every activity that executed. + ActivityExecuting = true, + }; + builder.Root = new Sequence + { + Activities = + { + new WriteLine("Commit before executing").WithCommitStateBehavior(ActivityCommitStateBehavior.Executing), + new WriteLine("Commit after executing").WithCommitStateBehavior(ActivityCommitStateBehavior.Executed), + new WriteLine("Commit only based on the workflow commit options").WithCommitStateBehavior(ActivityCommitStateBehavior.Default), + new WriteLine("Never commit the workflow when this activity is about to execute or has executed").WithCommitStateBehavior(ActivityCommitStateBehavior.Never), + } + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs index b4c240157..b38442bd8 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs @@ -56,7 +56,11 @@ public static class ActivityPropertyExtensions /// /// Sets the commit state behavior for the specified activity. /// - public static void SetCommitStateBehavior(this IActivity activity, ActivityCommitStateBehavior value) => activity.CustomProperties[CommitStateBehaviorName[0]] = value; + public static TActivity WithCommitStateBehavior(this TActivity activity, ActivityCommitStateBehavior value) where TActivity: IActivity + { + activity.CustomProperties[CommitStateBehaviorName[0]] = value; + return activity; + } /// /// Sets the source file and line number where this activity was instantiated, if any. diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 4ef109615..cca60aaeb 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -193,7 +193,7 @@ public class WorkflowRuntimeFeature : FeatureBase Module.AddActivitiesFrom(); Module.Configure(workflows => { - workflows.CommitStateHandler = sp => sp.GetRequiredService(); + workflows.CommitStateHandler = sp => sp.GetRequiredService(); }); Services.Configure(options => @@ -267,7 +267,7 @@ public class WorkflowRuntimeFeature : FeatureBase .AddScoped, WorkflowExecutionLogRecordExtractor>() .AddScoped() - .AddScoped() + .AddScoped() // Deprecated services. .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs similarity index 97% rename from src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs rename to src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs index 6479c0eb6..61acf737b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs @@ -5,7 +5,7 @@ using Elsa.Workflows.State; namespace Elsa.Workflows.Runtime; -public class StoreCommitStateHandler( +public class DefaultCommitStateHandler( IWorkflowInstanceManager workflowInstanceManager, IBookmarksPersister bookmarkPersister, IVariablePersistenceManager variablePersistenceManager, From 85282f9149a21a403d8ebd83e01214b198ef91c4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jan 2025 21:48:14 +0100 Subject: [PATCH 115/166] Update WorkflowExecutionContext.cs Remove unused property --- .../Contexts/WorkflowExecutionContext.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index df38d706d..b25bb012c 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -239,11 +239,6 @@ public partial class WorkflowExecutionContext : IExecutionContext /// The current sub status of the workflow. public WorkflowSubStatus SubStatus { get; internal set; } - - /// - /// The previous sub status of the workflow. - /// - public WorkflowSubStatus PreviousSubStatus { get; internal set; } /// The root associated with the execution context. public MemoryRegister MemoryRegister { get; private set; } = null!; @@ -627,4 +622,4 @@ public partial class WorkflowExecutionContext : IExecutionContext { return _commitStateHandler.CommitAsync(this, CancellationToken); } -} \ No newline at end of file +} From 8e88de546d0235c263018e3f3aede5a818564acc Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 00:11:25 +0100 Subject: [PATCH 116/166] Incremental work on restoring previous workflow runtime API surface for backward compatibility --- .../Contracts/IWorkflowInstanceManager.cs | 7 +- .../Services/WorkflowInstanceManager.cs | 10 + .../Proto/WorkflowInstance.proto | 1 + .../Services/ProtoActorWorkflowClient.cs | 5 + .../ProtoActorWorkflowRuntime.Obsolete.cs | 375 ++++++++++++++++++ .../Services/ProtoActorWorkflowRuntime.cs | 9 +- .../Contracts/IWorkflowClient.cs | 2 + .../Contracts/IWorkflowRuntime.cs | 107 +++++ .../Filters/WorkflowsFilter.cs | 11 + .../Matches/ResumableWorkflowMatch.cs | 6 + .../Matches/StartableWorkflowMatch.cs | 4 + .../Matches/WorkflowMatch.cs | 5 + .../Messages/RunWorkflowInstanceResponse.cs | 2 + .../Params/ExecuteWorkflowParams.cs | 2 + .../Requests/CountRunningWorkflowsRequest.cs | 22 + .../Responses/StartWorkflowResponse.cs | 2 + .../Services/DefaultWorkflowStarter.cs | 5 +- .../Services/LocalWorkflowClient.cs | 5 + .../Services/LocalWorkflowRuntime.Obsolete.cs | 241 +++++++++++ .../Services/LocalWorkflowRuntime.cs | 15 +- 20 files changed, 831 insertions(+), 5 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Matches/ResumableWorkflowMatch.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Matches/StartableWorkflowMatch.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Matches/WorkflowMatch.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs index 2ae45c492..886160157 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceManager.cs @@ -20,7 +20,12 @@ public interface IWorkflowInstanceManager /// Finds the first workflow instance that matches the specified filter. /// Task FindAsync(WorkflowInstanceFilter filter, CancellationToken cancellationToken = default); - + + /// + /// Determines whether a workflow instance with the specified ID exists. + /// + Task ExistsAsync(string instanceId, CancellationToken cancellationToken = default); + /// /// Saves the specified workflow instance. /// diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs index 12bc8ca29..f33e43a6d 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceManager.cs @@ -33,6 +33,16 @@ public class WorkflowInstanceManager( return await store.FindAsync(filter, cancellationToken); } + public async Task ExistsAsync(string instanceId, CancellationToken cancellationToken = default) + { + var filter = new WorkflowInstanceFilter + { + Id = instanceId + }; + var count = await store.CountAsync(filter, cancellationToken); + return count > 0; + } + /// public async Task SaveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken = default) { diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto index a23ca9ab7..47088c1d2 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto @@ -15,4 +15,5 @@ service WorkflowInstance { rpc Cancel (Empty) returns (Empty); rpc ExportState(Empty) returns (ExportWorkflowStateResponse); rpc ImportState(ImportWorkflowStateRequest) returns (Empty); + rpc InstanceExists(Empty) returns (bool); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowClient.cs index 7576d136f..a37cb11b3 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowClient.cs @@ -85,6 +85,11 @@ public class ProtoActorWorkflowClient : IWorkflowClient await _actorClient.ImportState(request, CreateHeaders(), cancellationToken); } + public Task InstanceExistsAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + private IDictionary CreateHeaders() { var headers = new Dictionary(); diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs new file mode 100644 index 000000000..629322c83 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -0,0 +1,375 @@ +using System.Diagnostics.CodeAnalysis; +using Elsa.Common.Models; +using Elsa.Extensions; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Matches; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Parameters; +using Elsa.Workflows.Runtime.Params; +using Elsa.Workflows.Runtime.ProtoActor.Extensions; +using Elsa.Workflows.Runtime.ProtoActor.ProtoBuf; +using Elsa.Workflows.Runtime.Requests; +using Elsa.Workflows.Runtime.Results; +using Elsa.Workflows.State; + +namespace Elsa.Workflows.Runtime.ProtoActor.Services; + +public partial class ProtoActorWorkflowRuntime +{ + /// + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); + var workflow = workflowGraph!.Workflow; + + var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() + { + Workflow = workflow, + CorrelationId = options?.CorrelationId, + CancellationToken = cancellationToken + }); + + return new CanStartWorkflowResult( + { + CanStart = canStart, + InstanceId = null + }; + } + + /// + public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new CreateAndRunWorkflowInstanceRequest + { + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId + }; + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + /// + public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new Workflows.Runtime.Messages.CreateAndRunWorkflowInstanceRequest + { + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = Workflows.Models.WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId + }; + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + /// + public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + { + var hash = _hasher.Hash(activityTypeName, bookmarkPayload); + var filter = new TriggerFilter + { + Hash = hash + }; + var systemCancellationToken = options?.CancellationTokens.SystemCancellationToken ?? default; + var triggers = await _triggerStore.FindManyAsync(filter, systemCancellationToken); + var results = new List(); + + foreach (var trigger in triggers) + { + var definitionId = trigger.WorkflowDefinitionId; + + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = options?.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = trigger.ActivityId, + InstanceId = options?.WorkflowInstanceId, + CancellationTokens = options?.CancellationTokens ?? default + }; + + var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions); + + // If we can't start the workflow, don't try it. + if (!canStartResult.CanStart) + continue; + + var startResult = await StartWorkflowAsync(definitionId, startOptions); + results.Add(startResult); + } + + return results; + } + + /// + public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = default) + { + var request = new ResumeWorkflowRequest + { + InstanceId = workflowInstanceId, + CorrelationId = options?.CorrelationId.EmptyIfNull(), + BookmarkId = options?.BookmarkId.EmptyIfNull(), + ActivityId = options?.ActivityId.EmptyIfNull(), + Input = options?.Input?.SerializeInput(), + Properties = options?.Properties?.SerializeProperties(), + }; + + var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); + var response = await client.Resume(request, options?.CancellationTokens.SystemCancellationToken ?? default); + + return _workflowExecutionResultMapper.Map(response!); + } + + /// + public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + { + var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options?.ActivityInstanceId); + var correlationId = options?.CorrelationId; + var workflowInstanceId = options?.WorkflowInstanceId; + var filter = new BookmarkFilter + { + Hash = hash, + CorrelationId = correlationId, + WorkflowInstanceId = workflowInstanceId + }; + var bookmarks = await _bookmarkStore.FindManyAsync(filter, options?.CancellationTokens.SystemCancellationToken ?? default); + + return await ResumeWorkflowsAsync( + bookmarks, + new ResumeWorkflowRuntimeParams + { + CorrelationId = correlationId, + Input = options?.Input, + Properties = options?.Properties, + CancellationTokens = options?.CancellationTokens ?? default + } + ); + } + + /// + public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + { + var startedWorkflows = await StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + var resumedWorkflows = await ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + var results = startedWorkflows.Concat(resumedWorkflows).ToList(); + + return new TriggerWorkflowsResult(results); + } + + /// + public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) + { + if (match is StartableWorkflowMatch collectedStartableWorkflow) + { + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = collectedStartableWorkflow.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = collectedStartableWorkflow.ActivityId, + InstanceId = collectedStartableWorkflow.WorkflowInstanceId, + CancellationTokens = options?.CancellationTokens ?? default + }; + return await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); + } + + var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; + var runtimeOptions = new ResumeWorkflowRuntimeParams + { + CorrelationId = collectedResumableWorkflow.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + BookmarkId = collectedResumableWorkflow.BookmarkId, + CancellationTokens = options?.CancellationTokens ?? default + }; + var result = await ResumeWorkflowAsync(match.WorkflowInstanceId, runtimeOptions); + + return result!; + } + + /// + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken) + { + var filter = new WorkflowInstanceFilter + { + Id = workflowInstanceId + }; + + var instance = await _workflowInstanceStore.FindAsync(filter, cancellationToken); + if (instance is null) + return new CancellationResult(false, FailureReason.NotFound); + + var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); + var result = await client.Cancel(cancellationToken); + return new CancellationResult(result?.Result ?? false); + } + + /// + public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); + var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); + var results = startableWorkflows.Concat(resumableWorkflows).ToList(); + return results; + } + + /// + [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.DeserializeAsync(String, CancellationToken)")] + public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); + var response = await client.ExportState(new ExportWorkflowStateRequest(), cancellationToken); + var json = response!.SerializedWorkflowState.Text; + var workflowState = await _workflowStateSerializer.DeserializeAsync(json, cancellationToken); + return workflowState; + } + + /// + [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.SerializeAsync(WorkflowState, CancellationToken)")] + public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var client = _cluster.GetNamedWorkflowGrain(workflowState.Id); + var json = await _workflowStateSerializer.SerializeAsync(workflowState, cancellationToken); + + var request = new ImportWorkflowStateRequest + { + SerializedWorkflowState = new Json + { + Text = json + } + }; + + await client.ImportState(request, cancellationToken); + } + + /// + public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) + { + await _bookmarkStore.SaveAsync(bookmark, cancellationToken); + } + + /// + public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) + { + var filter = new WorkflowInstanceFilter + { + DefinitionId = request.DefinitionId, + Version = request.Version, + CorrelationId = request.CorrelationId, + WorkflowStatus = WorkflowStatus.Running + }; + return await _workflowInstanceStore.CountAsync(filter, cancellationToken); + } + + private async Task> ResumeWorkflowsAsync(IEnumerable bookmarks, ResumeWorkflowRuntimeParams runtimeParams) + { + var resumedWorkflows = new List(); + + foreach (var bookmark in bookmarks) + { + var workflowInstanceId = bookmark.WorkflowInstanceId; + + var newRuntimeOptions = new ResumeWorkflowRuntimeParams + { + CorrelationId = runtimeParams.CorrelationId, + Input = runtimeParams.Input, + Properties = runtimeParams.Properties, + BookmarkId = bookmark.BookmarkId, + ActivityId = runtimeParams.ActivityId, + ActivityNodeId = runtimeParams.ActivityNodeId, + ActivityInstanceId = runtimeParams.ActivityInstanceId, + ActivityHash = runtimeParams.ActivityHash, + CancellationTokens = runtimeParams.CancellationTokens + }; + + var resumeResult = await ResumeWorkflowAsync(workflowInstanceId, newRuntimeOptions); + resumedWorkflows.Add(resumeResult!); + } + + return resumedWorkflows; + } + + private async Task> FindStartableWorkflowsAsync(WorkflowsFilter workflowsFilter, CancellationToken cancellationToken) + { + var hash = _hasher.Hash(workflowsFilter.ActivityTypeName, workflowsFilter.BookmarkPayload); + var filter = new TriggerFilter + { + Hash = hash + }; + var triggers = await _triggerStore.FindManyAsync(filter, cancellationToken); + var results = new List(); + + foreach (var trigger in triggers) + { + var definitionId = trigger.WorkflowDefinitionId; + + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = workflowsFilter.Options?.CorrelationId, + Input = workflowsFilter.Options.Input, + Properties = workflowsFilter.Options.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = trigger.ActivityId, + CancellationTokens = cancellationToken + }; + + var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions); + var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(trigger.WorkflowDefinitionVersionId, cancellationToken); + + if (workflowGraph == null) + { + _logger.LogWarning("Workflow version ID {DefinitionVersionId} not found", trigger.WorkflowDefinitionVersionId); + continue; + } + + var workflow = workflowGraph.Workflow; + var createWorkflowInstanceRequest = new CreateWorkflowInstanceRequest + { + Workflow = workflow, + CorrelationId = workflowsFilter.Options.CorrelationId, + WorkflowInstanceId = workflowsFilter.Options?.WorkflowInstanceId, + Input = workflowsFilter.Options?.Input, + Properties = workflowsFilter.Options?.Properties + }; + var workflowInstance = _workflowInstanceFactory.CreateWorkflowInstance(createWorkflowInstanceRequest); + + if (canStartResult.CanStart) + results.Add(new StartableWorkflowMatch(workflowInstance.Id, workflowInstance, workflowsFilter.Options?.CorrelationId, trigger.ActivityId, definitionId, trigger.Payload)); + } + + return results; + } + + private async Task> FindResumableWorkflowsAsync(WorkflowsFilter workflowsFilter, CancellationToken cancellationToken) + { + var hash = _hasher.Hash(workflowsFilter.ActivityTypeName, workflowsFilter.BookmarkPayload); + var correlationId = workflowsFilter.Options.CorrelationId; + var workflowInstanceId = workflowsFilter.Options.WorkflowInstanceId; + var activityInstanceId = workflowsFilter.Options.ActivityInstanceId; + var filter = new BookmarkFilter + { + Hash = hash, + CorrelationId = correlationId, + WorkflowInstanceId = workflowInstanceId, + ActivityInstanceId = activityInstanceId + }; + var bookmarks = await _bookmarkStore.FindManyAsync(filter, cancellationToken); + var collectedWorkflows = bookmarks.Select(b => new ResumableWorkflowMatch(b.WorkflowInstanceId, default, correlationId, b.BookmarkId, b.Payload)).ToList(); + return collectedWorkflows; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index 8ba6c1bd8..ce1266342 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -1,11 +1,18 @@ +using Elsa.Workflows.Management; using Microsoft.Extensions.DependencyInjection; +using Proto.Cluster; namespace Elsa.Workflows.Runtime.ProtoActor.Services; /// /// Represents a Proto.Actor implementation of the workflows runtime. /// -public class ProtoActorWorkflowRuntime(IServiceProvider serviceProvider, IIdentityGenerator identityGenerator) : IWorkflowRuntime +public partial class ProtoActorWorkflowRuntime( + IServiceProvider serviceProvider, + IWorkflowDefinitionService workflowDefinitionService, + IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, + Cluster cluster, + IIdentityGenerator identityGenerator) : IWorkflowRuntime { /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowClient.cs index aecfafdd3..8753a76bd 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowClient.cs @@ -42,4 +42,6 @@ public interface IWorkflowClient /// Imports the specified . /// Task ImportStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default); + + Task InstanceExistsAsync(CancellationToken cancellationToken = default); } \ 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 cbef32bcc..5ef752118 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs @@ -1,3 +1,13 @@ +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Matches; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Parameters; +using Elsa.Workflows.Runtime.Params; +using Elsa.Workflows.Runtime.Requests; +using Elsa.Workflows.Runtime.Results; +using Elsa.Workflows.State; + namespace Elsa.Workflows.Runtime; /// @@ -21,4 +31,101 @@ public interface IWorkflowRuntime /// A new instance. /// The workflow instance itself doesn't have to exist yet. ValueTask CreateClientAsync(string? workflowInstanceId, CancellationToken cancellationToken = default); + + + /// + /// Returns a value whether the specified workflow definition can create a new instance. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default); + + /// + /// Creates a new workflow instance of the specified definition ID and executes it. + /// + /// The workflow definition ID to run. + /// Options for starting the workflow. + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default); + + /// + /// Starts all workflows with triggers matching the specified activity type and bookmark payload. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default); + + /// + /// Tries to start a workflow and returns the result if successful. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default); + + /// + /// Resumes an existing workflow instance. + /// + /// The ID of the workflow instance to resume. + /// Options for resuming the workflow. + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = default); + + /// + /// Resumes all workflows that are bookmarked on the specified activity type. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default); + + /// + /// Starts all workflows and resumes existing workflow instances based on the specified activity type and bookmark payload. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default); + + /// + /// Executes a pending workflow. + /// + /// A workflow match to execute. + /// Options for executing the workflow. + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default); + + /// + /// Cancels the execution of a workflow. + /// + /// The ID of the workflow instance to cancel. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default); + + /// + /// Finds all the workflows that can be started or resumed based on a query model. + /// + /// + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default); + + /// + /// Exports the of the specified workflow instance. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default); + + /// + /// Imports the specified . + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default); + + /// + /// Updates the specified bookmark. + /// + /// The bookmark to update. + /// The cancellation token. + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default); + + /// + /// Counts the number of workflow instances based on the provided query args. + /// + [Obsolete("Use the client API instead, retrieved from CreateClientAsync")] + Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs b/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs new file mode 100644 index 000000000..438446c5a --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs @@ -0,0 +1,11 @@ +using Elsa.Workflows.Runtime.Options; + +namespace Elsa.Workflows.Runtime.Filters; + +/// +/// A filter for finding workflows to trigger. +/// +/// The activity type name to trigger workflows for. +/// The bookmark payload to trigger workflows for. +/// The options to use when triggering workflows. +public record WorkflowsFilter(string ActivityTypeName, object BookmarkPayload, TriggerWorkflowsOptions Options); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Matches/ResumableWorkflowMatch.cs b/src/modules/Elsa.Workflows.Runtime/Matches/ResumableWorkflowMatch.cs new file mode 100644 index 000000000..ba6f7669b --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Matches/ResumableWorkflowMatch.cs @@ -0,0 +1,6 @@ +using Elsa.Workflows.Management.Entities; + +namespace Elsa.Workflows.Runtime.Matches; + +public record ResumableWorkflowMatch(string WorkflowInstanceId, string? CorrelationId, string? BookmarkId, object? Payload) + : WorkflowMatch(CorrelationId, Payload); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Matches/StartableWorkflowMatch.cs b/src/modules/Elsa.Workflows.Runtime/Matches/StartableWorkflowMatch.cs new file mode 100644 index 000000000..3103b8c52 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Matches/StartableWorkflowMatch.cs @@ -0,0 +1,4 @@ +namespace Elsa.Workflows.Runtime.Matches; + +public record StartableWorkflowMatch(string? CorrelationId, string? ActivityId, string? DefinitionId, object? Payload) + : WorkflowMatch(CorrelationId, Payload); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Matches/WorkflowMatch.cs b/src/modules/Elsa.Workflows.Runtime/Matches/WorkflowMatch.cs new file mode 100644 index 000000000..541a691c9 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Matches/WorkflowMatch.cs @@ -0,0 +1,5 @@ +using Elsa.Workflows.Management.Entities; + +namespace Elsa.Workflows.Runtime.Matches; + +public record WorkflowMatch(string? CorrelationId, object? Payload); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceResponse.cs b/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceResponse.cs index 73d1a9596..8a49e2273 100644 --- a/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceResponse.cs +++ b/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceResponse.cs @@ -22,6 +22,8 @@ public record RunWorkflowInstanceResponse /// public WorkflowSubStatus SubStatus { get; set; } + public ICollection Bookmarks { get; set; } = new List(); + /// /// Any incidents that occurred during the execution of the workflow instance. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Params/ExecuteWorkflowParams.cs b/src/modules/Elsa.Workflows.Runtime/Params/ExecuteWorkflowParams.cs index e6c72ad91..f9d4ab40c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Params/ExecuteWorkflowParams.cs +++ b/src/modules/Elsa.Workflows.Runtime/Params/ExecuteWorkflowParams.cs @@ -2,6 +2,7 @@ using Elsa.Workflows.Models; namespace Elsa.Workflows.Runtime.Params; +[Obsolete("This type is obsolete.")] public class ExecuteWorkflowParams { public string? CorrelationId { get; set; } @@ -11,4 +12,5 @@ public class ExecuteWorkflowParams public IDictionary? Properties { get; set; } public string? TriggerActivityId { get; set; } public string? ParentWorkflowInstanceId { get; set; } + public CancellationToken CancellationToken { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs b/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs new file mode 100644 index 000000000..9fc36c54f --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs @@ -0,0 +1,22 @@ +namespace Elsa.Workflows.Runtime.Requests; + +/// +/// Contains arguments to use for counting the number of workflow instances. +/// +public class CountRunningWorkflowsRequest +{ + /// + /// The workflow definition ID to include in the query. + /// + public string? DefinitionId { get; set; } + + /// + /// The workflow definition version to include in the query. + /// + public int? Version { get; set; } + + /// + /// The correlation ID to include in the query. + /// + public string? CorrelationId { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Responses/StartWorkflowResponse.cs b/src/modules/Elsa.Workflows.Runtime/Responses/StartWorkflowResponse.cs index e3472732f..6745e54f7 100644 --- a/src/modules/Elsa.Workflows.Runtime/Responses/StartWorkflowResponse.cs +++ b/src/modules/Elsa.Workflows.Runtime/Responses/StartWorkflowResponse.cs @@ -24,6 +24,8 @@ public record StartWorkflowResponse /// The sub-status of the workflow instance. /// public WorkflowSubStatus? SubStatus { get; set; } + + public ICollection Bookmarks { get; set; } = new List(); /// /// Any incidents that occurred during the execution of the workflow instance. diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs index e1c2e79f7..3778ee265 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs @@ -12,7 +12,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio { var workflow = await GetWorkflowAsync(request, cancellationToken); - var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new WorkflowActivationStrategyEvaluationContext + var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() { Workflow = workflow, CorrelationId = request.CorrelationId @@ -41,6 +41,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio WorkflowInstanceId = runWorkflowResponse.WorkflowInstanceId, Status = runWorkflowResponse.Status, SubStatus = runWorkflowResponse.SubStatus, + Bookmarks = runWorkflowResponse.Bookmarks, Incidents = runWorkflowResponse.Incidents }; } @@ -56,7 +57,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.WorkflowDefinitionHandle, cancellationToken); if (workflowGraph == null) - throw new WorkflowGraphNotFoundException($"Workflow definition not found.", request.WorkflowDefinitionHandle); + throw new WorkflowGraphNotFoundException("Workflow definition not found.", request.WorkflowDefinitionHandle); return workflowGraph.Workflow; } diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 6b73d2906..8497c99dc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -96,6 +96,11 @@ public class LocalWorkflowClient( await workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken); } + public Task InstanceExistsAsync(CancellationToken cancellationToken = default) + { + return workflowInstanceManager.ExistsAsync(workflowInstanceId, cancellationToken); + } + private async Task RunInstanceAsync(WorkflowInstance workflowInstance, RunWorkflowInstanceRequest request, CancellationToken cancellationToken = default) { var workflowState = workflowInstance.WorkflowState; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs new file mode 100644 index 000000000..fcd4bb7a4 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -0,0 +1,241 @@ +using Elsa.Common.Models; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Matches; +using Elsa.Workflows.Runtime.Messages; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Parameters; +using Elsa.Workflows.Runtime.Params; +using Elsa.Workflows.Runtime.Requests; +using Elsa.Workflows.Runtime.Results; +using Elsa.Workflows.State; +using Open.Linq.AsyncExtensions; + +namespace Elsa.Workflows.Runtime; + +public partial class LocalWorkflowRuntime +{ + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); + var workflow = workflowGraph!.Workflow; + + var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() + { + Workflow = workflow, + CorrelationId = options?.CorrelationId, + CancellationToken = cancellationToken + }); + + return new CanStartWorkflowResult( + { + CanStart = canStart, + InstanceId = null + }; + } + + public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new CreateAndRunWorkflowInstanceRequest + { + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId + }; + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + return await StartWorkflowAsync(definitionId, options); + } + + public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); + var exists = await workflowClient.InstanceExistsAsync(cancellationToken); + + if (!exists) + return null; + + var runWorkflowRequest = new RunWorkflowInstanceRequest + { + Input = options?.Input, + Properties = options?.Properties, + ActivityHandle = options?.ActivityHandle, + BookmarkId = options?.BookmarkId + }; + + var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); + + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return new(results); + } + + public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + if (match is StartableWorkflowMatch collectedStartableWorkflow) + { + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = collectedStartableWorkflow.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = collectedStartableWorkflow.ActivityId, + CancellationToken = cancellationToken + }; + + var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); + return startResult with + { + TriggeredActivityId = collectedStartableWorkflow.ActivityId + }; + } + + var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; + var runtimeOptions = new ResumeWorkflowRuntimeParams + { + CorrelationId = collectedResumableWorkflow.CorrelationId, + BookmarkId = collectedResumableWorkflow.BookmarkId, + Input = options?.Input, + Properties = options?.Properties, + CancellationToken = cancellationToken, + }; + + return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; + } + + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + await client.CancelAsync(cancellationToken); + return new(true); + } + + public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); + var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); + var results = startableWorkflows.Concat(resumableWorkflows).ToList(); + return results; + } + + public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + return await client.ExportStateAsync(cancellationToken); + } + + public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowState.Id, cancellationToken); + await client.ImportStateAsync(workflowState, cancellationToken); + } + + public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) + { + await bookmarkStore.SaveAsync(bookmark, cancellationToken); + } + + public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) + { + var filter = new WorkflowInstanceFilter + { + DefinitionId = request.DefinitionId, + Version = request.Version, + CorrelationId = request.CorrelationId, + WorkflowStatus = WorkflowStatus.Running + }; + return await workflowInstanceStore.CountAsync(filter, cancellationToken); + } + + private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); + var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); + var correlationId = filter.Options.CorrelationId; + + var query = + from triggerBoundWorkflow in triggerBoundWorkflows + from trigger in triggerBoundWorkflow.Triggers + select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); + + return query.ToList(); + } + + private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) + { + var bookmarkOptions = new FindBookmarkOptions + { + CorrelationId = filter.Options.CorrelationId, + WorkflowInstanceId = filter.Options.WorkflowInstanceId, + ActivityInstanceId = filter.Options.ActivityInstanceId + }; + var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); + + return ( + from bookmarkBoundWorkflow in bookmarkBoundWorkflows + from bookmark in bookmarkBoundWorkflow.Bookmarks + select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs index dcfa181c7..cfd5309cf 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows.Management; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime; @@ -7,7 +8,19 @@ namespace Elsa.Workflows.Runtime; /// It does not support clustering and is intended for single-node deployments only. /// For distributed deployments, use Proto.Actor or another distributed runtime. /// -public class LocalWorkflowRuntime(IServiceProvider serviceProvider, IIdentityGenerator identityGenerator) : IWorkflowRuntime +public partial class LocalWorkflowRuntime( + IServiceProvider serviceProvider, + IIdentityGenerator identityGenerator, + IWorkflowDefinitionService workflowDefinitionService, + IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, + IStimulusSender stimulusSender, + IBookmarkResumer bookmarkResumer, + IStimulusHasher stimulusHasher, + IWorkflowCanceler workflowCanceler, + IBookmarkStore bookmarkStore, + IWorkflowInstanceStore workflowInstanceStore, + ITriggerBoundWorkflowService triggerBoundWorkflowService, + IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) : IWorkflowRuntime { /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) From db1556fd0b35fe283321e91f063c0486c29e9e9b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 00:21:49 +0100 Subject: [PATCH 117/166] Update ProtoActorWorkflowRuntime to implement backwards-compatible workflow runtime API --- .../ProtoActorWorkflowRuntime.Obsolete.cs | 327 ++++++------------ .../Services/ProtoActorWorkflowRuntime.cs | 8 +- 2 files changed, 116 insertions(+), 219 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs index 629322c83..054afc08b 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using Elsa.Common.Models; -using Elsa.Extensions; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; @@ -8,18 +7,17 @@ using Elsa.Workflows.Runtime.Matches; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Parameters; using Elsa.Workflows.Runtime.Params; -using Elsa.Workflows.Runtime.ProtoActor.Extensions; -using Elsa.Workflows.Runtime.ProtoActor.ProtoBuf; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; +using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime.ProtoActor.Services; public partial class ProtoActorWorkflowRuntime { /// - public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) { var cancellationToken = options?.CancellationToken ?? CancellationToken.None; var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); @@ -40,29 +38,11 @@ public partial class ProtoActorWorkflowRuntime } /// - public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) + public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) { var cancellationToken = options?.CancellationToken ?? CancellationToken.None; var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new CreateAndRunWorkflowInstanceRequest - { - Properties = options?.Properties, - CorrelationId = options?.CorrelationId, - Input = options?.Input, - WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), - ParentId = options?.ParentWorkflowInstanceId, - TriggerActivityId = options?.TriggerActivityId - }; - var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - /// - public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new Workflows.Runtime.Messages.CreateAndRunWorkflowInstanceRequest + var createRequest = new Messages.CreateAndRunWorkflowInstanceRequest { Properties = options?.Properties, CorrelationId = options?.CorrelationId, @@ -76,103 +56,101 @@ public partial class ProtoActorWorkflowRuntime } /// - public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) { - var hash = _hasher.Hash(activityTypeName, bookmarkPayload); - var filter = new TriggerFilter + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new Messages.CreateAndRunWorkflowInstanceRequest { - Hash = hash + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = Workflows.Models.WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId }; - var systemCancellationToken = options?.CancellationTokens.SystemCancellationToken ?? default; - var triggers = await _triggerStore.FindManyAsync(filter, systemCancellationToken); - var results = new List(); + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } - foreach (var trigger in triggers) + /// + public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata { - var definitionId = trigger.WorkflowDefinitionId; - - var startOptions = new StartWorkflowRuntimeParams - { - CorrelationId = options?.CorrelationId, - Input = options?.Input, - Properties = options?.Properties, - VersionOptions = VersionOptions.Published, - TriggerActivityId = trigger.ActivityId, - InstanceId = options?.WorkflowInstanceId, - CancellationTokens = options?.CancellationTokens ?? default - }; - - var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions); - - // If we can't start the workflow, don't try it. - if (!canStartResult.CanStart) - continue; - - var startResult = await StartWorkflowAsync(definitionId, startOptions); - results.Add(startResult); - } - + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); return results; } /// - public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = default) + public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) { - var request = new ResumeWorkflowRequest + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); + var exists = await workflowClient.InstanceExistsAsync(cancellationToken); + + if (!exists) + return null; + + var runWorkflowRequest = new Messages.RunWorkflowInstanceRequest { - InstanceId = workflowInstanceId, - CorrelationId = options?.CorrelationId.EmptyIfNull(), - BookmarkId = options?.BookmarkId.EmptyIfNull(), - ActivityId = options?.ActivityId.EmptyIfNull(), - Input = options?.Input?.SerializeInput(), - Properties = options?.Properties?.SerializeProperties(), + Input = options?.Input, + Properties = options?.Properties, + ActivityHandle = options?.ActivityHandle, + BookmarkId = options?.BookmarkId }; - var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); - var response = await client.Resume(request, options?.CancellationTokens.SystemCancellationToken ?? default); + var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); - return _workflowExecutionResultMapper.Map(response!); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); } /// - public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) { - var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options?.ActivityInstanceId); - var correlationId = options?.CorrelationId; - var workflowInstanceId = options?.WorkflowInstanceId; - var filter = new BookmarkFilter + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata { - Hash = hash, - CorrelationId = correlationId, - WorkflowInstanceId = workflowInstanceId + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input }; - var bookmarks = await _bookmarkStore.FindManyAsync(filter, options?.CancellationTokens.SystemCancellationToken ?? default); - - return await ResumeWorkflowsAsync( - bookmarks, - new ResumeWorkflowRuntimeParams - { - CorrelationId = correlationId, - Input = options?.Input, - Properties = options?.Properties, - CancellationTokens = options?.CancellationTokens ?? default - } - ); + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; } /// - public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = default) + public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) { - var startedWorkflows = await StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); - var resumedWorkflows = await ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); - var results = startedWorkflows.Concat(resumedWorkflows).ToList(); - - return new TriggerWorkflowsResult(results); + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return new(results); } /// - public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) + public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = null) { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; if (match is StartableWorkflowMatch collectedStartableWorkflow) { var startOptions = new StartWorkflowRuntimeParams @@ -182,41 +160,35 @@ public partial class ProtoActorWorkflowRuntime Properties = options?.Properties, VersionOptions = VersionOptions.Published, TriggerActivityId = collectedStartableWorkflow.ActivityId, - InstanceId = collectedStartableWorkflow.WorkflowInstanceId, - CancellationTokens = options?.CancellationTokens ?? default + CancellationToken = cancellationToken + }; + + var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); + return startResult with + { + TriggeredActivityId = collectedStartableWorkflow.ActivityId }; - return await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); } var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; var runtimeOptions = new ResumeWorkflowRuntimeParams { CorrelationId = collectedResumableWorkflow.CorrelationId, + BookmarkId = collectedResumableWorkflow.BookmarkId, Input = options?.Input, Properties = options?.Properties, - BookmarkId = collectedResumableWorkflow.BookmarkId, - CancellationTokens = options?.CancellationTokens ?? default + CancellationToken = cancellationToken, }; - var result = await ResumeWorkflowAsync(match.WorkflowInstanceId, runtimeOptions); - return result!; + return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; } /// public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken) { - var filter = new WorkflowInstanceFilter - { - Id = workflowInstanceId - }; - - var instance = await _workflowInstanceStore.FindAsync(filter, cancellationToken); - if (instance is null) - return new CancellationResult(false, FailureReason.NotFound); - - var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); - var result = await client.Cancel(cancellationToken); - return new CancellationResult(result?.Result ?? false); + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + await client.CancelAsync(cancellationToken); + return new(true); } /// @@ -232,35 +204,22 @@ public partial class ProtoActorWorkflowRuntime [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.DeserializeAsync(String, CancellationToken)")] public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) { - var client = _cluster.GetNamedWorkflowGrain(workflowInstanceId); - var response = await client.ExportState(new ExportWorkflowStateRequest(), cancellationToken); - var json = response!.SerializedWorkflowState.Text; - var workflowState = await _workflowStateSerializer.DeserializeAsync(json, cancellationToken); - return workflowState; + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + return await client.ExportStateAsync(cancellationToken); } /// [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.SerializeAsync(WorkflowState, CancellationToken)")] public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var client = _cluster.GetNamedWorkflowGrain(workflowState.Id); - var json = await _workflowStateSerializer.SerializeAsync(workflowState, cancellationToken); - - var request = new ImportWorkflowStateRequest - { - SerializedWorkflowState = new Json - { - Text = json - } - }; - - await client.ImportState(request, cancellationToken); + var client = await CreateClientAsync(workflowState.Id, cancellationToken); + await client.ImportStateAsync(workflowState, cancellationToken); } /// public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) { - await _bookmarkStore.SaveAsync(bookmark, cancellationToken); + await bookmarkStore.SaveAsync(bookmark, cancellationToken); } /// @@ -273,103 +232,37 @@ public partial class ProtoActorWorkflowRuntime CorrelationId = request.CorrelationId, WorkflowStatus = WorkflowStatus.Running }; - return await _workflowInstanceStore.CountAsync(filter, cancellationToken); + return await workflowInstanceStore.CountAsync(filter, cancellationToken); } - private async Task> ResumeWorkflowsAsync(IEnumerable bookmarks, ResumeWorkflowRuntimeParams runtimeParams) + private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) { - var resumedWorkflows = new List(); + var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); + var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); + var correlationId = filter.Options.CorrelationId; - foreach (var bookmark in bookmarks) - { - var workflowInstanceId = bookmark.WorkflowInstanceId; + var query = + from triggerBoundWorkflow in triggerBoundWorkflows + from trigger in triggerBoundWorkflow.Triggers + select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); - var newRuntimeOptions = new ResumeWorkflowRuntimeParams - { - CorrelationId = runtimeParams.CorrelationId, - Input = runtimeParams.Input, - Properties = runtimeParams.Properties, - BookmarkId = bookmark.BookmarkId, - ActivityId = runtimeParams.ActivityId, - ActivityNodeId = runtimeParams.ActivityNodeId, - ActivityInstanceId = runtimeParams.ActivityInstanceId, - ActivityHash = runtimeParams.ActivityHash, - CancellationTokens = runtimeParams.CancellationTokens - }; - - var resumeResult = await ResumeWorkflowAsync(workflowInstanceId, newRuntimeOptions); - resumedWorkflows.Add(resumeResult!); - } - - return resumedWorkflows; + return query.ToList(); } - private async Task> FindStartableWorkflowsAsync(WorkflowsFilter workflowsFilter, CancellationToken cancellationToken) + private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) { - var hash = _hasher.Hash(workflowsFilter.ActivityTypeName, workflowsFilter.BookmarkPayload); - var filter = new TriggerFilter + var bookmarkOptions = new FindBookmarkOptions { - Hash = hash + CorrelationId = filter.Options.CorrelationId, + WorkflowInstanceId = filter.Options.WorkflowInstanceId, + ActivityInstanceId = filter.Options.ActivityInstanceId }; - var triggers = await _triggerStore.FindManyAsync(filter, cancellationToken); - var results = new List(); + var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); - foreach (var trigger in triggers) - { - var definitionId = trigger.WorkflowDefinitionId; - - var startOptions = new StartWorkflowRuntimeParams - { - CorrelationId = workflowsFilter.Options?.CorrelationId, - Input = workflowsFilter.Options.Input, - Properties = workflowsFilter.Options.Properties, - VersionOptions = VersionOptions.Published, - TriggerActivityId = trigger.ActivityId, - CancellationTokens = cancellationToken - }; - - var canStartResult = await CanStartWorkflowAsync(definitionId, startOptions); - var workflowGraph = await _workflowDefinitionService.FindWorkflowGraphAsync(trigger.WorkflowDefinitionVersionId, cancellationToken); - - if (workflowGraph == null) - { - _logger.LogWarning("Workflow version ID {DefinitionVersionId} not found", trigger.WorkflowDefinitionVersionId); - continue; - } - - var workflow = workflowGraph.Workflow; - var createWorkflowInstanceRequest = new CreateWorkflowInstanceRequest - { - Workflow = workflow, - CorrelationId = workflowsFilter.Options.CorrelationId, - WorkflowInstanceId = workflowsFilter.Options?.WorkflowInstanceId, - Input = workflowsFilter.Options?.Input, - Properties = workflowsFilter.Options?.Properties - }; - var workflowInstance = _workflowInstanceFactory.CreateWorkflowInstance(createWorkflowInstanceRequest); - - if (canStartResult.CanStart) - results.Add(new StartableWorkflowMatch(workflowInstance.Id, workflowInstance, workflowsFilter.Options?.CorrelationId, trigger.ActivityId, definitionId, trigger.Payload)); - } - - return results; - } - - private async Task> FindResumableWorkflowsAsync(WorkflowsFilter workflowsFilter, CancellationToken cancellationToken) - { - var hash = _hasher.Hash(workflowsFilter.ActivityTypeName, workflowsFilter.BookmarkPayload); - var correlationId = workflowsFilter.Options.CorrelationId; - var workflowInstanceId = workflowsFilter.Options.WorkflowInstanceId; - var activityInstanceId = workflowsFilter.Options.ActivityInstanceId; - var filter = new BookmarkFilter - { - Hash = hash, - CorrelationId = correlationId, - WorkflowInstanceId = workflowInstanceId, - ActivityInstanceId = activityInstanceId - }; - var bookmarks = await _bookmarkStore.FindManyAsync(filter, cancellationToken); - var collectedWorkflows = bookmarks.Select(b => new ResumableWorkflowMatch(b.WorkflowInstanceId, default, correlationId, b.BookmarkId, b.Payload)).ToList(); - return collectedWorkflows; + return ( + from bookmarkBoundWorkflow in bookmarkBoundWorkflows + from bookmark in bookmarkBoundWorkflow.Bookmarks + select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) + .ToList(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index ce1266342..335afd1f4 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -1,6 +1,5 @@ using Elsa.Workflows.Management; using Microsoft.Extensions.DependencyInjection; -using Proto.Cluster; namespace Elsa.Workflows.Runtime.ProtoActor.Services; @@ -11,7 +10,12 @@ public partial class ProtoActorWorkflowRuntime( IServiceProvider serviceProvider, IWorkflowDefinitionService workflowDefinitionService, IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, - Cluster cluster, + IStimulusSender stimulusSender, + IStimulusHasher stimulusHasher, + ITriggerBoundWorkflowService triggerBoundWorkflowService, + IBookmarkBoundWorkflowService bookmarkBoundWorkflowService, + IBookmarkStore bookmarkStore, + IWorkflowInstanceStore workflowInstanceStore, IIdentityGenerator identityGenerator) : IWorkflowRuntime { /// From 60323d05227caedea19c7be07f46329b0c4268e3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 00:22:20 +0100 Subject: [PATCH 118/166] Refactor `CanStartWorkflowResult` to use compact constructor. Simplified the object creation in `CanStartWorkflowResult` by using a concise constructor format. This improves code readability and reduces verbosity without altering functionality. --- .../Services/LocalWorkflowRuntime.Obsolete.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs index fcd4bb7a4..1076ed713 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -30,11 +30,7 @@ public partial class LocalWorkflowRuntime CancellationToken = cancellationToken }); - return new CanStartWorkflowResult( - { - CanStart = canStart, - InstanceId = null - }; + return new(null, canStart); } public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) From e27101032b651003b8613c14b1b8ad21d791367f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 00:32:43 +0100 Subject: [PATCH 119/166] Incremental work on central obsolete workflow runtime for reuse --- .../Elsa.Workflows.Runtime.Distributed.csproj | 4 - .../Services/DistributedWorkflowClient.cs | 5 + .../DistributedWorkflowRuntime.Obsolete.cs | 237 +++++++++++++++++ .../Services/DistributedWorkflowRuntime.cs | 13 +- .../Services/ObsoleteWorkflowRuntime.cs | 247 ++++++++++++++++++ .../Proto/WorkflowInstance.Messages.proto | 4 + .../Proto/WorkflowInstance.proto | 2 +- .../ProtoActorWorkflowRuntime.Obsolete.cs | 6 +- .../Services/LocalWorkflowRuntime.Obsolete.cs | 2 +- .../Services/LocalWorkflowRuntime.cs | 2 - 10 files changed, 508 insertions(+), 14 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs create mode 100644 src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj index 8f3983f11..870cd0d78 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj @@ -18,8 +18,4 @@ - - - - diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs index 55179d545..a9468d11d 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs @@ -50,6 +50,11 @@ public class DistributedWorkflowClient( await _localWorkflowClient.ImportStateAsync(workflowState, cancellationToken); } + public async Task InstanceExistsAsync(CancellationToken cancellationToken = default) + { + return await _localWorkflowClient.InstanceExistsAsync(cancellationToken); + } + private async Task WithLockAsync(Func> func) { var lockKey = $"workflow-instance:{WorkflowInstanceId}"; diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs new file mode 100644 index 000000000..a28f34048 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs @@ -0,0 +1,237 @@ +using Elsa.Common.Models; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Matches; +using Elsa.Workflows.Runtime.Messages; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Parameters; +using Elsa.Workflows.Runtime.Params; +using Elsa.Workflows.Runtime.Requests; +using Elsa.Workflows.Runtime.Results; +using Elsa.Workflows.State; +using Open.Linq.AsyncExtensions; + +namespace Elsa.Workflows.Runtime.Distributed; + +public partial class DistributedWorkflowRuntime +{ + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); + var workflow = workflowGraph!.Workflow; + + var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() + { + Workflow = workflow, + CorrelationId = options?.CorrelationId, + CancellationToken = cancellationToken + }); + + return new(null, canStart); + } + + public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = await CreateClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new CreateAndRunWorkflowInstanceRequest + { + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId + }; + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + return await StartWorkflowAsync(definitionId, options); + } + + public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); + var exists = await workflowClient.InstanceExistsAsync(cancellationToken); + + if (!exists) + return null; + + var runWorkflowRequest = new RunWorkflowInstanceRequest + { + Input = options?.Input, + Properties = options?.Properties, + ActivityHandle = options?.ActivityHandle, + BookmarkId = options?.BookmarkId + }; + + var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); + + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return new(results); + } + + public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + if (match is StartableWorkflowMatch collectedStartableWorkflow) + { + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = collectedStartableWorkflow.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = collectedStartableWorkflow.ActivityId, + CancellationToken = cancellationToken + }; + + var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); + return startResult with + { + TriggeredActivityId = collectedStartableWorkflow.ActivityId + }; + } + + var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; + var runtimeOptions = new ResumeWorkflowRuntimeParams + { + CorrelationId = collectedResumableWorkflow.CorrelationId, + BookmarkId = collectedResumableWorkflow.BookmarkId, + Input = options?.Input, + Properties = options?.Properties, + CancellationToken = cancellationToken, + }; + + return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; + } + + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + await client.CancelAsync(cancellationToken); + return new(true); + } + + public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); + var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); + var results = startableWorkflows.Concat(resumableWorkflows).ToList(); + return results; + } + + public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowInstanceId, cancellationToken); + return await client.ExportStateAsync(cancellationToken); + } + + public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var client = await CreateClientAsync(workflowState.Id, cancellationToken); + await client.ImportStateAsync(workflowState, cancellationToken); + } + + public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) + { + await bookmarkStore.SaveAsync(bookmark, cancellationToken); + } + + public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) + { + var filter = new WorkflowInstanceFilter + { + DefinitionId = request.DefinitionId, + Version = request.Version, + CorrelationId = request.CorrelationId, + WorkflowStatus = WorkflowStatus.Running + }; + return await workflowInstanceStore.CountAsync(filter, cancellationToken); + } + + private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); + var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); + var correlationId = filter.Options.CorrelationId; + + var query = + from triggerBoundWorkflow in triggerBoundWorkflows + from trigger in triggerBoundWorkflow.Triggers + select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); + + return query.ToList(); + } + + private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) + { + var bookmarkOptions = new FindBookmarkOptions + { + CorrelationId = filter.Options.CorrelationId, + WorkflowInstanceId = filter.Options.WorkflowInstanceId, + ActivityInstanceId = filter.Options.ActivityInstanceId + }; + var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); + + return ( + from bookmarkBoundWorkflow in bookmarkBoundWorkflows + from bookmark in bookmarkBoundWorkflow.Bookmarks + select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs index 8069d0587..8285f2d69 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows.Management; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Distributed; @@ -5,7 +6,17 @@ namespace Elsa.Workflows.Runtime.Distributed; /// /// Represents a distributed workflow runtime that can create instances connected to a workflow instance. /// -public class DistributedWorkflowRuntime(IServiceProvider serviceProvider, IIdentityGenerator identityGenerator) : IWorkflowRuntime +public partial class DistributedWorkflowRuntime( + IServiceProvider serviceProvider, + IIdentityGenerator identityGenerator, + IWorkflowDefinitionService workflowDefinitionService, + IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, + IStimulusSender stimulusSender, + IStimulusHasher stimulusHasher, + IBookmarkStore bookmarkStore, + IWorkflowInstanceStore workflowInstanceStore, + ITriggerBoundWorkflowService triggerBoundWorkflowService, + IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) : IWorkflowRuntime { /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs new file mode 100644 index 000000000..2173a9319 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs @@ -0,0 +1,247 @@ +using Elsa.Common.Models; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.Runtime.Matches; +using Elsa.Workflows.Runtime.Messages; +using Elsa.Workflows.Runtime.Options; +using Elsa.Workflows.Runtime.Parameters; +using Elsa.Workflows.Runtime.Params; +using Elsa.Workflows.Runtime.Requests; +using Elsa.Workflows.Runtime.Results; +using Elsa.Workflows.State; +using Open.Linq.AsyncExtensions; + +namespace Elsa.Workflows.Runtime.Distributed; + +public class ObsoleteWorkflowRuntime( + Func> createClientAsync, + IWorkflowDefinitionService workflowDefinitionService, + IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, + IStimulusSender stimulusSender, + IStimulusHasher stimulusHasher, + IBookmarkStore bookmarkStore, + IWorkflowInstanceStore workflowInstanceStore, + ITriggerBoundWorkflowService triggerBoundWorkflowService, + IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) +{ + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); + var workflow = workflowGraph!.Workflow; + + var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() + { + Workflow = workflow, + CorrelationId = options?.CorrelationId, + CancellationToken = cancellationToken + }); + + return new(null, canStart); + } + + public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var client = await createClientAsync(options?.InstanceId, cancellationToken); + var createRequest = new CreateAndRunWorkflowInstanceRequest + { + Properties = options?.Properties, + CorrelationId = options?.CorrelationId, + Input = options?.Input, + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), + ParentId = options?.ParentWorkflowInstanceId, + TriggerActivityId = options?.TriggerActivityId + }; + var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) + { + return await StartWorkflowAsync(definitionId, options); + } + + public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var workflowClient = await createClientAsync(workflowInstanceId, cancellationToken); + var exists = await workflowClient.InstanceExistsAsync(cancellationToken); + + if (!exists) + return null; + + var runWorkflowRequest = new RunWorkflowInstanceRequest + { + Input = options?.Input, + Properties = options?.Properties, + ActivityHandle = options?.ActivityHandle, + BookmarkId = options?.BookmarkId + }; + + var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); + + return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); + } + + public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return results; + } + + public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + var metadata = new StimulusMetadata + { + CorrelationId = options?.CorrelationId, + WorkflowInstanceId = options?.WorkflowInstanceId, + Properties = options?.Properties, + ActivityInstanceId = options?.ActivityInstanceId, + Input = options?.Input + }; + var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); + var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); + return new(results); + } + + public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) + { + var cancellationToken = options?.CancellationToken ?? CancellationToken.None; + if (match is StartableWorkflowMatch collectedStartableWorkflow) + { + var startOptions = new StartWorkflowRuntimeParams + { + CorrelationId = collectedStartableWorkflow.CorrelationId, + Input = options?.Input, + Properties = options?.Properties, + VersionOptions = VersionOptions.Published, + TriggerActivityId = collectedStartableWorkflow.ActivityId, + CancellationToken = cancellationToken + }; + + var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); + return startResult with + { + TriggeredActivityId = collectedStartableWorkflow.ActivityId + }; + } + + var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; + var runtimeOptions = new ResumeWorkflowRuntimeParams + { + CorrelationId = collectedResumableWorkflow.CorrelationId, + BookmarkId = collectedResumableWorkflow.BookmarkId, + Input = options?.Input, + Properties = options?.Properties, + CancellationToken = cancellationToken, + }; + + return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; + } + + public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await createClientAsync(workflowInstanceId, cancellationToken); + await client.CancelAsync(cancellationToken); + return new(true); + } + + public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); + var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); + var results = startableWorkflows.Concat(resumableWorkflows).ToList(); + return results; + } + + public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) + { + var client = await createClientAsync(workflowInstanceId, cancellationToken); + return await client.ExportStateAsync(cancellationToken); + } + + public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var client = await createClientAsync(workflowState.Id, cancellationToken); + await client.ImportStateAsync(workflowState, cancellationToken); + } + + public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) + { + await bookmarkStore.SaveAsync(bookmark, cancellationToken); + } + + public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) + { + var filter = new WorkflowInstanceFilter + { + DefinitionId = request.DefinitionId, + Version = request.Version, + CorrelationId = request.CorrelationId, + WorkflowStatus = WorkflowStatus.Running + }; + return await workflowInstanceStore.CountAsync(filter, cancellationToken); + } + + private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) + { + var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); + var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); + var correlationId = filter.Options.CorrelationId; + + var query = + from triggerBoundWorkflow in triggerBoundWorkflows + from trigger in triggerBoundWorkflow.Triggers + select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); + + return query.ToList(); + } + + private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) + { + var bookmarkOptions = new FindBookmarkOptions + { + CorrelationId = filter.Options.CorrelationId, + WorkflowInstanceId = filter.Options.WorkflowInstanceId, + ActivityInstanceId = filter.Options.ActivityInstanceId + }; + var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); + + return ( + from bookmarkBoundWorkflow in bookmarkBoundWorkflows + from bookmark in bookmarkBoundWorkflow.Bookmarks + select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto index 5eeaa3699..719572c28 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto @@ -85,3 +85,7 @@ message ExportWorkflowStateResponse { message ImportWorkflowStateRequest { Json SerializedWorkflowState = 1; } + +message InstanceExistsResponse { + bool Exists = 1; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto index 47088c1d2..ded6d1d68 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.proto @@ -15,5 +15,5 @@ service WorkflowInstance { rpc Cancel (Empty) returns (Empty); rpc ExportState(Empty) returns (ExportWorkflowStateResponse); rpc ImportState(ImportWorkflowStateRequest) returns (Empty); - rpc InstanceExists(Empty) returns (bool); + rpc InstanceExists(Empty) returns (InstanceExistsResponse); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs index 054afc08b..f6ec2297a 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -30,11 +30,7 @@ public partial class ProtoActorWorkflowRuntime CancellationToken = cancellationToken }); - return new CanStartWorkflowResult( - { - CanStart = canStart, - InstanceId = null - }; + return new(null, canStart); } /// diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs index 1076ed713..66c5bfb16 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -36,7 +36,7 @@ public partial class LocalWorkflowRuntime public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) { var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); + var client = await CreateClientAsync(options?.InstanceId, cancellationToken); var createRequest = new CreateAndRunWorkflowInstanceRequest { Properties = options?.Properties, diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs index cfd5309cf..fba019f0f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs @@ -14,9 +14,7 @@ public partial class LocalWorkflowRuntime( IWorkflowDefinitionService workflowDefinitionService, IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, IStimulusSender stimulusSender, - IBookmarkResumer bookmarkResumer, IStimulusHasher stimulusHasher, - IWorkflowCanceler workflowCanceler, IBookmarkStore bookmarkStore, IWorkflowInstanceStore workflowInstanceStore, ITriggerBoundWorkflowService triggerBoundWorkflowService, From ee883dbd0720d74fd08237e68ddbea39d4c76351 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 09:18:24 +0100 Subject: [PATCH 120/166] Refactor workflow runtime to use ObsoleteWorkflowRuntime delegation Replaced direct method implementations in workflow runtimes with delegations to the new `ObsoleteWorkflowRuntime` class, simplifying the codebase. This consolidates logic and aligns the runtimes under a unified deprecated API layer. --- .../DistributedWorkflowRuntime.Obsolete.cs | 237 ++-------------- .../Services/DistributedWorkflowRuntime.cs | 30 +- .../Actors/WorkflowInstance.cs | 17 +- .../ProtoActorWorkflowRuntime.Obsolete.cs | 265 ++---------------- .../Services/ProtoActorWorkflowRuntime.cs | 32 ++- .../Deprecated}/ObsoleteWorkflowRuntime.cs | 4 +- .../Services/LocalWorkflowRuntime.Obsolete.cs | 237 ++-------------- .../Services/LocalWorkflowRuntime.cs | 33 ++- 8 files changed, 118 insertions(+), 737 deletions(-) rename src/modules/{Elsa.Workflows.Runtime.Distributed/Services => Elsa.Workflows.Runtime/Deprecated}/ObsoleteWorkflowRuntime.cs (98%) diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs index a28f34048..45c9fb00c 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs @@ -1,237 +1,32 @@ -using Elsa.Common.Models; -using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Deprecated; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; -using Elsa.Workflows.Runtime.Messages; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Parameters; using Elsa.Workflows.Runtime.Params; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; -using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime.Distributed; public partial class DistributedWorkflowRuntime { - public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); - var workflow = workflowGraph!.Workflow; + private readonly ObsoleteWorkflowRuntime _obsoleteApi; - var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() - { - Workflow = workflow, - CorrelationId = options?.CorrelationId, - CancellationToken = cancellationToken - }); - - return new(null, canStart); - } - - public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new CreateAndRunWorkflowInstanceRequest - { - Properties = options?.Properties, - CorrelationId = options?.CorrelationId, - Input = options?.Input, - WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), - ParentId = options?.ParentWorkflowInstanceId, - TriggerActivityId = options?.TriggerActivityId - }; - var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - return await StartWorkflowAsync(definitionId, options); - } - - public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); - var exists = await workflowClient.InstanceExistsAsync(cancellationToken); - - if (!exists) - return null; - - var runWorkflowRequest = new RunWorkflowInstanceRequest - { - Input = options?.Input, - Properties = options?.Properties, - ActivityHandle = options?.ActivityHandle, - BookmarkId = options?.BookmarkId - }; - - var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); - - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return new(results); - } - - public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - if (match is StartableWorkflowMatch collectedStartableWorkflow) - { - var startOptions = new StartWorkflowRuntimeParams - { - CorrelationId = collectedStartableWorkflow.CorrelationId, - Input = options?.Input, - Properties = options?.Properties, - VersionOptions = VersionOptions.Published, - TriggerActivityId = collectedStartableWorkflow.ActivityId, - CancellationToken = cancellationToken - }; - - var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); - return startResult with - { - TriggeredActivityId = collectedStartableWorkflow.ActivityId - }; - } - - var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; - var runtimeOptions = new ResumeWorkflowRuntimeParams - { - CorrelationId = collectedResumableWorkflow.CorrelationId, - BookmarkId = collectedResumableWorkflow.BookmarkId, - Input = options?.Input, - Properties = options?.Properties, - CancellationToken = cancellationToken, - }; - - return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; - } - - public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - await client.CancelAsync(cancellationToken); - return new(true); - } - - public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); - var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); - var results = startableWorkflows.Concat(resumableWorkflows).ToList(); - return results; - } - - public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - return await client.ExportStateAsync(cancellationToken); - } - - public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowState.Id, cancellationToken); - await client.ImportStateAsync(workflowState, cancellationToken); - } - - public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) - { - await bookmarkStore.SaveAsync(bookmark, cancellationToken); - } - - public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) - { - var filter = new WorkflowInstanceFilter - { - DefinitionId = request.DefinitionId, - Version = request.Version, - CorrelationId = request.CorrelationId, - WorkflowStatus = WorkflowStatus.Running - }; - return await workflowInstanceStore.CountAsync(filter, cancellationToken); - } - - private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); - var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); - var correlationId = filter.Options.CorrelationId; - - var query = - from triggerBoundWorkflow in triggerBoundWorkflows - from trigger in triggerBoundWorkflow.Triggers - select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); - - return query.ToList(); - } - - private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) - { - var bookmarkOptions = new FindBookmarkOptions - { - CorrelationId = filter.Options.CorrelationId, - WorkflowInstanceId = filter.Options.WorkflowInstanceId, - ActivityInstanceId = filter.Options.ActivityInstanceId - }; - var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); - - return ( - from bookmarkBoundWorkflow in bookmarkBoundWorkflows - from bookmark in bookmarkBoundWorkflow.Bookmarks - select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) - .ToList(); - } + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs index 8285f2d69..4b0a6056b 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs @@ -1,4 +1,5 @@ using Elsa.Workflows.Management; +using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Distributed; @@ -6,18 +7,21 @@ namespace Elsa.Workflows.Runtime.Distributed; /// /// Represents a distributed workflow runtime that can create instances connected to a workflow instance. /// -public partial class DistributedWorkflowRuntime( - IServiceProvider serviceProvider, - IIdentityGenerator identityGenerator, - IWorkflowDefinitionService workflowDefinitionService, - IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, - IStimulusSender stimulusSender, - IStimulusHasher stimulusHasher, - IBookmarkStore bookmarkStore, - IWorkflowInstanceStore workflowInstanceStore, - ITriggerBoundWorkflowService triggerBoundWorkflowService, - IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) : IWorkflowRuntime +public partial class DistributedWorkflowRuntime : IWorkflowRuntime { + private readonly IServiceProvider _serviceProvider; + private readonly IIdentityGenerator _identityGenerator; + + /// + /// Represents a distributed workflow runtime that can create instances connected to a workflow instance. + /// + public DistributedWorkflowRuntime(IServiceProvider serviceProvider, IIdentityGenerator identityGenerator) + { + _serviceProvider = serviceProvider; + _identityGenerator = identityGenerator; + _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + } + /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) { @@ -27,8 +31,8 @@ public partial class DistributedWorkflowRuntime( /// public ValueTask CreateClientAsync(string? workflowInstanceId, CancellationToken cancellationToken = default) { - workflowInstanceId ??= identityGenerator.GenerateId(); - var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(serviceProvider, typeof(DistributedWorkflowClient), workflowInstanceId); + workflowInstanceId ??= _identityGenerator.GenerateId(); + var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(DistributedWorkflowClient), workflowInstanceId); return new(client); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs index 370b77e09..748197690 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Actors/WorkflowInstance.cs @@ -41,7 +41,7 @@ internal class WorkflowInstance( public override Task OnStarted() { - _linkedTokenSource = new CancellationTokenSource(); + _linkedTokenSource = new(); _linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(Context.CancellationToken, _linkedTokenSource.Token).Token; return Task.CompletedTask; } @@ -61,7 +61,7 @@ internal class WorkflowInstance( if (result.IsFaulted) onError(result.Exception.Message); else - respond(new CreateWorkflowInstanceResponse()); + respond(new()); }); return Task.CompletedTask; @@ -170,7 +170,7 @@ internal class WorkflowInstance( { await EnsureStateAsync(); var json = mappers.WorkflowStateJsonMapper.Map(WorkflowState); - return new ExportWorkflowStateResponse + return new() { SerializedWorkflowState = json }; @@ -186,12 +186,21 @@ internal class WorkflowInstance( await workflowInstanceManager.SaveAsync(WorkflowState, Context.CancellationToken); } + public override Task InstanceExists() + { + var exists = _workflowInstanceId != null; + return Task.FromResult(new InstanceExistsResponse + { + Exists = exists + }); + } + private async Task RunAsync(RunWorkflowOptions runWorkflowOptions) { if (_isRunning) { _queuedRunWorkflowOptions.Enqueue(runWorkflowOptions); - return new RunWorkflowResult(null!, null!, null); + return new(null!, null!, null); } _isRunning = true; diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs index f6ec2297a..6393c5149 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -1,6 +1,5 @@ -using System.Diagnostics.CodeAnalysis; -using Elsa.Common.Models; -using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Runtime.Deprecated; +using Elsa.Workflows.Runtime.Distributed; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; @@ -10,255 +9,25 @@ using Elsa.Workflows.Runtime.Params; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; -using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime.ProtoActor.Services; public partial class ProtoActorWorkflowRuntime { - /// - public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); - var workflow = workflowGraph!.Workflow; + private readonly ObsoleteWorkflowRuntime _obsoleteApi; - var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() - { - Workflow = workflow, - CorrelationId = options?.CorrelationId, - CancellationToken = cancellationToken - }); - - return new(null, canStart); - } - - /// - public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new Messages.CreateAndRunWorkflowInstanceRequest - { - Properties = options?.Properties, - CorrelationId = options?.CorrelationId, - Input = options?.Input, - WorkflowDefinitionHandle = Workflows.Models.WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), - ParentId = options?.ParentWorkflowInstanceId, - TriggerActivityId = options?.TriggerActivityId - }; - var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - /// - public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = (LocalWorkflowClient)await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new Messages.CreateAndRunWorkflowInstanceRequest - { - Properties = options?.Properties, - CorrelationId = options?.CorrelationId, - Input = options?.Input, - WorkflowDefinitionHandle = Workflows.Models.WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), - ParentId = options?.ParentWorkflowInstanceId, - TriggerActivityId = options?.TriggerActivityId - }; - var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - /// - public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - /// - public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); - var exists = await workflowClient.InstanceExistsAsync(cancellationToken); - - if (!exists) - return null; - - var runWorkflowRequest = new Messages.RunWorkflowInstanceRequest - { - Input = options?.Input, - Properties = options?.Properties, - ActivityHandle = options?.ActivityHandle, - BookmarkId = options?.BookmarkId - }; - - var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); - - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - /// - public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - /// - public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return new(results); - } - - /// - public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - if (match is StartableWorkflowMatch collectedStartableWorkflow) - { - var startOptions = new StartWorkflowRuntimeParams - { - CorrelationId = collectedStartableWorkflow.CorrelationId, - Input = options?.Input, - Properties = options?.Properties, - VersionOptions = VersionOptions.Published, - TriggerActivityId = collectedStartableWorkflow.ActivityId, - CancellationToken = cancellationToken - }; - - var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); - return startResult with - { - TriggeredActivityId = collectedStartableWorkflow.ActivityId - }; - } - - var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; - var runtimeOptions = new ResumeWorkflowRuntimeParams - { - CorrelationId = collectedResumableWorkflow.CorrelationId, - BookmarkId = collectedResumableWorkflow.BookmarkId, - Input = options?.Input, - Properties = options?.Properties, - CancellationToken = cancellationToken, - }; - - return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; - } - - /// - public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - await client.CancelAsync(cancellationToken); - return new(true); - } - - /// - public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); - var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); - var results = startableWorkflows.Concat(resumableWorkflows).ToList(); - return results; - } - - /// - [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.DeserializeAsync(String, CancellationToken)")] - public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - return await client.ExportStateAsync(cancellationToken); - } - - /// - [RequiresUnreferencedCode("Calls Elsa.Workflows.Contracts.IWorkflowStateSerializer.SerializeAsync(WorkflowState, CancellationToken)")] - public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowState.Id, cancellationToken); - await client.ImportStateAsync(workflowState, cancellationToken); - } - - /// - public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) - { - await bookmarkStore.SaveAsync(bookmark, cancellationToken); - } - - /// - public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) - { - var filter = new WorkflowInstanceFilter - { - DefinitionId = request.DefinitionId, - Version = request.Version, - CorrelationId = request.CorrelationId, - WorkflowStatus = WorkflowStatus.Running - }; - return await workflowInstanceStore.CountAsync(filter, cancellationToken); - } - - private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); - var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); - var correlationId = filter.Options.CorrelationId; - - var query = - from triggerBoundWorkflow in triggerBoundWorkflows - from trigger in triggerBoundWorkflow.Triggers - select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); - - return query.ToList(); - } - - private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) - { - var bookmarkOptions = new FindBookmarkOptions - { - CorrelationId = filter.Options.CorrelationId, - WorkflowInstanceId = filter.Options.WorkflowInstanceId, - ActivityInstanceId = filter.Options.ActivityInstanceId - }; - var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); - - return ( - from bookmarkBoundWorkflow in bookmarkBoundWorkflows - from bookmark in bookmarkBoundWorkflow.Bookmarks - select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) - .ToList(); - } + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index 335afd1f4..98122f13b 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -1,4 +1,4 @@ -using Elsa.Workflows.Management; +using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.ProtoActor.Services; @@ -6,18 +6,22 @@ namespace Elsa.Workflows.Runtime.ProtoActor.Services; /// /// Represents a Proto.Actor implementation of the workflows runtime. /// -public partial class ProtoActorWorkflowRuntime( - IServiceProvider serviceProvider, - IWorkflowDefinitionService workflowDefinitionService, - IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, - IStimulusSender stimulusSender, - IStimulusHasher stimulusHasher, - ITriggerBoundWorkflowService triggerBoundWorkflowService, - IBookmarkBoundWorkflowService bookmarkBoundWorkflowService, - IBookmarkStore bookmarkStore, - IWorkflowInstanceStore workflowInstanceStore, - IIdentityGenerator identityGenerator) : IWorkflowRuntime +public partial class ProtoActorWorkflowRuntime : IWorkflowRuntime { + private readonly IServiceProvider _serviceProvider; + private readonly IIdentityGenerator _identityGenerator; + + /// + /// Represents a Proto.Actor implementation of the workflows runtime. + /// + public ProtoActorWorkflowRuntime(IServiceProvider serviceProvider, + IIdentityGenerator identityGenerator) + { + _serviceProvider = serviceProvider; + _identityGenerator = identityGenerator; + _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + } + /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) { @@ -27,8 +31,8 @@ public partial class ProtoActorWorkflowRuntime( /// public ValueTask CreateClientAsync(string? workflowInstanceId, CancellationToken cancellationToken = default) { - workflowInstanceId ??= identityGenerator.GenerateId(); - var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(serviceProvider, typeof(ProtoActorWorkflowClient), workflowInstanceId); + workflowInstanceId ??= _identityGenerator.GenerateId(); + var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(ProtoActorWorkflowClient), workflowInstanceId); return new(client); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs similarity index 98% rename from src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs rename to src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs index 2173a9319..28087e8bc 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/ObsoleteWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs @@ -14,10 +14,10 @@ using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; using Open.Linq.AsyncExtensions; -namespace Elsa.Workflows.Runtime.Distributed; +namespace Elsa.Workflows.Runtime.Deprecated; public class ObsoleteWorkflowRuntime( - Func> createClientAsync, + Func> createClientAsync, IWorkflowDefinitionService workflowDefinitionService, IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, IStimulusSender stimulusSender, diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs index 66c5bfb16..df5be09f7 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -1,237 +1,32 @@ -using Elsa.Common.Models; -using Elsa.Workflows.Management.Filters; -using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Deprecated; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; -using Elsa.Workflows.Runtime.Messages; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Parameters; using Elsa.Workflows.Runtime.Params; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; -using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime; public partial class LocalWorkflowRuntime { - public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, options?.VersionOptions ?? VersionOptions.Published, cancellationToken); - var workflow = workflowGraph!.Workflow; + private readonly ObsoleteWorkflowRuntime _obsoleteApi; - var canStart = await workflowActivationStrategyEvaluator.CanStartWorkflowAsync(new() - { - Workflow = workflow, - CorrelationId = options?.CorrelationId, - CancellationToken = cancellationToken - }); - - return new(null, canStart); - } - - public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var client = await CreateClientAsync(options?.InstanceId, cancellationToken); - var createRequest = new CreateAndRunWorkflowInstanceRequest - { - Properties = options?.Properties, - CorrelationId = options?.CorrelationId, - Input = options?.Input, - WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, options?.VersionOptions ?? VersionOptions.Published), - ParentId = options?.ParentWorkflowInstanceId, - TriggerActivityId = options?.TriggerActivityId - }; - var response = await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - public async Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - public async Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) - { - return await StartWorkflowAsync(definitionId, options); - } - - public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var workflowClient = await CreateClientAsync(workflowInstanceId, cancellationToken); - var exists = await workflowClient.InstanceExistsAsync(cancellationToken); - - if (!exists) - return null; - - var runWorkflowRequest = new RunWorkflowInstanceRequest - { - Input = options?.Input, - Properties = options?.Properties, - ActivityHandle = options?.ActivityHandle, - BookmarkId = options?.BookmarkId - }; - - var response = await workflowClient.RunInstanceAsync(runWorkflowRequest, cancellationToken); - - return new(response.WorkflowInstanceId, response.Status, response.SubStatus, response.Bookmarks, response.Incidents); - } - - public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return results; - } - - public async Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - var metadata = new StimulusMetadata - { - CorrelationId = options?.CorrelationId, - WorkflowInstanceId = options?.WorkflowInstanceId, - Properties = options?.Properties, - ActivityInstanceId = options?.ActivityInstanceId, - Input = options?.Input - }; - var result = await stimulusSender.SendAsync(activityTypeName, bookmarkPayload, metadata, cancellationToken); - var results = result.WorkflowInstanceResponses.Select(x => new WorkflowExecutionResult(x.WorkflowInstanceId, x.Status, x.SubStatus, x.Bookmarks, x.Incidents)).ToList(); - return new(results); - } - - public async Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) - { - var cancellationToken = options?.CancellationToken ?? CancellationToken.None; - if (match is StartableWorkflowMatch collectedStartableWorkflow) - { - var startOptions = new StartWorkflowRuntimeParams - { - CorrelationId = collectedStartableWorkflow.CorrelationId, - Input = options?.Input, - Properties = options?.Properties, - VersionOptions = VersionOptions.Published, - TriggerActivityId = collectedStartableWorkflow.ActivityId, - CancellationToken = cancellationToken - }; - - var startResult = await StartWorkflowAsync(collectedStartableWorkflow.DefinitionId!, startOptions); - return startResult with - { - TriggeredActivityId = collectedStartableWorkflow.ActivityId - }; - } - - var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!; - var runtimeOptions = new ResumeWorkflowRuntimeParams - { - CorrelationId = collectedResumableWorkflow.CorrelationId, - BookmarkId = collectedResumableWorkflow.BookmarkId, - Input = options?.Input, - Properties = options?.Properties, - CancellationToken = cancellationToken, - }; - - return (await ResumeWorkflowAsync(collectedResumableWorkflow.WorkflowInstanceId, runtimeOptions))!; - } - - public async Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - await client.CancelAsync(cancellationToken); - return new(true); - } - - public async Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var startableWorkflows = await FindStartableWorkflowsAsync(filter, cancellationToken); - var resumableWorkflows = await FindResumableWorkflowsAsync(filter, cancellationToken); - var results = startableWorkflows.Concat(resumableWorkflows).ToList(); - return results; - } - - public async Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowInstanceId, cancellationToken); - return await client.ExportStateAsync(cancellationToken); - } - - public async Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) - { - var client = await CreateClientAsync(workflowState.Id, cancellationToken); - await client.ImportStateAsync(workflowState, cancellationToken); - } - - public async Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) - { - await bookmarkStore.SaveAsync(bookmark, cancellationToken); - } - - public async Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) - { - var filter = new WorkflowInstanceFilter - { - DefinitionId = request.DefinitionId, - Version = request.Version, - CorrelationId = request.CorrelationId, - WorkflowStatus = WorkflowStatus.Running - }; - return await workflowInstanceStore.CountAsync(filter, cancellationToken); - } - - private async Task> FindStartableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) - { - var stimulusHash = stimulusHasher.Hash(filter.ActivityTypeName, filter.BookmarkPayload, filter.Options.ActivityInstanceId); - var triggerBoundWorkflows = await triggerBoundWorkflowService.FindManyAsync(stimulusHash, cancellationToken).ToList(); - var correlationId = filter.Options.CorrelationId; - - var query = - from triggerBoundWorkflow in triggerBoundWorkflows - from trigger in triggerBoundWorkflow.Triggers - select new StartableWorkflowMatch(correlationId, trigger.ActivityId, triggerBoundWorkflow.WorkflowGraph.Workflow.Identity.DefinitionId, filter.BookmarkPayload); - - return query.ToList(); - } - - private async Task> FindResumableWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken) - { - var bookmarkOptions = new FindBookmarkOptions - { - CorrelationId = filter.Options.CorrelationId, - WorkflowInstanceId = filter.Options.WorkflowInstanceId, - ActivityInstanceId = filter.Options.ActivityInstanceId - }; - var bookmarkBoundWorkflows = await bookmarkBoundWorkflowService.FindManyAsync(filter.ActivityTypeName, filter.BookmarkPayload, bookmarkOptions, cancellationToken).ToList(); - - return ( - from bookmarkBoundWorkflow in bookmarkBoundWorkflows - from bookmark in bookmarkBoundWorkflow.Bookmarks - select new ResumableWorkflowMatch(bookmarkBoundWorkflow.WorkflowInstanceId, bookmark.CorrelationId, bookmark.Id, bookmark.Payload)) - .ToList(); - } + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs index fba019f0f..3f1414d58 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs @@ -1,4 +1,4 @@ -using Elsa.Workflows.Management; +using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime; @@ -8,18 +8,23 @@ namespace Elsa.Workflows.Runtime; /// It does not support clustering and is intended for single-node deployments only. /// For distributed deployments, use Proto.Actor or another distributed runtime. /// -public partial class LocalWorkflowRuntime( - IServiceProvider serviceProvider, - IIdentityGenerator identityGenerator, - IWorkflowDefinitionService workflowDefinitionService, - IWorkflowActivationStrategyEvaluator workflowActivationStrategyEvaluator, - IStimulusSender stimulusSender, - IStimulusHasher stimulusHasher, - IBookmarkStore bookmarkStore, - IWorkflowInstanceStore workflowInstanceStore, - ITriggerBoundWorkflowService triggerBoundWorkflowService, - IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) : IWorkflowRuntime +public partial class LocalWorkflowRuntime : IWorkflowRuntime { + private readonly IServiceProvider _serviceProvider; + private readonly IIdentityGenerator _identityGenerator; + + /// + /// Represents a local implementation of the distributed runtime for running workflows. + /// It does not support clustering and is intended for single-node deployments only. + /// For distributed deployments, use Proto.Actor or another distributed runtime. + /// + public LocalWorkflowRuntime(IServiceProvider serviceProvider, IIdentityGenerator identityGenerator) + { + _serviceProvider = serviceProvider; + _identityGenerator = identityGenerator; + _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + } + /// public async ValueTask CreateClientAsync(CancellationToken cancellationToken = default) { @@ -29,8 +34,8 @@ public partial class LocalWorkflowRuntime( /// public ValueTask CreateClientAsync(string? workflowInstanceId, CancellationToken cancellationToken = default) { - workflowInstanceId ??= identityGenerator.GenerateId(); - var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(serviceProvider, typeof(LocalWorkflowClient), workflowInstanceId); + workflowInstanceId ??= _identityGenerator.GenerateId(); + var client = (IWorkflowClient)ActivatorUtilities.CreateInstance(_serviceProvider, typeof(LocalWorkflowClient), workflowInstanceId); return new(client); } } \ No newline at end of file From 9f01e148a96c2d7d594a4808fa78fb57a05bc026 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 09:26:46 +0100 Subject: [PATCH 121/166] Deprecate and clean up legacy workflow runtime components. Marked various types as obsolete, advising migration to `CreateClientAsync` methods or `IBookmarkQueue` services. Removed unused `Deprecated` namespace imports, aligning code with updated runtime standards. --- .../Services/DistributedWorkflowRuntime.Obsolete.cs | 1 - .../Services/DistributedWorkflowRuntime.cs | 1 - .../Services/ProtoActorWorkflowRuntime.Obsolete.cs | 1 - .../Services/ProtoActorWorkflowRuntime.cs | 1 - .../Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs | 1 + .../Options/TriggerWorkflowsOptions.cs | 1 + .../Parameters/ResumeWorkflowRuntimeParams.cs | 1 + .../Parameters/StartWorkflowRuntimeParams.cs | 1 + .../Params/WorkflowInboxMessageDeliveryParams.cs | 1 + .../Requests/CountRunningWorkflowsRequest.cs | 1 + .../Elsa.Workflows.Runtime/Results/CancellationResult.cs | 3 ++- .../Services/LocalWorkflowRuntime.Obsolete.cs | 1 - .../Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs | 1 - .../{Deprecated => Services}/ObsoleteWorkflowRuntime.cs | 5 ++++- 14 files changed, 12 insertions(+), 8 deletions(-) rename src/modules/Elsa.Workflows.Runtime/{Deprecated => Services}/ObsoleteWorkflowRuntime.cs (98%) diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs index 45c9fb00c..03860d555 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Deprecated; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs index 4b0a6056b..9ce03bf52 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs @@ -1,5 +1,4 @@ using Elsa.Workflows.Management; -using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Distributed; diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs index 6393c5149..9da1fe8a3 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Deprecated; using Elsa.Workflows.Runtime.Distributed; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index 98122f13b..b66d97ee8 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.ProtoActor.Services; diff --git a/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs b/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs index 438446c5a..a6492d081 100644 --- a/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Filters/WorkflowsFilter.cs @@ -8,4 +8,5 @@ namespace Elsa.Workflows.Runtime.Filters; /// The activity type name to trigger workflows for. /// The bookmark payload to trigger workflows for. /// The options to use when triggering workflows. +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] public record WorkflowsFilter(string ActivityTypeName, object BookmarkPayload, TriggerWorkflowsOptions Options); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Options/TriggerWorkflowsOptions.cs b/src/modules/Elsa.Workflows.Runtime/Options/TriggerWorkflowsOptions.cs index 5ac0f1837..15a784823 100644 --- a/src/modules/Elsa.Workflows.Runtime/Options/TriggerWorkflowsOptions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Options/TriggerWorkflowsOptions.cs @@ -3,6 +3,7 @@ namespace Elsa.Workflows.Runtime.Options; /// /// Options for triggering workflows. /// +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] public class TriggerWorkflowsOptions { public string? CorrelationId { get; set; } diff --git a/src/modules/Elsa.Workflows.Runtime/Parameters/ResumeWorkflowRuntimeParams.cs b/src/modules/Elsa.Workflows.Runtime/Parameters/ResumeWorkflowRuntimeParams.cs index 6836fcc0b..b1b744071 100644 --- a/src/modules/Elsa.Workflows.Runtime/Parameters/ResumeWorkflowRuntimeParams.cs +++ b/src/modules/Elsa.Workflows.Runtime/Parameters/ResumeWorkflowRuntimeParams.cs @@ -5,6 +5,7 @@ namespace Elsa.Workflows.Runtime.Parameters; /// /// Options for resuming workflows. /// +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] public class ResumeWorkflowRuntimeParams { public string? CorrelationId { get; set; } diff --git a/src/modules/Elsa.Workflows.Runtime/Parameters/StartWorkflowRuntimeParams.cs b/src/modules/Elsa.Workflows.Runtime/Parameters/StartWorkflowRuntimeParams.cs index d8e337c73..79a1dd72d 100644 --- a/src/modules/Elsa.Workflows.Runtime/Parameters/StartWorkflowRuntimeParams.cs +++ b/src/modules/Elsa.Workflows.Runtime/Parameters/StartWorkflowRuntimeParams.cs @@ -5,6 +5,7 @@ namespace Elsa.Workflows.Runtime.Parameters; /// /// Represents parameters for starting a workflow. /// +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] public class StartWorkflowRuntimeParams { public string? CorrelationId { get; set; } diff --git a/src/modules/Elsa.Workflows.Runtime/Params/WorkflowInboxMessageDeliveryParams.cs b/src/modules/Elsa.Workflows.Runtime/Params/WorkflowInboxMessageDeliveryParams.cs index 5f800b381..b275ac64d 100644 --- a/src/modules/Elsa.Workflows.Runtime/Params/WorkflowInboxMessageDeliveryParams.cs +++ b/src/modules/Elsa.Workflows.Runtime/Params/WorkflowInboxMessageDeliveryParams.cs @@ -3,6 +3,7 @@ namespace Elsa.Workflows.Runtime.Params; /// /// Options for delivering a workflow inbox message. /// +[Obsolete("This type is obsolete. Use the new IBookmarkQueue service instead.")] public class WorkflowInboxMessageDeliveryParams { /// diff --git a/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs b/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs index 9fc36c54f..3d4cec8ac 100644 --- a/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Requests/CountRunningWorkflowsRequest.cs @@ -3,6 +3,7 @@ namespace Elsa.Workflows.Runtime.Requests; /// /// Contains arguments to use for counting the number of workflow instances. /// +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] public class CountRunningWorkflowsRequest { /// diff --git a/src/modules/Elsa.Workflows.Runtime/Results/CancellationResult.cs b/src/modules/Elsa.Workflows.Runtime/Results/CancellationResult.cs index 86092adb7..f40bf1a0b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Results/CancellationResult.cs +++ b/src/modules/Elsa.Workflows.Runtime/Results/CancellationResult.cs @@ -5,4 +5,5 @@ namespace Elsa.Workflows.Runtime.Results; /// /// True if the operation was successful; otherwise, false. /// The reason for the failure, if any. -public record CancellationResult(bool Success, CancellationFailureReason? Reason = default); \ No newline at end of file +[Obsolete("This type is obsolete. Use the new CreateClientAsync methods of IWorkflowRuntime instead.")] +public record CancellationResult(bool Success, CancellationFailureReason? Reason = null); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs index df5be09f7..7d9cfae0c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Deprecated; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs index 3f1414d58..b5f028fde 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Deprecated; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime; diff --git a/src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs similarity index 98% rename from src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs rename to src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs index 28087e8bc..9eb5c5af3 100644 --- a/src/modules/Elsa.Workflows.Runtime/Deprecated/ObsoleteWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs @@ -14,8 +14,11 @@ using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; using Open.Linq.AsyncExtensions; -namespace Elsa.Workflows.Runtime.Deprecated; +namespace Elsa.Workflows.Runtime; +/// +/// Implements the now deprecated workflow runtime API methods. +/// public class ObsoleteWorkflowRuntime( Func> createClientAsync, IWorkflowDefinitionService workflowDefinitionService, From 9a5181a3d7d20666509f93130b152d087f0523d7 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 11:17:02 +0100 Subject: [PATCH 122/166] WIP on feature/4835 --- .../Contexts/WorkflowExecutionContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 56788e661..101685f74 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -517,10 +517,10 @@ public partial class WorkflowExecutionContext : IExecutionContext if (Status == WorkflowStatus.Finished) FinishedAt = UpdatedAt; - + if (Status == WorkflowStatus.Finished || SubStatus == WorkflowSubStatus.Suspended) { - foreach (var registration in _cancellationRegistrations) + foreach (var registration in _cancellationRegistrations) registration.Dispose(); } } @@ -607,7 +607,7 @@ public partial class WorkflowExecutionContext : IExecutionContext WorkflowSubStatus.Suspended => WorkflowStatus.Running, _ => throw new ArgumentOutOfRangeException(nameof(subStatus), subStatus, null) }; - + // TODO: Check if we should not use the target subStatus here instead. private bool ValidateStatusTransition() { From dbd89905c811b64ef5f310ef95b4887c0f6f0a48 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 11:40:14 +0100 Subject: [PATCH 123/166] Remove redundant PreviousSubStatus assignment in workflow context The PreviousSubStatus assignment was removed as it was unused and unnecessary. This cleanup simplifies the code and ensures focus only on relevant functionality. --- .../Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 9c7ed3910..d2d561213 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -513,8 +513,7 @@ public partial class WorkflowExecutionContext : IExecutionContext { if (!ValidateStatusTransition()) throw new($"Cannot transition from {SubStatus} to {subStatus}"); - - PreviousSubStatus = SubStatus; + SubStatus = subStatus; UpdatedAt = SystemClock.UtcNow; From 9437ab3a3ddde4af7e9a2cefdf885b4ed76d28fc Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 13:53:22 +0100 Subject: [PATCH 124/166] Refactor initialization of `ObsoleteWorkflowRuntime`. Replaced direct instantiation of `ObsoleteWorkflowRuntime` with `Lazy` across multiple runtime services to avoid circular dependency resolution. --- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 4 +++ .../DistributedWorkflowRuntime.Obsolete.cs | 31 +++++++++--------- .../Services/DistributedWorkflowRuntime.cs | 3 +- .../ProtoActorWorkflowRuntime.Obsolete.cs | 32 +++++++++---------- .../Services/ProtoActorWorkflowRuntime.cs | 2 +- .../Services/LocalWorkflowRuntime.Obsolete.cs | 31 +++++++++--------- .../Services/LocalWorkflowRuntime.cs | 2 +- .../Services/ObsoleteWorkflowRuntime.cs | 6 ++++ .../Services/StimulusSender.cs | 1 - 9 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 30802d6a5..bb2614836 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -65,4 +65,8 @@ + + + + diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs index 03860d555..26b73aaa8 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.Obsolete.cs @@ -12,20 +12,21 @@ namespace Elsa.Workflows.Runtime.Distributed; public partial class DistributedWorkflowRuntime { - private readonly ObsoleteWorkflowRuntime _obsoleteApi; + private readonly Lazy _obsoleteApi; + private ObsoleteWorkflowRuntime ObsoleteApi => _obsoleteApi.Value; - public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); - public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); - public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); - public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); - public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); - public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); - public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); - public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); - public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); - public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); - public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => ObsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => ObsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => ObsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => ObsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => ObsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => ObsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs index 9ce03bf52..6c932fe69 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowRuntime.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Management; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Distributed; @@ -18,7 +17,7 @@ public partial class DistributedWorkflowRuntime : IWorkflowRuntime { _serviceProvider = serviceProvider; _identityGenerator = identityGenerator; - _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + _obsoleteApi = new(() => ObsoleteWorkflowRuntime.Create(serviceProvider, CreateClientAsync)); } /// diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs index 9da1fe8a3..bf5593756 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.Obsolete.cs @@ -1,4 +1,3 @@ -using Elsa.Workflows.Runtime.Distributed; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.Matches; @@ -13,20 +12,21 @@ namespace Elsa.Workflows.Runtime.ProtoActor.Services; public partial class ProtoActorWorkflowRuntime { - private readonly ObsoleteWorkflowRuntime _obsoleteApi; + private readonly Lazy _obsoleteApi; + private ObsoleteWorkflowRuntime ObsoleteApi => _obsoleteApi.Value; - public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); - public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); - public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); - public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); - public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); - public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); - public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); - public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); - public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); - public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); - public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => ObsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => ObsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => ObsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => ObsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => ObsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => ObsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs index b66d97ee8..e97ca0037 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Services/ProtoActorWorkflowRuntime.cs @@ -18,7 +18,7 @@ public partial class ProtoActorWorkflowRuntime : IWorkflowRuntime { _serviceProvider = serviceProvider; _identityGenerator = identityGenerator; - _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + _obsoleteApi = new(() => ObsoleteWorkflowRuntime.Create(serviceProvider, CreateClientAsync)); } /// diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs index 7d9cfae0c..0c929966e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.Obsolete.cs @@ -12,20 +12,21 @@ namespace Elsa.Workflows.Runtime; public partial class LocalWorkflowRuntime { - private readonly ObsoleteWorkflowRuntime _obsoleteApi; + private readonly Lazy _obsoleteApi; + private ObsoleteWorkflowRuntime ObsoleteApi => _obsoleteApi.Value; - public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.CanStartWorkflowAsync(definitionId, options); - public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.StartWorkflowAsync(definitionId, options); - public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => _obsoleteApi.TryStartWorkflowAsync(definitionId, options); - public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => _obsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); - public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => _obsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); - public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => _obsoleteApi.ExecuteWorkflowAsync(match, options); - public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); - public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => _obsoleteApi.FindWorkflowsAsync(filter, cancellationToken); - public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => _obsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); - public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => _obsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); - public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => _obsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); - public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => _obsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); + public Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.CanStartWorkflowAsync(definitionId, options); + public Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.StartWorkflowAsync(definitionId, options); + public Task> StartWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.StartWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) => ObsoleteApi.TryStartWorkflowAsync(definitionId, options); + public Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeParams? options = null) => ObsoleteApi.ResumeWorkflowAsync(workflowInstanceId, options); + public Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.ResumeWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task TriggerWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options = null) => ObsoleteApi.TriggerWorkflowsAsync(activityTypeName, bookmarkPayload, options); + public Task ExecuteWorkflowAsync(WorkflowMatch match, ExecuteWorkflowParams? options = default) => ObsoleteApi.ExecuteWorkflowAsync(match, options); + public Task CancelWorkflowAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.CancelWorkflowAsync(workflowInstanceId, cancellationToken); + public Task> FindWorkflowsAsync(WorkflowsFilter filter, CancellationToken cancellationToken = default) => ObsoleteApi.FindWorkflowsAsync(filter, cancellationToken); + public Task ExportWorkflowStateAsync(string workflowInstanceId, CancellationToken cancellationToken = default) => ObsoleteApi.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken); + public Task ImportWorkflowStateAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) => ObsoleteApi.ImportWorkflowStateAsync(workflowState, cancellationToken); + public Task UpdateBookmarkAsync(StoredBookmark bookmark, CancellationToken cancellationToken = default) => ObsoleteApi.UpdateBookmarkAsync(bookmark, cancellationToken); + public Task CountRunningWorkflowsAsync(CountRunningWorkflowsRequest request, CancellationToken cancellationToken = default) => ObsoleteApi.CountRunningWorkflowsAsync(request, cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs index b5f028fde..d20b48e17 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowRuntime.cs @@ -21,7 +21,7 @@ public partial class LocalWorkflowRuntime : IWorkflowRuntime { _serviceProvider = serviceProvider; _identityGenerator = identityGenerator; - _obsoleteApi = ActivatorUtilities.CreateInstance(serviceProvider, (Func>)CreateClientAsync); + _obsoleteApi = new(() => ObsoleteWorkflowRuntime.Create(serviceProvider, CreateClientAsync)); } /// diff --git a/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs index 9eb5c5af3..aa26779bc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/ObsoleteWorkflowRuntime.cs @@ -12,6 +12,7 @@ using Elsa.Workflows.Runtime.Params; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.Runtime.Results; using Elsa.Workflows.State; +using Microsoft.Extensions.DependencyInjection; using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime; @@ -30,6 +31,11 @@ public class ObsoleteWorkflowRuntime( ITriggerBoundWorkflowService triggerBoundWorkflowService, IBookmarkBoundWorkflowService bookmarkBoundWorkflowService) { + public static ObsoleteWorkflowRuntime Create(IServiceProvider serviceProvider, Func> createClientAsync) + { + return ActivatorUtilities.CreateInstance(serviceProvider, createClientAsync); + } + public async Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = null) { var cancellationToken = options?.CancellationToken ?? CancellationToken.None; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs b/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs index 039bb6b05..58894281c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StimulusSender.cs @@ -14,7 +14,6 @@ public class StimulusSender( IBookmarkBoundWorkflowService bookmarkBoundWorkflowService, IBookmarkQueue bookmarkQueue, IWorkflowRuntime workflowRuntime, - IWorkflowStarter workflowStarter, ITriggerInvoker triggerInvoker, ILogger logger) : IStimulusSender { From 8bc6fa6a574c37491f06b3dab92df52c882486b9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 20:24:53 +0100 Subject: [PATCH 125/166] Refactor activity context management and state handling. Replaced `ActivityExecutionRecordExtractor` with a `ChangeTrackingDictionary` for better state mutation tracking in activity execution contexts. Introduced tainting mechanisms to track dirty states and ensure precise logging and persistence of activity execution logs. Updated several components like `ActivityExecutionLogSink` and `ExpressionExecutionContext` to utilize these changes effectively. --- .../Models/ExpressionExecutionContext.cs | 14 ++-- .../Contexts/ActivityExecutionContext.cs | 83 ++++++++++++++++--- .../Contexts/WorkflowExecutionContext.cs | 14 ++-- .../Contracts/IExecutionContext.cs | 2 +- .../Extensions/TriggerExtensions.cs | 2 +- .../Models/ChangeTrackingDictionary.cs | 27 ++++++ .../Services/ActivityInvoker.cs | 1 + .../Services/WorkflowStateExtractor.cs | 7 +- .../Features/WorkflowRuntimeFeature.cs | 1 - .../ActivityExecutionLogUpdated.cs | 2 +- .../ActivityExecutionRecordExtractor.cs | 17 ---- .../Services/StoreActivityExecutionLogSink.cs | 22 +++-- 12 files changed, 135 insertions(+), 57 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Models/ChangeTrackingDictionary.cs delete mode 100644 src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs diff --git a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs index 1c5ff2806..3ca25e693 100644 --- a/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs +++ b/src/modules/Elsa.Expressions/Models/ExpressionExecutionContext.cs @@ -9,8 +9,9 @@ namespace Elsa.Expressions.Models; public class ExpressionExecutionContext( IServiceProvider serviceProvider, MemoryRegister memory, - ExpressionExecutionContext? parentContext = default, - IDictionary? transientProperties = default, + ExpressionExecutionContext? parentContext = null, + IDictionary? transientProperties = null, + Action? onChange = null, CancellationToken cancellationToken = default) { /// @@ -54,7 +55,7 @@ public class ExpressionExecutionContext( public bool TryGetBlock(MemoryBlockReference blockReference, out MemoryBlock block) { var b = GetBlockInternal(blockReference); - block = b ?? default!; + block = b ?? null!; return b != null; } @@ -79,7 +80,7 @@ public class ExpressionExecutionContext( return true; } - value = default; + value = null; return false; } @@ -96,16 +97,17 @@ public class ExpressionExecutionContext( /// /// Sets the value of the memory block pointed to by the specified memory block reference. /// - public void Set(Func blockReference, object? value, Action? configure = default) => Set(blockReference(), value, configure); + public void Set(Func blockReference, object? value, Action? configure = null) => Set(blockReference(), value, configure); /// /// Sets the value of the memory block pointed to by the specified memory block reference. /// - public void Set(MemoryBlockReference blockReference, object? value, Action? configure = default) + public void Set(MemoryBlockReference blockReference, object? value, Action? configure = null) { var block = GetBlockInternal(blockReference) ?? Memory.Declare(blockReference); block.Value = value; configure?.Invoke(block); + onChange?.Invoke(); } /// diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 099f71c40..a27a12e19 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -20,6 +20,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable { private readonly ISystemClock _systemClock; private readonly List _bookmarks = []; + private ActivityStatus _status; + private Exception? _exception; private long _executionCount; private ActivityExecutionContext? _parentActivityExecutionContext; @@ -30,7 +32,6 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable string id, WorkflowExecutionContext workflowExecutionContext, ActivityExecutionContext? parentActivityExecutionContext, - ExpressionExecutionContext expressionExecutionContext, IActivity activity, ActivityDescriptor activityDescriptor, DateTimeOffset startedAt, @@ -39,9 +40,14 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable CancellationToken cancellationToken) { _systemClock = systemClock; + Properties = new ChangeTrackingDictionary(Taint); + ActivityState = new ChangeTrackingDictionary(Taint); + ActivityInput = new ChangeTrackingDictionary(Taint); WorkflowExecutionContext = workflowExecutionContext; _parentActivityExecutionContext = parentActivityExecutionContext; - ExpressionExecutionContext = expressionExecutionContext; + var expressionExecutionContextProps = ExpressionExecutionContextExtensions.CreateActivityExecutionContextPropertiesFrom(workflowExecutionContext, workflowExecutionContext.Input); + expressionExecutionContextProps[ExpressionExecutionContextExtensions.ActivityKey] = activity; + ExpressionExecutionContext = new(workflowExecutionContext.ServiceProvider, new(), parentActivityExecutionContext?.ExpressionExecutionContext ?? workflowExecutionContext.ExpressionExecutionContext, expressionExecutionContextProps, Taint, CancellationToken);; Activity = activity; ActivityDescriptor = activityDescriptor; StartedAt = startedAt; @@ -134,7 +140,18 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// The current status of the activity. /// - public ActivityStatus Status { get; private set; } + public ActivityStatus Status + { + get => _status; + private set + { + if (value == _status) + return; + + _status = value; + Taint(); + } + } /// /// Sets the current status of the activity. @@ -147,10 +164,18 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// Gets or sets the exception that occurred during the activity execution, if any. /// - public Exception? Exception { get; set; } + public Exception? Exception + { + get => _exception; + set + { + _exception = value; + Taint(); + } + } /// - public IDictionary Properties { get; set; } = new Dictionary(); + public IDictionary Properties { get; private set; } /// /// A transient dictionary of values that can be associated with this activity execution context. @@ -168,7 +193,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// As of tool version 3.0, all activity Ids are already unique, so there's no need to construct a hierarchical ID public string NodeId => ActivityNode.NodeId; - + public ISet Children { get; } = new HashSet(); /// @@ -189,18 +214,23 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// A dictionary of inputs for the current activity. /// - public IDictionary ActivityInput { get; set; } = new Dictionary(); + public IDictionary ActivityInput { get; private set; } /// /// Journal data will be added to the workflow execution log for the "Executed" event. /// // ReSharper disable once CollectionNeverQueried.Global - public IDictionary JournalData { get; } = new Dictionary(); + public IDictionary JournalData { get; } = new Dictionary(); /// /// Stores the evaluated inputs, serialized, for the current activity for historical purposes. /// - public IDictionary ActivityState { get; set; } = new Dictionary(); + public IDictionary ActivityState { get; } + + /// + /// Indicates whether the state of the current activity execution context has been modified. + /// + public bool IsDirty { get; private set; } /// /// Schedules the specified activity to be executed. @@ -357,13 +387,21 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// Adds each bookmark to the list of bookmarks. /// /// The bookmarks to add. - public void AddBookmarks(IEnumerable bookmarks) => _bookmarks.AddRange(bookmarks); + public void AddBookmarks(IEnumerable bookmarks) + { + _bookmarks.AddRange(bookmarks); + Taint(); + } /// /// Adds a bookmark to the list of bookmarks. /// /// The bookmark to add. - public void AddBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark); + public void AddBookmark(Bookmark bookmark) + { + _bookmarks.Add(bookmark); + Taint(); + } /// /// Creates a bookmark so that this activity can be resumed at a later time. @@ -467,7 +505,11 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// Clear all bookmarks. /// - public void ClearBookmarks() => _bookmarks.Clear(); + public void ClearBookmarks() + { + _bookmarks.Clear(); + Taint(); + } /// /// Returns a property value associated with the current activity context. @@ -674,7 +716,22 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable WorkflowExecutionContext.RemoveCompletionCallbacks(entriesToRemove); } - internal void IncrementExecutionCount() => _executionCount++; + public void Taint() + { + if (!IsDirty) + IsDirty = true; + } + + public void ClearTaint() + { + if (IsDirty) + IsDirty = false; + } + + internal void IncrementExecutionCount() + { + _executionCount++; + } private MemoryBlock? GetMemoryBlock(MemoryBlockReference locationBlockReference) { diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index d2d561213..5a8548378 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -533,20 +533,15 @@ public partial class WorkflowExecutionContext : IExecutionContext var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); var tag = options?.Tag; var parentContext = options?.Owner; - var parentExpressionExecutionContext = parentContext?.ExpressionExecutionContext ?? ExpressionExecutionContext; - var properties = ExpressionExecutionContextExtensions.CreateActivityExecutionContextPropertiesFrom(this, Input); - properties[ExpressionExecutionContextExtensions.ActivityKey] = activity; - var memory = new MemoryRegister(); var now = SystemClock.UtcNow; - var expressionExecutionContext = new ExpressionExecutionContext(ServiceProvider, memory, parentExpressionExecutionContext, properties, CancellationToken); var id = IdentityGenerator.GenerateId(); - var activityExecutionContext = new ActivityExecutionContext(id, this, parentContext, expressionExecutionContext, activity, activityDescriptor, now, tag, SystemClock, CancellationToken); + var activityExecutionContext = new ActivityExecutionContext(id, this, parentContext, activity, activityDescriptor, now, tag, SystemClock, CancellationToken); var variablesToDeclare = options?.Variables ?? Array.Empty(); var variableContainer = new[] { activityExecutionContext.ActivityNode }.Concat(activityExecutionContext.ActivityNode.Ancestors()).FirstOrDefault(x => x.Activity is IVariableContainer)?.Activity as IVariableContainer; - expressionExecutionContext.TransientProperties[ExpressionExecutionContextExtensions.ActivityExecutionContextKey] = activityExecutionContext; + activityExecutionContext.ExpressionExecutionContext.TransientProperties[ExpressionExecutionContextExtensions.ActivityExecutionContextKey] = activityExecutionContext; if (variableContainer != null) { @@ -557,11 +552,12 @@ public partial class WorkflowExecutionContext : IExecutionContext activityExecutionContext.DynamicVariables.Add(variable); // Assign the variable to the expression execution context. - expressionExecutionContext.CreateVariable(variable.Name, variable.Value); + activityExecutionContext.ExpressionExecutionContext.CreateVariable(variable.Name, variable.Value); } } - activityExecutionContext.ActivityInput = options?.Input ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + var activityInput = options?.Input ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + activityExecutionContext.ActivityInput.Merge(activityInput); return activityExecutionContext; } diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contracts/IExecutionContext.cs index 09ddfc5d9..41f6d9cfd 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IExecutionContext.cs @@ -31,5 +31,5 @@ public interface IExecutionContext /// /// A dictionary of values that can be associated with this activity execution context. /// - public IDictionary Properties { get; set; } + public IDictionary Properties { get; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/TriggerExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/TriggerExtensions.cs index a6de0a24f..87a051e0f 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/TriggerExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/TriggerExtensions.cs @@ -50,7 +50,7 @@ public static class TriggerExtensions var expressionInput = new Dictionary(); var applicationProperties = ExpressionExecutionContextExtensions.CreateTriggerIndexingPropertiesFrom(context.Workflow, expressionInput); applicationProperties[ExpressionExecutionContextExtensions.ActivityKey] = trigger; - var expressionExecutionContext = new ExpressionExecutionContext(serviceProvider, register, default, applicationProperties, cancellationToken); + var expressionExecutionContext = new ExpressionExecutionContext(serviceProvider, register, null, applicationProperties, null, cancellationToken); // Evaluate activity inputs before requesting trigger data. foreach (var namedInput in assignedInputs) diff --git a/src/modules/Elsa.Workflows.Core/Models/ChangeTrackingDictionary.cs b/src/modules/Elsa.Workflows.Core/Models/ChangeTrackingDictionary.cs new file mode 100644 index 000000000..e4a18a189 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/ChangeTrackingDictionary.cs @@ -0,0 +1,27 @@ +namespace Elsa.Workflows; + +public class ChangeTrackingDictionary(Action onChange) : Dictionary where TKey : notnull +{ + public new void Add(TKey key, TValue value) + { + base.Add(key, value); + onChange(); + } + + public new bool Remove(TKey key) + { + var result = base.Remove(key); + if (result) onChange(); + return result; + } + + public new TValue this[TKey key] + { + get => base[key]; + set + { + base[key] = value; + onChange(); + } + } +} diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs index 13f074bfa..a7d86aa5d 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs @@ -26,6 +26,7 @@ public class ActivityInvoker( { // Create a new activity execution context. activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity, options); + activityExecutionContext.Taint(); // Add the activity context to the workflow context. workflowExecutionContext.AddActivityExecutionContext(activityExecutionContext); diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index 2767dc099..f7df5fbf4 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -139,8 +139,11 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor var properties = activityExecutionContextState.Properties; var activityExecutionContext = await workflowExecutionContext.CreateActivityExecutionContextAsync(activity); activityExecutionContext.Id = activityExecutionContextState.Id; - activityExecutionContext.Properties = properties; - activityExecutionContext.ActivityState = activityExecutionContextState.ActivityState ?? new Dictionary(); + activityExecutionContext.Properties.Merge(properties); + + if(activityExecutionContextState.ActivityState != null) + activityExecutionContext.ActivityState.Merge(activityExecutionContextState.ActivityState); + activityExecutionContext.TransitionTo(activityExecutionContextState.Status); activityExecutionContext.StartedAt = activityExecutionContextState.StartedAt; activityExecutionContext.CompletedAt = activityExecutionContextState.CompletedAt; diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index cca60aaeb..5ee379d74 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -263,7 +263,6 @@ public class WorkflowRuntimeFeature : FeatureBase .AddScoped() .AddScoped() .AddScoped() - .AddScoped, ActivityExecutionRecordExtractor>() .AddScoped, WorkflowExecutionLogRecordExtractor>() .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/ActivityExecutionLogUpdated.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/ActivityExecutionLogUpdated.cs index f662d238d..9766cf884 100644 --- a/src/modules/Elsa.Workflows.Runtime/Notifications/ActivityExecutionLogUpdated.cs +++ b/src/modules/Elsa.Workflows.Runtime/Notifications/ActivityExecutionLogUpdated.cs @@ -8,4 +8,4 @@ namespace Elsa.Workflows.Runtime.Notifications; /// /// The workflow execution context. /// The activity execution records. -public record ActivityExecutionLogUpdated(WorkflowExecutionContext WorkflowExecutionContext, List Records) : INotification; \ No newline at end of file +public record ActivityExecutionLogUpdated(WorkflowExecutionContext WorkflowExecutionContext, ICollection Records) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs b/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs deleted file mode 100644 index 293601250..000000000 --- a/src/modules/Elsa.Workflows.Runtime/Services/ActivityExecutionRecordExtractor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Elsa.Workflows.Runtime.Entities; - -namespace Elsa.Workflows.Runtime; - -/// -/// Extracts activity execution log records. -/// -public class ActivityExecutionRecordExtractor(IActivityExecutionMapper activityExecutionMapper) : ILogRecordExtractor -{ - /// - public async Task> ExtractLogRecordsAsync(WorkflowExecutionContext context) - { - var activityExecutionContexts = context.ActivityExecutionContexts; - var tasks = activityExecutionContexts.Select(activityExecutionMapper.MapAsync).ToList(); - return await Task.WhenAll(tasks); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs index 23cab65a3..a1fda06ea 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreActivityExecutionLogSink.cs @@ -1,25 +1,35 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Notifications; -using Open.Linq.AsyncExtensions; namespace Elsa.Workflows.Runtime.Services; /// /// This implementation saves directly through the store. /// -public class StoreActivityExecutionLogSink(IActivityExecutionStore activityExecutionStore, ILogRecordExtractor extractor, INotificationSender notificationSender) +public class StoreActivityExecutionLogSink( + IActivityExecutionStore activityExecutionStore, + IActivityExecutionMapper mapper, + INotificationSender notificationSender) : ILogRecordSink { /// public async Task PersistExecutionLogsAsync(WorkflowExecutionContext context, CancellationToken cancellationToken = default) { - var records = await extractor.ExtractLogRecordsAsync(context).ToList(); - - if(records.Count == 0) + // Select tainted activity execution contexts to avoid saving untainted ones multiple times. + var activityExecutionContexts = context.ActivityExecutionContexts.Where(x => x.IsDirty).ToList(); + + if (activityExecutionContexts.Count == 0) return; - + + var tasks = activityExecutionContexts.Select(mapper.MapAsync).ToList(); + var records = await Task.WhenAll(tasks); await activityExecutionStore.SaveManyAsync(records, cancellationToken); + + // Untaint activity execution contexts. + foreach (var activityExecutionContext in activityExecutionContexts) + activityExecutionContext.ClearTaint(); + await notificationSender.SendAsync(new ActivityExecutionLogUpdated(context, records), cancellationToken); } } \ No newline at end of file From 0eec9693f70d141b41869309f05c9f9f78c8c18f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 28 Jan 2025 20:59:27 +0100 Subject: [PATCH 126/166] Refactor and optimize workflow execution context handling. Replaced inline filtering logic with reusable methods to simplify and unify activity execution context management. Introduced `ClearCompletedActivityExecutionContexts` to remove redundant contexts and migrated filtering logic to `WorkflowExecutionContext`. Improved code clarity and maintainability by removing duplicate methods and streamlining exception throwing. --- .../Contexts/WorkflowExecutionContext.cs | 16 ++++++++++++++++ .../Services/WorkflowStateExtractor.cs | 18 +++++------------- .../Services/DefaultCommitStateHandler.cs | 1 + 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 5a8548378..c413bd08c 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -578,6 +578,22 @@ public partial class WorkflowExecutionContext : IExecutionContext /// The predicate used to filter the activity execution contexts to remove. public void RemoveActivityExecutionContext(Func predicate) => _activityExecutionContexts.RemoveWhere(predicate); + /// + /// Removes all completed activity execution contexts that have a parent activity execution context. + /// + public void ClearCompletedActivityExecutionContexts() + { + RemoveActivityExecutionContext(x => x is { IsCompleted: true, ParentActivityExecutionContext: not null }); + } + + public IEnumerable GetActiveActivityExecutionContexts() + { + // Filter out completed activity execution contexts, except for the root Workflow activity context, which stores workflow-level variables. + // This will currently break scripts accessing activity output directly, but there's a workaround for that via variable capturing. + // We may ultimately restore direct output access, but in a different way. + return ActivityExecutionContexts.Where(x => !x.IsCompleted || x.ParentActivityExecutionContext == null); + } + /// /// Records the output of the specified activity into the current workflow execution context. /// diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index f7df5fbf4..99a11a214 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -192,14 +192,14 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor private static void ExtractCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { - // Assert all referenced owner contexts exist. - var activeContexts = GetActiveActivityExecutionContexts(workflowExecutionContext.ActivityExecutionContexts).ToList(); + // Assert that all referenced owner contexts exist. + var activeContexts = workflowExecutionContext.GetActiveActivityExecutionContexts().ToList(); foreach (var completionCallback in workflowExecutionContext.CompletionCallbacks) { var ownerContext = activeContexts.FirstOrDefault(x => x == completionCallback.Owner); if (ownerContext == null) - throw new Exception("Lost an owner context"); + throw new("Lost an owner context"); } var completionCallbacks = workflowExecutionContext @@ -220,7 +220,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor var parentContext = activityExecutionContext.WorkflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == parentId); if (parentContext == null) - throw new Exception("We lost a context. This could indicate a bug in a parent activity that completed before (some of) its child activities."); + throw new("We lost a context. This could indicate a bug in a parent activity that completed before (some of) its child activities."); } var activityExecutionContextState = new ActivityExecutionContextState @@ -241,7 +241,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor } // Only persist non-completed contexts. - state.ActivityExecutionContexts = GetActiveActivityExecutionContexts(workflowExecutionContext.ActivityExecutionContexts).Reverse().Select(CreateActivityExecutionContextState).ToList(); + state.ActivityExecutionContexts = workflowExecutionContext.GetActiveActivityExecutionContexts().Reverse().Select(CreateActivityExecutionContextState).ToList(); } private void ExtractScheduledActivities(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) @@ -260,12 +260,4 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor state.ScheduledActivities = scheduledActivities.ToList(); } - - private static IEnumerable GetActiveActivityExecutionContexts(IEnumerable activityExecutionContexts) - { - // Filter out completed activity execution contexts, except for the root Workflow activity context, which stores workflow-level variables. - // This will currently break scripts accessing activity output directly, but there's a workaround for that via variable capturing. - // We may ultimately restore direct output access, but in a different way. - return activityExecutionContexts.Where(x => !x.IsCompleted || x.ParentActivityExecutionContext == null).ToList(); - } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs index 61acf737b..aeb0c37fc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs @@ -27,6 +27,7 @@ public class DefaultCommitStateHandler( await variablePersistenceManager.SaveVariablesAsync(workflowExecutionContext); await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); workflowExecutionContext.ExecutionLog.Clear(); + workflowExecutionContext.ClearCompletedActivityExecutionContexts(); await workflowExecutionContext.ExecuteDeferredTasksAsync(); } } \ No newline at end of file From 0560d11432646da012cb090a62ccdfbd00df1297 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 10:50:42 +0100 Subject: [PATCH 127/166] Improve JSON array conversion in ObjectConverter Refactored ObjectConverter to handle JSON array conversions more robustly, including support for arrays of complex types. Added a `Person` class for unit testing and updated tests to validate the new functionality. Included necessary project reference updates to ensure proper functionality. --- .../Elsa.Expressions/Elsa.Expressions.csproj | 1 + .../Helpers/ObjectConverter.cs | 28 ++++++++++++++----- .../ObjectConversion/Person.cs | 9 ++++++ .../ObjectConversion/Tests.cs | 17 +++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs diff --git a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj index d696bff63..66d1a6e5e 100644 --- a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj +++ b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj @@ -15,6 +15,7 @@ + diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index f59e9861c..899a84ddc 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -102,15 +102,29 @@ public static class ObjectConverter return jsonElement.Deserialize(targetType, serializerOptions); } - if (value is JsonNode jsonNode and not JsonArray) // If the value is a JsonNode, we can convert it to the target type. If it's a JsonArray, we let the enumerable conversion logic handle it. + if (value is JsonNode jsonNode) { - return underlyingTargetType switch + if (jsonNode is not JsonArray jsonArray) { - { } t when t == typeof(string) => jsonNode.ToString(), - { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()), - { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), - _ => jsonNode - }; + return underlyingTargetType switch + { + { } t when t == typeof(string) => jsonNode.ToString(), + { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()), + { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), + _ => jsonNode + }; + } + + // Convert to target type if target type is an array or a generic collection. + if (targetType.IsArray || targetType.IsCollectionType()) + { + // The element type of the source array is JsonObject. If the element type of the target array is Object then return the source array as an array of JsonObjects. + // Deserializing normally would return an array of JsonElement instead of JsonObject, but we want to keep JsonObject elements: + var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArguments[0]; + + if (targetElementType != typeof(object)) + return jsonArray.Deserialize(targetType, serializerOptions); + } } if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object)) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs new file mode 100644 index 000000000..b5583db8b --- /dev/null +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.Core.UnitTests.ObjectConversion +{ + public class Person + { + public double? Age { get; set; } + + public string? Name { get; set; } + } +} diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs index 6525f5d0e..c245cb5cc 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs @@ -283,4 +283,21 @@ public class Tests Assert.Equal("Bob", secondElement["name"]?.ToString()); Assert.Equal("25", secondElement["age"]?.ToString()); } + + [Fact] + public void ConvertFrom_JsonArrayToArrayOfComplextType_ReturnsArrayOfComplexType() + { + // Arrange + var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]"; + var options = new ObjectConverterOptions(); + + // Act + var result = jsonArrayString.ConvertTo(options); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Length); + Assert.Equal("Alice", result[0].Name); + Assert.Equal("Bob", result[1].Name); + } } \ No newline at end of file From 4d7b551d79122c21bcc5607fd15ebd5251b57b31 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 11:24:13 +0100 Subject: [PATCH 128/166] Add support for ExpandoObject attachments in email sending This update enables handling ExpandoObject as email attachments by extracting relevant properties like FileName, ContentType, and Content. It ensures proper parsing of the content and supports both byte arrays and streams for attachment data. --- src/modules/Elsa.Email/Activities/SendEmail.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/modules/Elsa.Email/Activities/SendEmail.cs b/src/modules/Elsa.Email/Activities/SendEmail.cs index 4ec9ca02d..db68773ca 100644 --- a/src/modules/Elsa.Email/Activities/SendEmail.cs +++ b/src/modules/Elsa.Email/Activities/SendEmail.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Dynamic; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -183,6 +184,22 @@ public class SendEmail : Activity else if (emailAttachment.Content is Stream stream) await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); + break; + } + case ExpandoObject expandoObject: + { + var dictionary = new Dictionary(expandoObject, StringComparer.OrdinalIgnoreCase); + var fileName = dictionary.GetValue("FileName") ?? $"Attachment-{++index}"; + var contentType = dictionary.GetValue("ContentType") ?? "application/binary"; + var parsedContentType = ContentType.Parse(contentType); + var content = dictionary.GetValue("Content"); + + if (content is byte[] bytes) + bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType); + + else if (content is Stream stream) + await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); + break; } default: From f4a31079b715c64f2118407b3ab9633f52a8dde1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 16:18:59 +0100 Subject: [PATCH 129/166] Refactor activity execution context removal logic. Updated methods to ensure proper cleanup when removing activity execution contexts, including handling parent-child relationships. Improved robustness and clarity by introducing step-by-step removal for multiple contexts. --- .../Contexts/ActivityExecutionContext.cs | 3 ++- .../Contexts/WorkflowExecutionContext.cs | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index a27a12e19..667faa22a 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -47,7 +47,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable _parentActivityExecutionContext = parentActivityExecutionContext; var expressionExecutionContextProps = ExpressionExecutionContextExtensions.CreateActivityExecutionContextPropertiesFrom(workflowExecutionContext, workflowExecutionContext.Input); expressionExecutionContextProps[ExpressionExecutionContextExtensions.ActivityKey] = activity; - ExpressionExecutionContext = new(workflowExecutionContext.ServiceProvider, new(), parentActivityExecutionContext?.ExpressionExecutionContext ?? workflowExecutionContext.ExpressionExecutionContext, expressionExecutionContextProps, Taint, CancellationToken);; + ExpressionExecutionContext = new(workflowExecutionContext.ServiceProvider, new(), parentActivityExecutionContext?.ExpressionExecutionContext ?? workflowExecutionContext.ExpressionExecutionContext, expressionExecutionContextProps, Taint, CancellationToken); + ; Activity = activity; ActivityDescriptor = activityDescriptor; StartedAt = startedAt; diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index c413bd08c..770ac1c75 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -239,7 +239,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// The current sub status of the workflow. public WorkflowSubStatus SubStatus { get; internal set; } - + /// The root associated with the execution context. public MemoryRegister MemoryRegister { get; private set; } = null!; @@ -513,7 +513,7 @@ public partial class WorkflowExecutionContext : IExecutionContext { if (!ValidateStatusTransition()) throw new($"Cannot transition from {SubStatus} to {subStatus}"); - + SubStatus = subStatus; UpdatedAt = SystemClock.UtcNow; @@ -572,20 +572,29 @@ public partial class WorkflowExecutionContext : IExecutionContext public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); /// Removes the specified from the workflow execution context. - public void RemoveActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Remove(context); + public void RemoveActivityExecutionContext(ActivityExecutionContext context) + { + _activityExecutionContexts.Remove(context); + context.ParentActivityExecutionContext?.Children.Remove(context); + } /// Removes the specified from the workflow execution context. /// The predicate used to filter the activity execution contexts to remove. - public void RemoveActivityExecutionContext(Func predicate) => _activityExecutionContexts.RemoveWhere(predicate); + public void RemoveActivityExecutionContexts(Func predicate) + { + var itemsToRemove = _activityExecutionContexts.Where(predicate).ToList(); + foreach (var item in itemsToRemove) + RemoveActivityExecutionContext(item); + } /// /// Removes all completed activity execution contexts that have a parent activity execution context. /// public void ClearCompletedActivityExecutionContexts() { - RemoveActivityExecutionContext(x => x is { IsCompleted: true, ParentActivityExecutionContext: not null }); + RemoveActivityExecutionContexts(x => x is { IsCompleted: true, ParentActivityExecutionContext: not null }); } - + public IEnumerable GetActiveActivityExecutionContexts() { // Filter out completed activity execution contexts, except for the root Workflow activity context, which stores workflow-level variables. @@ -633,4 +642,4 @@ public partial class WorkflowExecutionContext : IExecutionContext { return _commitStateHandler.CommitAsync(this, CancellationToken); } -} +} \ No newline at end of file From d50081746810f2f5dd92f15e3456a0d838daa338 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 19:03:59 +0100 Subject: [PATCH 130/166] Simplify DbContext creation with ActivatorUtilities. Replaced Activator.CreateInstance with ActivatorUtilities.CreateInstance to streamline the DbContext instantiation. This improves dependency injection support and ensures better compatibility with service provider configurations. --- src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + src/apps/Elsa.Server.Web/Program.cs | 14 ++++++++++---- .../Abstractions/DesignTimeDbContextFactoryBase.cs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index bb2614836..badb50298 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -14,6 +14,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 1f1c393d4..199ff6cce 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -167,7 +167,7 @@ services { jobStorage = new MemoryStorage(); } - + elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage)); } @@ -371,7 +371,7 @@ services // Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11 // alternatively, you can set the PYTHONNET_PYDLL environment variable. configuration.GetSection("Scripting:Python").Bind(options); - + options.AddScript(sb => { sb.AppendLine("def greet():"); @@ -428,7 +428,13 @@ services if (useQuartz) { - elsa.UseQuartz(quartz => { quartz.UseSqlite(sqliteConnectionString); }); + elsa.UseQuartz(quartz => + { + if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) + quartz.UseSqlite(sqliteConnectionString); + else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + quartz.UsePostgreSql(postgresConnectionString); + }); } if (useSignalR) @@ -560,7 +566,7 @@ services .UseSecretsScripting() ; } - + elsa.UseRetention(r => { r.SweepInterval = TimeSpan.FromHours(5); diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs index fe1951e6e..df23d655f 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs @@ -27,7 +27,7 @@ public abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb ConfigureBuilder(builder, connectionString); - return (TDbContext)Activator.CreateInstance(typeof(TDbContext), builder.Options, serviceProvider)!; + return (TDbContext)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TDbContext), builder.Options); } /// From ce859efc4af82c594b39986994c88df52035d734 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 11:59:47 +0100 Subject: [PATCH 131/166] Refactor commit state handling with a strategy-based approach Replaced the legacy commit state behavior enums and options with a flexible, strategy-based system for activities and workflows. Introduced new interfaces, models, and strategies to enable fine-grained control of commit state logic. Updated related code to integrate the new commit strategies, ensuring modular and extensible commit handling. --- src/apps/Elsa.Server.Web/Program.cs | 22 ++++++ src/apps/Elsa.Server.Web/SampleWorkflow.cs | 22 ++---- .../CommitStates/CommitStrategiesFeature.cs | 37 +++++++++ .../Contracts/IActivityCommitStrategy.cs | 6 ++ .../Contracts/ICommitStateHandler.cs | 2 +- .../Contracts/ICommitStrategyRegistry.cs | 11 +++ .../Contracts/IWorkflowCommitStrategy.cs | 10 +++ .../Extensions/ModuleExtensions.cs | 15 ++++ .../ActivityCommitStateStrategyContext.cs | 3 + .../Models/ActivityLifetimeEvent.cs | 7 ++ .../Models/ActivityStrategyDescriptor.cs | 3 + .../CommitStates/Models/CommitAction.cs | 8 ++ .../WorkflowCommitStateStrategyContext.cs | 3 + .../Models/WorkflowLifetimeEvent.cs | 9 +++ .../Models/WorkflowStrategyDescriptor.cs | 3 + .../Options/CommitStateOptions.cs | 7 ++ ...orkflowCommitStateStrategyJsonConverter.cs | 60 +++++++++++++++ .../Services/DefaultCommitStrategyRegistry.cs | 38 ++++++++++ .../CommitAlwaysActivityStrategy.cs | 9 +++ .../Activities/CommitNeverActivityStrategy.cs | 9 +++ .../Activities/DefaultActivityStrategy.cs | 9 +++ .../Activities/ExecutedActivityStrategy.cs | 9 +++ .../Activities/ExecutingActivityStrategy.cs | 9 +++ .../ActivityExecutedWorkflowStrategy.cs | 9 +++ .../ActivityExecutingWorkflowStrategy.cs | 9 +++ .../Workflows/DefaultWorkflowStrategy.cs | 9 +++ .../Workflows/PeriodicWorkflowStrategy.cs | 22 ++++++ .../WorkflowExecutedWorkflowStrategy.cs | 9 +++ .../WorkflowExecutingWorkflowStrategy.cs | 9 +++ .../Tasks/PopulateCommitStrategyRegistry.cs | 15 ++++ .../Contexts/WorkflowExecutionContext.cs | 1 + .../Elsa.Workflows.Core.csproj.DotSettings | 1 + .../Enums/ActivityCommitStateBehavior.cs | 29 ------- .../Extensions/ActivityPropertyExtensions.cs | 21 ++--- .../Features/WorkflowsFeature.cs | 2 + .../DefaultActivityInvokerMiddleware.cs | 76 ++++++++++--------- .../DefaultActivitySchedulerMiddleware.cs | 28 +++---- .../Models/WorkflowCommitStateOptions.cs | 19 ----- .../Models/WorkflowOptions.cs | 2 +- .../Serializers/JsonPayloadSerializer.cs | 2 +- .../Services/NoopCommitStateHandler.cs | 1 + .../Services/WorkflowRunner.cs | 1 + .../BackgroundActivityInvokerMiddleware.cs | 6 +- .../Services/DefaultCommitStateHandler.cs | 1 + 44 files changed, 454 insertions(+), 129 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IActivityCommitStrategy.cs rename src/modules/Elsa.Workflows.Core/{ => CommitStates}/Contracts/ICommitStateHandler.cs (89%) create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Extensions/ModuleExtensions.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStateStrategyContext.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityLifetimeEvent.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityStrategyDescriptor.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitAction.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStateStrategyContext.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowLifetimeEvent.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowStrategyDescriptor.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs delete mode 100644 src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs delete mode 100644 src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 199ff6cce..e02e5e568 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -41,6 +41,8 @@ using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.Workflows; using Elsa.Workflows.Api; +using Elsa.Workflows.CommitStates.Strategies.Activities; +using Elsa.Workflows.CommitStates.Strategies.Workflows; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Compression; @@ -211,6 +213,26 @@ services { workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing()); workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing()); + workflows.UseCommitStrategies(strategies => + { + // Workflow strategies. + strategies.RegisterStrategy(new DefaultWorkflowStrategy()); + strategies.RegisterStrategy(new WorkflowExecutingWorkflowStrategy()); + strategies.RegisterStrategy(new WorkflowExecutedWorkflowStrategy()); + strategies.RegisterStrategy(new ActivityExecutingWorkflowStrategy()); + strategies.RegisterStrategy(new ActivityExecutedWorkflowStrategy()); + strategies.RegisterStrategy("Every 10 seconds", new PeriodicWorkflowStrategy + { + Interval = TimeSpan.FromSeconds(10) + }); + + // Activity strategies. + strategies.RegisterStrategy(new DefaultActivityStrategy()); + strategies.RegisterStrategy(new CommitAlwaysActivityStrategy()); + strategies.RegisterStrategy(new CommitNeverActivityStrategy()); + strategies.RegisterStrategy(new ExecutingActivityStrategy()); + strategies.RegisterStrategy(new ExecutedActivityStrategy()); + }); }) .UseWorkflowManagement(management => { diff --git a/src/apps/Elsa.Server.Web/SampleWorkflow.cs b/src/apps/Elsa.Server.Web/SampleWorkflow.cs index e8dbc6da0..e9030396d 100644 --- a/src/apps/Elsa.Server.Web/SampleWorkflow.cs +++ b/src/apps/Elsa.Server.Web/SampleWorkflow.cs @@ -1,6 +1,7 @@ using Elsa.Workflows; using Elsa.Workflows.Activities; using Elsa.Extensions; +using Elsa.Workflows.CommitStates.Strategies.Activities; namespace Elsa.Server.Web; @@ -8,25 +9,16 @@ public class SampleWorkflow : WorkflowBase { protected override void Build(IWorkflowBuilder builder) { - builder.WorkflowOptions.CommitStateOptions = new() - { - // Commit state before workflow starts executing. - Starting = true, - - // Commit state before every activity that is about to execute. - ActivityExecuted = true, - - // Commit state after every activity that executed. - ActivityExecuting = true, - }; + builder.WorkflowOptions.CommitStrategyName = "Every 10 seconds"; builder.Root = new Sequence { Activities = { - new WriteLine("Commit before executing").WithCommitStateBehavior(ActivityCommitStateBehavior.Executing), - new WriteLine("Commit after executing").WithCommitStateBehavior(ActivityCommitStateBehavior.Executed), - new WriteLine("Commit only based on the workflow commit options").WithCommitStateBehavior(ActivityCommitStateBehavior.Default), - new WriteLine("Never commit the workflow when this activity is about to execute or has executed").WithCommitStateBehavior(ActivityCommitStateBehavior.Never), + new WriteLine("Commit before executing").WithCommitStateStrategy(nameof(ExecutingActivityStrategy)), + new WriteLine("Commit after executing").WithCommitStateStrategy(nameof(ExecutedActivityStrategy)), + new WriteLine("Commit before & after executing").WithCommitStateStrategy(nameof(CommitAlwaysActivityStrategy)), + new WriteLine("Commit only based on the workflow commit options").WithCommitStateStrategy(nameof(DefaultActivityStrategy)), + new WriteLine("Never commit the workflow when this activity is about to execute or has executed").WithCommitStateStrategy(nameof(CommitNeverActivityStrategy)), } }; } diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs new file mode 100644 index 000000000..9f9c334a2 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs @@ -0,0 +1,37 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Elsa.Workflows.CommitStates.Options; +using Elsa.Workflows.CommitStates.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.CommitStates; + +public class CommitStrategiesFeature(IModule module) : FeatureBase(module) +{ + public void RegisterStrategy(IWorkflowCommitStrategy strategy) + { + RegisterStrategy(strategy.GetType().Name, strategy); + } + + public void RegisterStrategy(string name, IWorkflowCommitStrategy strategy) + { + Services.Configure(options => options.WorkflowCommitStrategies[name] = strategy); + } + + public void RegisterStrategy(IActivityCommitStrategy strategy) + { + RegisterStrategy(strategy.GetType().Name, strategy); + } + + public void RegisterStrategy(string name, IActivityCommitStrategy strategy) + { + Services.Configure(options => options.ActivityCommitStrategies[name] = strategy); + } + + public override void Apply() + { + Services.AddSingleton(); + Services.AddStartupTask(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IActivityCommitStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IActivityCommitStrategy.cs new file mode 100644 index 000000000..b648a4670 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IActivityCommitStrategy.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows.CommitStates; + +public interface IActivityCommitStrategy +{ + CommitAction ShouldCommit(ActivityCommitStateStrategyContext context); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStateHandler.cs similarity index 89% rename from src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs rename to src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStateHandler.cs index a73659ea5..175f42828 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/ICommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStateHandler.cs @@ -1,6 +1,6 @@ using Elsa.Workflows.State; -namespace Elsa.Workflows; +namespace Elsa.Workflows.CommitStates; public interface ICommitStateHandler { diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs new file mode 100644 index 000000000..430e63abb --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs @@ -0,0 +1,11 @@ +namespace Elsa.Workflows.CommitStates; + +public interface ICommitStrategyRegistry +{ + IEnumerable ListWorkflowStrategies(); + IEnumerable ListActivityStrategies(); + void RegisterStrategy(string name, IWorkflowCommitStrategy strategy); + void RegisterStrategy(string name, IActivityCommitStrategy strategy); + IWorkflowCommitStrategy? FindWorkflowStrategy(string name); + IActivityCommitStrategy? FindActivityStrategy(string name); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs new file mode 100644 index 000000000..639936c9a --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; +using Elsa.Workflows.CommitStates.Serialization; + +namespace Elsa.Workflows.CommitStates; + +[JsonConverter(typeof(WorkflowCommitStateStrategyJsonConverter))] +public interface IWorkflowCommitStrategy +{ + CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..9d2bf94c5 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Extensions/ModuleExtensions.cs @@ -0,0 +1,15 @@ +using Elsa.Workflows.CommitStates; +using Elsa.Workflows.Features; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +public static class WorkflowsFeatureCommitStateExtensions +{ + public static WorkflowsFeature UseCommitStrategies(this WorkflowsFeature workflowsFeature, Action? configure = null) + { + workflowsFeature.Module.Use(configure); + return workflowsFeature; + } + +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStateStrategyContext.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStateStrategyContext.cs new file mode 100644 index 000000000..bde323f1e --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStateStrategyContext.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.CommitStates; + +public record ActivityCommitStateStrategyContext(ActivityExecutionContext ActivityExecutionContext, ActivityLifetimeEvent LifetimeEvent); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityLifetimeEvent.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityLifetimeEvent.cs new file mode 100644 index 000000000..9a2643416 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityLifetimeEvent.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.CommitStates; + +public enum ActivityLifetimeEvent +{ + ActivityExecuting, + ActivityExecuted +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityStrategyDescriptor.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityStrategyDescriptor.cs new file mode 100644 index 000000000..8b1c266cc --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityStrategyDescriptor.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.CommitStates; + +public record ActivityStrategyDescriptor(string Name, string Description, IActivityCommitStrategy Strategy); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitAction.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitAction.cs new file mode 100644 index 000000000..cd2878354 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitAction.cs @@ -0,0 +1,8 @@ +namespace Elsa.Workflows.CommitStates; + +public enum CommitAction +{ + Default, + Commit, + Skip +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStateStrategyContext.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStateStrategyContext.cs new file mode 100644 index 000000000..d649e08f1 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStateStrategyContext.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.CommitStates; + +public record WorkflowCommitStateStrategyContext(WorkflowExecutionContext WorkflowExecutionContext, WorkflowLifetimeEvent LifetimeEvent); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowLifetimeEvent.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowLifetimeEvent.cs new file mode 100644 index 000000000..1819083c1 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowLifetimeEvent.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates; + +public enum WorkflowLifetimeEvent +{ + WorkflowExecuting, + ActivityExecuting, + ActivityExecuted, + WorkflowExecuted +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowStrategyDescriptor.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowStrategyDescriptor.cs new file mode 100644 index 000000000..1aa38e723 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowStrategyDescriptor.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.CommitStates; + +public record WorkflowStrategyDescriptor(string Name, string Description); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs new file mode 100644 index 000000000..3a7993986 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.CommitStates.Options; + +public class CommitStateOptions +{ + public IDictionary WorkflowCommitStrategies { get; set; } = new Dictionary(); + public IDictionary ActivityCommitStrategies { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs new file mode 100644 index 000000000..3ccb7c752 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs @@ -0,0 +1,60 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Workflows.CommitStates.Strategies.Workflows; + +namespace Elsa.Workflows.CommitStates.Serialization +{ + public class WorkflowCommitStateStrategyJsonConverter : JsonConverter + { + public override IWorkflowCommitStrategy? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("Expected StartObject token"); + + // Read the JSON object + using var document = JsonDocument.ParseValue(ref reader); + var rootElement = document.RootElement; + + // Extract the type information + if (!rootElement.TryGetProperty("$type", out var typeProperty)) + return new DefaultWorkflowStrategy(); + + var typeName = typeProperty.GetString(); + if (string.IsNullOrEmpty(typeName)) throw new JsonException("The $type property is empty or null"); + + // Resolve the type from the type name + var type = Type.GetType(typeName); + if (type == null) throw new JsonException($"Could not resolve type: {typeName}"); + + // Ensure the type implements the expected interface + if (!typeof(IWorkflowCommitStrategy).IsAssignableFrom(type)) throw new JsonException($"The type {typeName} does not implement IWorkflowCommitStateStrategy"); + + // Deserialize the "value" object to the resolved type + if (!rootElement.TryGetProperty("value", out var valueProperty)) throw new JsonException("Could not find 'value' property in JSON payload"); + + var value = JsonSerializer.Deserialize(valueProperty.GetRawText(), type, options); + + // Ensure the deserialized object is an IWorkflowCommitStateStrategy + return value as IWorkflowCommitStrategy ?? throw new JsonException($"Deserialized object is not an IWorkflowCommitStateStrategy"); + } + + public override void Write(Utf8JsonWriter writer, IWorkflowCommitStrategy value, JsonSerializerOptions options) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + + // Writing the type name for deserialization purposes. + var type = value.GetType(); + + writer.WriteStartObject(); + + // Serialize the type name to enable proper deserialization + writer.WriteString("$type", type.AssemblyQualifiedName); + + // Serialize the object using the default serializer + writer.WritePropertyName("value"); + JsonSerializer.Serialize(writer, value, type, options); + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs new file mode 100644 index 000000000..e2dd19fb1 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs @@ -0,0 +1,38 @@ +namespace Elsa.Workflows.CommitStates; + +public class DefaultCommitStrategyRegistry : ICommitStrategyRegistry +{ + private readonly IDictionary _workflowStrategies = new Dictionary(); + private readonly IDictionary _activityStrategies = new Dictionary(); + + + public IEnumerable ListWorkflowStrategies() + { + return _workflowStrategies.Keys; + } + + public IEnumerable ListActivityStrategies() + { + return _activityStrategies.Keys; + } + + public void RegisterStrategy(string name, IWorkflowCommitStrategy strategy) + { + _workflowStrategies[name] = strategy; + } + + public void RegisterStrategy(string name, IActivityCommitStrategy strategy) + { + _activityStrategies[name] = strategy; + } + + public IWorkflowCommitStrategy? FindWorkflowStrategy(string name) + { + return _workflowStrategies.TryGetValue(name, out var strategy) ? strategy : null; + } + + public IActivityCommitStrategy? FindActivityStrategy(string name) + { + return _activityStrategies.TryGetValue(name, out var strategy) ? strategy : null; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs new file mode 100644 index 000000000..2217e07ec --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Activities; + +public class CommitAlwaysActivityStrategy : IActivityCommitStrategy +{ + public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) + { + return CommitAction.Commit; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs new file mode 100644 index 000000000..b2f022358 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Activities; + +public class CommitNeverActivityStrategy : IActivityCommitStrategy +{ + public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) + { + return CommitAction.Skip; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs new file mode 100644 index 000000000..fcf307ce8 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Activities; + +public class DefaultActivityStrategy : IActivityCommitStrategy +{ + public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) + { + return CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs new file mode 100644 index 000000000..393e1960c --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Activities; + +public class ExecutedActivityStrategy : IActivityCommitStrategy +{ + public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) + { + return context.LifetimeEvent == ActivityLifetimeEvent.ActivityExecuted ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs new file mode 100644 index 000000000..f8531c5d3 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Activities; + +public class ExecutingActivityStrategy : IActivityCommitStrategy +{ + public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) + { + return context.LifetimeEvent == ActivityLifetimeEvent.ActivityExecuting ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs new file mode 100644 index 000000000..9bd24513d --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class ActivityExecutedWorkflowStrategy : IWorkflowCommitStrategy +{ + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + return context.LifetimeEvent == WorkflowLifetimeEvent.ActivityExecuted ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs new file mode 100644 index 000000000..02dc647ea --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class ActivityExecutingWorkflowStrategy : IWorkflowCommitStrategy +{ + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + return context.LifetimeEvent == WorkflowLifetimeEvent.ActivityExecuting ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs new file mode 100644 index 000000000..54a995994 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class DefaultWorkflowStrategy : IWorkflowCommitStrategy +{ + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + return CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs new file mode 100644 index 000000000..add5e85a2 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs @@ -0,0 +1,22 @@ +using Elsa.Common; + +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class PeriodicWorkflowStrategy : IWorkflowCommitStrategy +{ + private static readonly object LastCommitPropertyKey = new(); + + public TimeSpan Interval { get; set; } + + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + var lastCommit = context.WorkflowExecutionContext.TransientProperties.TryGetValue(LastCommitPropertyKey, out var value) ? (DateTimeOffset?)value : null; + var now = context.WorkflowExecutionContext.GetRequiredService().UtcNow; + var shouldCommit = lastCommit == null || (now - lastCommit.Value).TotalMilliseconds > Interval.TotalMilliseconds; + + if (shouldCommit) + context.WorkflowExecutionContext.TransientProperties[LastCommitPropertyKey] = now; + + return shouldCommit ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs new file mode 100644 index 000000000..5e6dd50b0 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class WorkflowExecutedWorkflowStrategy : IWorkflowCommitStrategy +{ + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + return context.LifetimeEvent == WorkflowLifetimeEvent.WorkflowExecuted ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs new file mode 100644 index 000000000..643d8e64c --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.CommitStates.Strategies.Workflows; + +public class WorkflowExecutingWorkflowStrategy : IWorkflowCommitStrategy +{ + public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) + { + return context.LifetimeEvent == WorkflowLifetimeEvent.WorkflowExecuting ? CommitAction.Commit : CommitAction.Default; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs new file mode 100644 index 000000000..9c52a4861 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs @@ -0,0 +1,15 @@ +using Elsa.Common; +using Elsa.Workflows.CommitStates.Options; +using Microsoft.Extensions.Options; + +namespace Elsa.Workflows.CommitStates.Tasks; + +public class PopulateCommitStrategyRegistry(ICommitStrategyRegistry registry, IOptions options) : IStartupTask +{ + public Task ExecuteAsync(CancellationToken cancellationToken) + { + foreach (var strategy in options.Value.WorkflowCommitStrategies) registry.RegisterStrategy(strategy.Key, strategy.Value); + foreach (var strategy in options.Value.ActivityCommitStrategies) registry.RegisterStrategy(strategy.Key, strategy.Value); + return Task.CompletedTask; + } +} \ 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 770ac1c75..345295d32 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -4,6 +4,7 @@ using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Workflows.Activities; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Exceptions; using Elsa.Workflows.Helpers; using Elsa.Workflows.Memory; diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings index 300c67063..f646732bd 100644 --- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings @@ -9,6 +9,7 @@ True True True + True True True True diff --git a/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs b/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs deleted file mode 100644 index 3ae3e3062..000000000 --- a/src/modules/Elsa.Workflows.Core/Enums/ActivityCommitStateBehavior.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Elsa.Workflows; - -public enum ActivityCommitStateBehavior -{ - /// - /// Never commit state, regardless of the workflow commit state options. - /// - Never, - - /// - /// Look at the workflow commit state options to determine if state should be committed. - /// - Default, - - /// - /// Commit state before the activity starts. - /// - Executing, - - /// - /// Commit state after the activity executes. - /// - Executed, - - /// - /// Commit state before the activity starts and after the activity executes. - /// - BeforeAndAfterExecution -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs index b38442bd8..bcf0fc6e1 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs @@ -1,4 +1,5 @@ using Elsa.Workflows; +using Elsa.Workflows.CommitStates; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; @@ -11,7 +12,7 @@ public static class ActivityPropertyExtensions private static readonly string[] CanStartWorkflowPropertyName = ["canStartWorkflow", "CanStartWorkflow"]; private static readonly string[] RunAsynchronouslyPropertyName = ["runAsynchronously", "RunAsynchronously"]; private static readonly string[] SourcePropertyName = ["source", "Source"]; - private static readonly string[] CommitStateBehaviorName = ["commitStateBehavior", "CommitStateBehavior"]; + private static readonly string[] CommitStateStrategyName = ["commitStateBehavior", "CommitStateBehavior"]; /// /// Gets a flag indicating whether this activity can be used for starting a workflow. @@ -47,18 +48,18 @@ public static class ActivityPropertyExtensions /// Sets the source file and line number where this activity was instantiated, if any. /// public static void SetSource(this IActivity activity, string value) => activity.CustomProperties[SourcePropertyName[0]] = value; - + /// /// Gets the commit state behavior for the specified activity. /// - public static ActivityCommitStateBehavior GetCommitStateBehavior(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CommitStateBehaviorName, () => ActivityCommitStateBehavior.Default); - + public static string? GetCommitStateStrategy(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CommitStateStrategyName, () => null); + /// /// Sets the commit state behavior for the specified activity. /// - public static TActivity WithCommitStateBehavior(this TActivity activity, ActivityCommitStateBehavior value) where TActivity: IActivity + public static TActivity WithCommitStateStrategy(this TActivity activity, string name) where TActivity : IActivity { - activity.CustomProperties[CommitStateBehaviorName[0]] = value; + activity.CustomProperties[CommitStateStrategyName[0]] = name; return activity; } @@ -73,22 +74,22 @@ public static class ActivityPropertyExtensions var source = $"{Path.GetFileName(sourceFile)}:{lineNumber}"; activity.SetSource(source); } - + /// /// Gets the display text for the specified activity. /// public static string? GetDisplayText(this IActivity activity) => activity.Metadata.TryGetValue("displayText", out var value) ? value.ToString() : null; - + /// /// Sets the display text for the specified activity. /// public static void SetDisplayText(this IActivity activity, string value) => activity.Metadata["displayText"] = value; - + /// /// Gets the description for the specified activity. /// public static string? GetDescription(this IActivity activity) => activity.Metadata.TryGetValue("description", out var value) ? value.ToString() : null; - + /// /// Sets the description for the specified activity. /// diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 67e5cb4a6..1212e8bff 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -9,6 +9,7 @@ using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Workflows.ActivationValidators; using Elsa.Workflows.Builders; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.IncidentStrategies; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.LogPersistence.Strategies; @@ -36,6 +37,7 @@ namespace Elsa.Workflows.Features; [DependsOn(typeof(MediatorFeature))] [DependsOn(typeof(DefaultFormattersFeature))] [DependsOn(typeof(MultitenancyFeature))] +[DependsOn(typeof(CommitStrategiesFeature))] public class WorkflowsFeature : FeatureBase { /// diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index a1d884d10..80fb921ba 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -1,5 +1,6 @@ using Elsa.Extensions; using Elsa.Workflows.Activities; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Pipelines.ActivityExecution; using Microsoft.Extensions.Logging; @@ -19,19 +20,19 @@ public static class ActivityInvokerMiddlewareExtensions /// /// A default activity execution middleware component that evaluates the current activity's properties, executes the activity and adds any produced bookmarks to the workflow execution context. /// -public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, ILogger logger) +public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, ICommitStrategyRegistry commitStrategyRegistry, ILogger logger) : IActivityExecutionMiddleware { /// public async ValueTask InvokeAsync(ActivityExecutionContext context) { context.CancellationToken.ThrowIfCancellationRequested(); - + var workflowExecutionContext = context.WorkflowExecutionContext; // Evaluate input properties. await EvaluateInputPropertiesAsync(context); - + // Prevent the activity from being started if cancellation is requested. if (context.CancellationToken.IsCancellationRequested) { @@ -39,7 +40,7 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I context.AddExecutionLogEntry("Activity cancelled"); return; } - + // Check if the activity can be executed. if (!await context.Activity.CanExecuteAsync(context)) { @@ -47,9 +48,9 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time"); return; } - + // Conditionally commit the workflow state. - if(ShouldCommitWhenExecuting(context)) + if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuting)) await context.WorkflowExecutionContext.CommitAsync(); context.TransitionTo(ActivityStatus.Running); @@ -82,9 +83,9 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks); logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count); } - + // Conditionally commit the workflow state. - if(ShouldCommitWhenExecuted(context)) + if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted)) await context.WorkflowExecutionContext.CommitAsync(); } @@ -119,40 +120,41 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I // Evaluate input properties. await context.EvaluateInputPropertiesAsync(); } - - private bool ShouldCommitWhenExecuting(ActivityExecutionContext context) - { - var behavior = context.Activity.GetCommitStateBehavior(); - - if (behavior == ActivityCommitStateBehavior.Executing) - return true; - if (behavior == ActivityCommitStateBehavior.Default) - { - var workflowOptions = context.WorkflowExecutionContext.Workflow.Options.CommitStateOptions; - - if(workflowOptions.ActivityExecuting) - return true; - } - - return false; - } - - private bool ShouldCommitWhenExecuted(ActivityExecutionContext context) + private bool ShouldCommit(ActivityExecutionContext context, ActivityLifetimeEvent lifetimeEvent) { - var behavior = context.Activity.GetCommitStateBehavior(); - - if (behavior == ActivityCommitStateBehavior.Executed) - return true; + var strategyName = context.Activity.GetCommitStateStrategy(); + var strategy = string.IsNullOrWhiteSpace(strategyName) ? null : commitStrategyRegistry.FindActivityStrategy(strategyName); + var commitAction = CommitAction.Default; - if (behavior == ActivityCommitStateBehavior.Default) + if (strategy != null) { - var workflowOptions = context.WorkflowExecutionContext.Workflow.Options.CommitStateOptions; - - if(workflowOptions.ActivityExecuted) - return true; + var strategyContext = new ActivityCommitStateStrategyContext(context, lifetimeEvent); + commitAction = strategy.ShouldCommit(strategyContext); } + + switch (commitAction) + { + case CommitAction.Skip: + return false; + case CommitAction.Commit: + return true; + case CommitAction.Default: + { + var workflowStrategyName = context.WorkflowExecutionContext.Workflow.Options.CommitStrategyName; + var workflowStrategy = string.IsNullOrWhiteSpace(workflowStrategyName) ? null : commitStrategyRegistry.FindWorkflowStrategy(workflowStrategyName); - return false; + if(workflowStrategy == null) + return false; + + var workflowLifetimeEvent = lifetimeEvent == ActivityLifetimeEvent.ActivityExecuting ? WorkflowLifetimeEvent.ActivityExecuting : WorkflowLifetimeEvent.ActivityExecuted; + var workflowCommitStateStrategyContext = new WorkflowCommitStateStrategyContext(context.WorkflowExecutionContext, workflowLifetimeEvent); + commitAction = workflowStrategy.ShouldCommit(workflowCommitStateStrategyContext); + + return commitAction == CommitAction.Commit; + } + default: + throw new ArgumentOutOfRangeException(nameof(commitAction), commitAction, "Unknown commit action"); + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs index 078474b68..200f8c203 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs @@ -1,4 +1,5 @@ using Elsa.Extensions; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Models; using Elsa.Workflows.Options; using Elsa.Workflows.Pipelines.WorkflowExecution; @@ -19,16 +20,8 @@ public static class UseActivitySchedulerMiddlewareExtensions /// /// A workflow execution middleware component that executes scheduled work items. /// -public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware +public class DefaultActivitySchedulerMiddleware(WorkflowMiddlewareDelegate next, IActivityInvoker activityInvoker, ICommitStrategyRegistry commitStrategyRegistry) : WorkflowExecutionMiddleware(next) { - private readonly IActivityInvoker _activityInvoker; - - /// - public DefaultActivitySchedulerMiddleware(WorkflowMiddlewareDelegate next, IActivityInvoker activityInvoker) : base(next) - { - _activityInvoker = activityInvoker; - } - /// public override async ValueTask InvokeAsync(WorkflowExecutionContext context) { @@ -36,7 +29,7 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware context.TransitionTo(WorkflowSubStatus.Executing); - await ConditionallyCommitStateAsync(context); + await ConditionallyCommitStateAsync(context, WorkflowLifetimeEvent.WorkflowExecuting); while (scheduler.HasAny) { @@ -65,14 +58,21 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware Input = workItem.Input }; - await _activityInvoker.InvokeAsync(context, workItem.Activity, options); + await activityInvoker.InvokeAsync(context, workItem.Activity, options); } - private async Task ConditionallyCommitStateAsync(WorkflowExecutionContext context) + private async Task ConditionallyCommitStateAsync(WorkflowExecutionContext context, WorkflowLifetimeEvent lifetimeEvent) { - var shouldCommit = context.Workflow.Options.CommitStateOptions.Starting; + var strategyName = context.Workflow.Options.CommitStrategyName; + var strategy = string.IsNullOrWhiteSpace(strategyName) ? null : commitStrategyRegistry.FindWorkflowStrategy(strategyName); - if (shouldCommit) + if(strategy == null) + return; + + var strategyContext = new WorkflowCommitStateStrategyContext(context, lifetimeEvent); + var commitAction = strategy.ShouldCommit(strategyContext); + + if (commitAction is CommitAction.Commit) await context.CommitAsync(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs deleted file mode 100644 index 514a255db..000000000 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowCommitStateOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Elsa.Workflows.Models; - -public class WorkflowCommitStateOptions -{ - /// - /// Commit workflow state before the workflow starts. - /// - public bool Starting { get; set; } - - /// - /// Commit workflow state before an activity executes, unless the activity is configured to not commit state. - /// - public bool ActivityExecuting { get; set; } - - /// - /// Commit workflow state after an activity executes, unless the activity is configured to not commit state. - /// - public bool ActivityExecuted { get; set; } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs index f2ac78484..4b688f914 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowOptions.cs @@ -33,5 +33,5 @@ public class WorkflowOptions /// /// The options for committing workflow state. /// - public WorkflowCommitStateOptions CommitStateOptions { get; set; } = new(); + public string? CommitStrategyName { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs index 26b811d89..c5ab81a8f 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs @@ -74,7 +74,7 @@ public class JsonPayloadSerializer : IPayloadSerializer { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; options.Converters.Add(new JsonStringEnumConverter()); diff --git a/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs b/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs index e0a6b7f81..b6b55d6bd 100644 --- a/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Services/NoopCommitStateHandler.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows.CommitStates; using Elsa.Workflows.State; namespace Elsa.Workflows; diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index 7962a96b7..a79842799 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -1,6 +1,7 @@ using Elsa.Extensions; using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Models; using Elsa.Workflows.Notifications; using Elsa.Workflows.Options; diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs index e0ed60f80..640a07744 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Elsa.Extensions; +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Middleware.Activities; using Elsa.Workflows.Models; using Elsa.Workflows.Options; @@ -18,8 +19,9 @@ public class BackgroundActivityInvokerMiddleware( ActivityMiddlewareDelegate next, ILogger logger, IIdentityGenerator identityGenerator, - IBackgroundActivityScheduler backgroundActivityScheduler) - : DefaultActivityInvokerMiddleware(next, logger) + IBackgroundActivityScheduler backgroundActivityScheduler, + ICommitStrategyRegistry commitStrategyRegistry) + : DefaultActivityInvokerMiddleware(next, commitStrategyRegistry, logger) { internal static string GetBackgroundActivityOutputKey(string activityNodeId) => $"__BackgroundActivityOutput:{activityNodeId}"; internal static string GetBackgroundActivityOutcomesKey(string activityNodeId) => $"__BackgroundActivityOutcomes:{activityNodeId}"; diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs index aeb0c37fc..3df97e11c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultCommitStateHandler.cs @@ -1,3 +1,4 @@ +using Elsa.Workflows.CommitStates; using Elsa.Workflows.Management; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Requests; From 4b308cc94a1c7acafb0b16b62f5f5a95a6a4e068 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 14:29:04 +0100 Subject: [PATCH 132/166] Refactor commit strategy naming and implementation Renamed `CommitStateStrategy` to `CommitStrategy` for consistency and clarity. Updated method names, property names, and logic to reflect the new naming convention. Improved null handling when setting commit strategy properties. --- src/apps/Elsa.Server.Web/SampleWorkflow.cs | 10 +++++----- .../Elsa.Workflows.Core/Abstractions/Activity.cs | 7 +++++++ .../Extensions/ActivityPropertyExtensions.cs | 12 +++++++----- .../Activities/DefaultActivityInvokerMiddleware.cs | 2 +- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/apps/Elsa.Server.Web/SampleWorkflow.cs b/src/apps/Elsa.Server.Web/SampleWorkflow.cs index e9030396d..3acb9059a 100644 --- a/src/apps/Elsa.Server.Web/SampleWorkflow.cs +++ b/src/apps/Elsa.Server.Web/SampleWorkflow.cs @@ -14,11 +14,11 @@ public class SampleWorkflow : WorkflowBase { Activities = { - new WriteLine("Commit before executing").WithCommitStateStrategy(nameof(ExecutingActivityStrategy)), - new WriteLine("Commit after executing").WithCommitStateStrategy(nameof(ExecutedActivityStrategy)), - new WriteLine("Commit before & after executing").WithCommitStateStrategy(nameof(CommitAlwaysActivityStrategy)), - new WriteLine("Commit only based on the workflow commit options").WithCommitStateStrategy(nameof(DefaultActivityStrategy)), - new WriteLine("Never commit the workflow when this activity is about to execute or has executed").WithCommitStateStrategy(nameof(CommitNeverActivityStrategy)), + new WriteLine("Commit before executing").SetCommitStrategy(nameof(ExecutingActivityStrategy)), + new WriteLine("Commit after executing").SetCommitStrategy(nameof(ExecutedActivityStrategy)), + new WriteLine("Commit before & after executing").SetCommitStrategy(nameof(CommitAlwaysActivityStrategy)), + new WriteLine("Commit only based on the workflow commit options").SetCommitStrategy(nameof(DefaultActivityStrategy)), + new WriteLine("Never commit the workflow when this activity is about to execute or has executed").SetCommitStrategy(nameof(CommitNeverActivityStrategy)), } }; } diff --git a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs index ad52a70a3..3be660125 100644 --- a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs @@ -73,6 +73,13 @@ public abstract class Activity : IActivity, ISignalHandler get => this.GetRunAsynchronously(); set => this.SetRunAsynchronously(value); } + + [JsonIgnore] + public string? CommitStrategy + { + get => this.GetCommitStrategy(); + set => this.SetCommitStrategy(value); + } /// [JsonConverter(typeof(PolymorphicObjectConverterFactory))] diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs index bcf0fc6e1..ff9bee7c5 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs @@ -1,5 +1,4 @@ using Elsa.Workflows; -using Elsa.Workflows.CommitStates; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; @@ -12,7 +11,7 @@ public static class ActivityPropertyExtensions private static readonly string[] CanStartWorkflowPropertyName = ["canStartWorkflow", "CanStartWorkflow"]; private static readonly string[] RunAsynchronouslyPropertyName = ["runAsynchronously", "RunAsynchronously"]; private static readonly string[] SourcePropertyName = ["source", "Source"]; - private static readonly string[] CommitStateStrategyName = ["commitStateBehavior", "CommitStateBehavior"]; + private static readonly string[] CommitStrategyName = ["commitStrategyName", "CommitStrategyName"]; /// /// Gets a flag indicating whether this activity can be used for starting a workflow. @@ -52,14 +51,17 @@ public static class ActivityPropertyExtensions /// /// Gets the commit state behavior for the specified activity. /// - public static string? GetCommitStateStrategy(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CommitStateStrategyName, () => null); + public static string? GetCommitStrategy(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CommitStrategyName, () => null); /// /// Sets the commit state behavior for the specified activity. /// - public static TActivity WithCommitStateStrategy(this TActivity activity, string name) where TActivity : IActivity + public static TActivity SetCommitStrategy(this TActivity activity, string? name) where TActivity : IActivity { - activity.CustomProperties[CommitStateStrategyName[0]] = name; + if (string.IsNullOrWhiteSpace(name)) + activity.CustomProperties.Remove(CommitStrategyName[0]); + else + activity.CustomProperties[CommitStrategyName[0]] = name; return activity; } diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 80fb921ba..9b5ef9cd9 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -123,7 +123,7 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I private bool ShouldCommit(ActivityExecutionContext context, ActivityLifetimeEvent lifetimeEvent) { - var strategyName = context.Activity.GetCommitStateStrategy(); + var strategyName = context.Activity.GetCommitStrategy(); var strategy = string.IsNullOrWhiteSpace(strategyName) ? null : commitStrategyRegistry.FindActivityStrategy(strategyName); var commitAction = CommitAction.Default; From e151810600419e64d6fe6975e79e5240eb80d852 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 15:14:58 +0100 Subject: [PATCH 133/166] Refactor commit strategy architecture for flexibility. Replaces direct strategy registration with metadata-based registrations, introducing `ObjectRegistration`, `CommitStrategyMetadata`, and associated helpers. Updates existing strategies and APIs to use the new metadata-driven approach, improving extensibility and maintainability. --- src/apps/Elsa.Server.Web/Program.cs | 8 +-- src/apps/Elsa.Server.Web/SampleWorkflow.cs | 2 +- .../CommitStrategies/List/Endpoint.cs | 43 +++++++++++++ .../CommitStates/CommitStrategiesFeature.cs | 63 ++++++++++++++++--- .../Contracts/ICommitStrategyRegistry.cs | 8 +-- .../Helpers/ObjectMetadataDescriber.cs | 23 +++++++ .../Helpers/ObjectRegistrationFactory.cs | 16 +++++ .../ActivityCommitStrategyRegistration.cs | 14 +++++ .../Models/CommitStrategyMetadata.cs | 8 +++ .../CommitStates/Models/ObjectRegistration.cs | 7 +++ .../WorkflowCommitStrategyRegistration.cs | 14 +++++ .../Options/CommitStateOptions.cs | 6 +- ...orkflowCommitStateStrategyJsonConverter.cs | 2 +- .../Services/DefaultCommitStrategyRegistry.cs | 24 +++---- .../CommitAlwaysActivityStrategy.cs | 10 ++- .../Activities/CommitNeverActivityStrategy.cs | 11 +++- .../Activities/DefaultActivityStrategy.cs | 13 +++- .../Activities/ExecutedActivityStrategy.cs | 14 ++++- .../Activities/ExecutingActivityStrategy.cs | 16 ++++- .../ActivityExecutedWorkflowStrategy.cs | 15 ++++- .../ActivityExecutingWorkflowStrategy.cs | 15 ++++- .../Workflows/DefaultWorkflowStrategy.cs | 13 +++- .../Workflows/PeriodicWorkflowStrategy.cs | 12 +++- .../WorkflowExecutedWorkflowStrategy.cs | 9 ++- .../WorkflowExecutingWorkflowStrategy.cs | 15 ++++- .../Tasks/PopulateCommitStrategyRegistry.cs | 7 ++- .../Elsa.Workflows.Core.csproj.DotSettings | 5 ++ 27 files changed, 345 insertions(+), 48 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectMetadataDescriber.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectRegistrationFactory.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStrategyRegistration.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitStrategyMetadata.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/ObjectRegistration.cs create mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStrategyRegistration.cs diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index e02e5e568..03636999e 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -41,8 +41,7 @@ using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.Workflows; using Elsa.Workflows.Api; -using Elsa.Workflows.CommitStates.Strategies.Activities; -using Elsa.Workflows.CommitStates.Strategies.Workflows; +using Elsa.Workflows.CommitStates.Strategies; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Compression; @@ -221,10 +220,7 @@ services strategies.RegisterStrategy(new WorkflowExecutedWorkflowStrategy()); strategies.RegisterStrategy(new ActivityExecutingWorkflowStrategy()); strategies.RegisterStrategy(new ActivityExecutedWorkflowStrategy()); - strategies.RegisterStrategy("Every 10 seconds", new PeriodicWorkflowStrategy - { - Interval = TimeSpan.FromSeconds(10) - }); + strategies.RegisterStrategy("Every 10 seconds", PeriodicWorkflowStrategy.Create(TimeSpan.FromSeconds(10))); // Activity strategies. strategies.RegisterStrategy(new DefaultActivityStrategy()); diff --git a/src/apps/Elsa.Server.Web/SampleWorkflow.cs b/src/apps/Elsa.Server.Web/SampleWorkflow.cs index 3acb9059a..4f031c47d 100644 --- a/src/apps/Elsa.Server.Web/SampleWorkflow.cs +++ b/src/apps/Elsa.Server.Web/SampleWorkflow.cs @@ -1,7 +1,7 @@ using Elsa.Workflows; using Elsa.Workflows.Activities; using Elsa.Extensions; -using Elsa.Workflows.CommitStates.Strategies.Activities; +using Elsa.Workflows.CommitStates.Strategies; namespace Elsa.Server.Web; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs new file mode 100644 index 000000000..2828013ca --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Elsa.Abstractions; +using Elsa.Extensions; +using Elsa.Models; +using Humanizer; + +namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.List; + +/// +/// Returns list of available implementations. +/// +internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/descriptors/incident-strategies"); + ConfigurePermissions("read:incident-strategies"); + } + + public override Task> ExecuteAsync(CancellationToken cancellationToken) + { + var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); + var response =new ListResponse(descriptors); + return Task.FromResult(response); + } +} + +internal record IncidentStrategyDescriptor(string DisplayName, string Description, string TypeName) +{ + public static IncidentStrategyDescriptor FromStrategy(IIncidentStrategy strategy) + { + var type = strategy.GetType(); + var displayNameAttribute = type.GetCustomAttribute(); + var descriptionAttribute = type.GetCustomAttribute(); + var displayAttribute = type.GetCustomAttribute(); + var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize(); + var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? ""; + + return new IncidentStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName()); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs index 9f9c334a2..19c8c2f13 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs @@ -1,7 +1,6 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; -using Elsa.Workflows.CommitStates.Options; using Elsa.Workflows.CommitStates.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -11,22 +10,72 @@ public class CommitStrategiesFeature(IModule module) : FeatureBase(module) { public void RegisterStrategy(IWorkflowCommitStrategy strategy) { - RegisterStrategy(strategy.GetType().Name, strategy); + var registration = ObjectRegistrationFactory.Describe(strategy); + RegisterStrategy(registration); } - public void RegisterStrategy(string name, IWorkflowCommitStrategy strategy) + public void RegisterStrategy(string displayName, IWorkflowCommitStrategy strategy) { - Services.Configure(options => options.WorkflowCommitStrategies[name] = strategy); + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.DisplayName = displayName; + RegisterStrategy(registration); + } + + public void RegisterStrategy(string displayName, string description, IWorkflowCommitStrategy strategy) + { + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.DisplayName = displayName; + registration.Metadata.Description = description; + RegisterStrategy(registration); + } + + public void RegisterStrategy(string name, string displayName, string description, IWorkflowCommitStrategy strategy) + { + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.Name = name; + registration.Metadata.DisplayName = displayName; + registration.Metadata.Description = description; + RegisterStrategy(registration); + } + + public void RegisterStrategy(WorkflowCommitStrategyRegistration registration) + { + Services.Configure(options => options.WorkflowCommitStrategies[registration.Metadata.Name] = registration); } public void RegisterStrategy(IActivityCommitStrategy strategy) { - RegisterStrategy(strategy.GetType().Name, strategy); + var registration = ObjectRegistrationFactory.Describe(strategy); + RegisterStrategy(registration); } - public void RegisterStrategy(string name, IActivityCommitStrategy strategy) + public void RegisterStrategy(string displayName, IActivityCommitStrategy strategy) { - Services.Configure(options => options.ActivityCommitStrategies[name] = strategy); + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.DisplayName = displayName; + RegisterStrategy(registration); + } + + public void RegisterStrategy(string displayName, string description, IActivityCommitStrategy strategy) + { + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.DisplayName = displayName; + registration.Metadata.Description = description; + RegisterStrategy(registration); + } + + public void RegisterStrategy(string name, string displayName, string description, IActivityCommitStrategy strategy) + { + var registration = ObjectRegistrationFactory.Describe(strategy); + registration.Metadata.Name = name; + registration.Metadata.DisplayName = displayName; + registration.Metadata.Description = description; + RegisterStrategy(registration); + } + + public void RegisterStrategy(ActivityCommitStrategyRegistration registration) + { + Services.Configure(options => options.ActivityCommitStrategies[registration.Metadata.Name] = registration); } public override void Apply() diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs index 430e63abb..92a57376c 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/ICommitStrategyRegistry.cs @@ -2,10 +2,10 @@ namespace Elsa.Workflows.CommitStates; public interface ICommitStrategyRegistry { - IEnumerable ListWorkflowStrategies(); - IEnumerable ListActivityStrategies(); - void RegisterStrategy(string name, IWorkflowCommitStrategy strategy); - void RegisterStrategy(string name, IActivityCommitStrategy strategy); + IEnumerable ListWorkflowStrategyRegistrations(); + IEnumerable ListActivityStrategyRegistrations(); + void RegisterStrategy(WorkflowCommitStrategyRegistration registration); + void RegisterStrategy(ActivityCommitStrategyRegistration registration); IWorkflowCommitStrategy? FindWorkflowStrategy(string name); IActivityCommitStrategy? FindActivityStrategy(string name); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectMetadataDescriber.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectMetadataDescriber.cs new file mode 100644 index 000000000..817ae0908 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectMetadataDescriber.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Reflection; +using Humanizer; + +namespace Elsa.Workflows.CommitStates; + +public static class ObjectMetadataDescriber +{ + public static CommitStrategyMetadata Describe(Type strategyType) + { + var suffix = strategyType.IsAssignableTo(typeof(IActivityCommitStrategy)) ? "ActivityStrategy" : "WorkflowStrategy"; + var name = strategyType.Name.Replace(suffix, ""); + var displayName = strategyType.GetCustomAttribute()?.DisplayName ?? strategyType.Name.Replace("CommitStrategy", "").Humanize(); + var description = strategyType.GetCustomAttribute()?.Description ?? string.Empty; + + return new() + { + Name = name, + DisplayName = displayName, + Description = description + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectRegistrationFactory.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectRegistrationFactory.cs new file mode 100644 index 000000000..d06a8ae04 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Helpers/ObjectRegistrationFactory.cs @@ -0,0 +1,16 @@ +namespace Elsa.Workflows.CommitStates; + +public static class ObjectRegistrationFactory +{ + public static WorkflowCommitStrategyRegistration Describe(IWorkflowCommitStrategy strategy) + { + var metadata = ObjectMetadataDescriber.Describe(strategy.GetType()); + return new(strategy, metadata); + } + + public static ActivityCommitStrategyRegistration Describe(IActivityCommitStrategy strategy) + { + var metadata = ObjectMetadataDescriber.Describe(strategy.GetType()); + return new(strategy, metadata); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStrategyRegistration.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStrategyRegistration.cs new file mode 100644 index 000000000..a5bb7b997 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ActivityCommitStrategyRegistration.cs @@ -0,0 +1,14 @@ +namespace Elsa.Workflows.CommitStates; + +public class ActivityCommitStrategyRegistration : ObjectRegistration +{ + public ActivityCommitStrategyRegistration() + { + } + + public ActivityCommitStrategyRegistration(IActivityCommitStrategy strategy, CommitStrategyMetadata metadata) + { + Strategy = strategy; + Metadata = metadata; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitStrategyMetadata.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitStrategyMetadata.cs new file mode 100644 index 000000000..da7a3763a --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/CommitStrategyMetadata.cs @@ -0,0 +1,8 @@ +namespace Elsa.Workflows.CommitStates; + +public class CommitStrategyMetadata +{ + public string Name { get; set; } = null!; + public string DisplayName { get; set; } = null!; + public string Description { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/ObjectRegistration.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ObjectRegistration.cs new file mode 100644 index 000000000..fb98ac5c4 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/ObjectRegistration.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.CommitStates; + +public class ObjectRegistration +{ + public T Strategy { get; set; } = default!; + public TMeta Metadata { get; set; } = default!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStrategyRegistration.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStrategyRegistration.cs new file mode 100644 index 000000000..6842a0114 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Models/WorkflowCommitStrategyRegistration.cs @@ -0,0 +1,14 @@ +namespace Elsa.Workflows.CommitStates; + +public class WorkflowCommitStrategyRegistration : ObjectRegistration +{ + public WorkflowCommitStrategyRegistration() + { + } + + public WorkflowCommitStrategyRegistration(IWorkflowCommitStrategy strategy, CommitStrategyMetadata metadata) + { + Strategy = strategy; + Metadata = metadata; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs index 3a7993986..cf607f986 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Options/CommitStateOptions.cs @@ -1,7 +1,7 @@ -namespace Elsa.Workflows.CommitStates.Options; +namespace Elsa.Workflows.CommitStates; public class CommitStateOptions { - public IDictionary WorkflowCommitStrategies { get; set; } = new Dictionary(); - public IDictionary ActivityCommitStrategies { get; set; } = new Dictionary(); + public IDictionary WorkflowCommitStrategies { get; set; } = new Dictionary(); + public IDictionary ActivityCommitStrategies { get; set; } = new Dictionary(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs index 3ccb7c752..066cfe2df 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs @@ -1,6 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -using Elsa.Workflows.CommitStates.Strategies.Workflows; +using Elsa.Workflows.CommitStates.Strategies; namespace Elsa.Workflows.CommitStates.Serialization { diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs index e2dd19fb1..ff72157d1 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Services/DefaultCommitStrategyRegistry.cs @@ -2,37 +2,37 @@ namespace Elsa.Workflows.CommitStates; public class DefaultCommitStrategyRegistry : ICommitStrategyRegistry { - private readonly IDictionary _workflowStrategies = new Dictionary(); - private readonly IDictionary _activityStrategies = new Dictionary(); + private readonly IDictionary _workflowStrategies = new Dictionary(); + private readonly IDictionary _activityStrategies = new Dictionary(); - public IEnumerable ListWorkflowStrategies() + public IEnumerable ListWorkflowStrategyRegistrations() { - return _workflowStrategies.Keys; + return _workflowStrategies.Values; } - public IEnumerable ListActivityStrategies() + public IEnumerable ListActivityStrategyRegistrations() { - return _activityStrategies.Keys; + return _activityStrategies.Values; } - public void RegisterStrategy(string name, IWorkflowCommitStrategy strategy) + public void RegisterStrategy(WorkflowCommitStrategyRegistration registration) { - _workflowStrategies[name] = strategy; + _workflowStrategies[registration.Metadata.Name] = registration; } - public void RegisterStrategy(string name, IActivityCommitStrategy strategy) + public void RegisterStrategy(ActivityCommitStrategyRegistration registration) { - _activityStrategies[name] = strategy; + _activityStrategies[registration.Metadata.Name] = registration; } public IWorkflowCommitStrategy? FindWorkflowStrategy(string name) { - return _workflowStrategies.TryGetValue(name, out var strategy) ? strategy : null; + return _workflowStrategies.TryGetValue(name, out var registration) ? registration.Strategy : null; } public IActivityCommitStrategy? FindActivityStrategy(string name) { - return _activityStrategies.TryGetValue(name, out var strategy) ? strategy : null; + return _activityStrategies.TryGetValue(name, out var registration) ? registration.Strategy : null; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs index 2217e07ec..c803a371a 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitAlwaysActivityStrategy.cs @@ -1,5 +1,13 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Activities; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents a strategy that always commits the workflow whenever the associated activity is about to execute or has executed. +/// This strategy ensures that the workflow's state is persisted at both pre-execution and post-execution stages of the activity. +/// +[DisplayName("Always Commit")] +[Description("Always commit the workflow state when the activity is about to execute or has executed.")] public class CommitAlwaysActivityStrategy : IActivityCommitStrategy { public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs index b2f022358..cdc42514a 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/CommitNeverActivityStrategy.cs @@ -1,5 +1,14 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Activities; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// A strategy implementation of that specifies +/// the workflow should never commit when the associated activity is executed or has executed. +/// This overrides any workflow-level commit strategy. +/// +[DisplayName("Never Commit")] +[Description("Never commit the workflow state when the activity is about to execute or has executed.")] public class CommitNeverActivityStrategy : IActivityCommitStrategy { public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs index fcf307ce8..5eeaedc69 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/DefaultActivityStrategy.cs @@ -1,5 +1,16 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Activities; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents the default activity commit strategy for workflow activities. +/// +/// +/// This strategy determines whether a workflow should commit changes during the execution of an activity. +/// By default, it delegates the commit behavior to the workflow's global commit options or other overriding strategies. +/// +[DisplayName("Default")] +[Description("The default activity commit strategy.")] public class DefaultActivityStrategy : IActivityCommitStrategy { public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs index 393e1960c..d4c1ec976 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutedActivityStrategy.cs @@ -1,5 +1,17 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Activities; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents an activity commit strategy that determines whether a commit action should occur +/// after an activity has executed within a workflow. +/// +/// +/// This strategy evaluates the activity's execution events, specifically committing only if the +/// activity's lifetime event is "ActivityExecuted". +/// +[DisplayName("Executed")] +[Description("Commit the workflow state after the activity has executed.")] public class ExecutedActivityStrategy : IActivityCommitStrategy { public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs index f8531c5d3..a16ea900a 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Activities/ExecutingActivityStrategy.cs @@ -1,5 +1,19 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Activities; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents a commit strategy that evaluates whether a workflow commit should occur +/// when an activity is in the "Executing" state. +/// +/// +/// This strategy determines commit behavior based on the activity's lifecycle event. +/// Specifically, it commits the workflow if the activity is currently executing +/// (i.e., the lifetime event is `ActivityLifetimeEvent.ActivityExecuting`). +/// For all other states, it defaults to no specific commit action. +/// +[DisplayName("Executing")] +[Description("Commit the workflow state when the activity is executing.")] public class ExecutingActivityStrategy : IActivityCommitStrategy { public CommitAction ShouldCommit(ActivityCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs index 9bd24513d..d35187ff9 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutedWorkflowStrategy.cs @@ -1,5 +1,18 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents a commit strategy that determines whether a workflow state should be committed +/// based on the "ActivityExecuted" lifetime event of an activity. +/// +/// +/// This strategy evaluates the provided during execution +/// and returns a action if the activity's lifetime event corresponds +/// to . Otherwise, it defaults to . +/// +[DisplayName("Activity Executed")] +[Description("Determines whether a workflow state should be committed if the current activity has executed.")] public class ActivityExecutedWorkflowStrategy : IWorkflowCommitStrategy { public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs index 02dc647ea..5f891a525 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/ActivityExecutingWorkflowStrategy.cs @@ -1,5 +1,18 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents a workflow commit strategy that determines whether a commit should occur +/// during the ActivityExecuting lifecycle event of an activity. +/// +/// +/// This strategy evaluates the workflow execution context and checks if the current lifecycle event is +/// . If the condition is met, the strategy indicates +/// that a commit action should be performed. Otherwise, a default action will be returned. +/// +[DisplayName("Activity Executing")] +[Description("Determines whether a workflow state should be committed if the current activity is executing.")] public class ActivityExecutingWorkflowStrategy : IWorkflowCommitStrategy { public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs index 54a995994..505f32788 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/DefaultWorkflowStrategy.cs @@ -1,5 +1,16 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents the default strategy for determining whether a workflow should commit its state. +/// +/// +/// This strategy always returns the default commit action as defined by the enum, +/// ensuring that workflows adhere to the standard behavior unless overridden by custom strategies. +/// +[DisplayName("Default")] +[Description("The default workflow commit strategy.")] public class DefaultWorkflowStrategy : IWorkflowCommitStrategy { public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs index add5e85a2..2f35bb086 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs @@ -1,11 +1,21 @@ +using System.ComponentModel; using Elsa.Common; -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +namespace Elsa.Workflows.CommitStates.Strategies; +/// +/// Implements a periodic workflow commit strategy based on a specified time interval. +/// This strategy determines if a workflow should commit by comparing the elapsed time +/// since the last commit with the configured interval. +/// +[DisplayName("Periodic")] +[Description("Determines whether a workflow state should be committed based on a specified time interval.")] public class PeriodicWorkflowStrategy : IWorkflowCommitStrategy { private static readonly object LastCommitPropertyKey = new(); + public static PeriodicWorkflowStrategy Create(TimeSpan interval) => new() { Interval = interval }; + public TimeSpan Interval { get; set; } public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs index 5e6dd50b0..8be2a361a 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutedWorkflowStrategy.cs @@ -1,5 +1,12 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Represents a workflow commit strategy that commits changes when the workflow has been executed. +/// +[DisplayName("Workflow Executed")] +[Description("Commit the workflow state when the workflow has been executed.")] public class WorkflowExecutedWorkflowStrategy : IWorkflowCommitStrategy { public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs index 643d8e64c..91162258e 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/WorkflowExecutingWorkflowStrategy.cs @@ -1,5 +1,18 @@ -namespace Elsa.Workflows.CommitStates.Strategies.Workflows; +using System.ComponentModel; +namespace Elsa.Workflows.CommitStates.Strategies; + +/// +/// Implements a commit strategy that determines whether the workflow state +/// should be committed when the workflow is in the "executing" lifetime event. +/// +/// +/// This strategy checks the current context's `LifetimeEvent` and commits +/// the workflow state if it corresponds to the `WorkflowExecuting` event. +/// Otherwise, it defaults to no explicit commit action. +/// +[DisplayName("Workflow Executing")] +[Description("Commit the workflow state when the workflow is executing.")] public class WorkflowExecutingWorkflowStrategy : IWorkflowCommitStrategy { public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs index 9c52a4861..2bd9b962d 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Tasks/PopulateCommitStrategyRegistry.cs @@ -1,15 +1,16 @@ using Elsa.Common; -using Elsa.Workflows.CommitStates.Options; +using JetBrains.Annotations; using Microsoft.Extensions.Options; namespace Elsa.Workflows.CommitStates.Tasks; +[UsedImplicitly] public class PopulateCommitStrategyRegistry(ICommitStrategyRegistry registry, IOptions options) : IStartupTask { public Task ExecuteAsync(CancellationToken cancellationToken) { - foreach (var strategy in options.Value.WorkflowCommitStrategies) registry.RegisterStrategy(strategy.Key, strategy.Value); - foreach (var strategy in options.Value.ActivityCommitStrategies) registry.RegisterStrategy(strategy.Key, strategy.Value); + foreach (var strategy in options.Value.WorkflowCommitStrategies.Values) registry.RegisterStrategy(strategy); + foreach (var strategy in options.Value.ActivityCommitStrategies.Values) registry.RegisterStrategy(strategy); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings index f646732bd..63e48fa9c 100644 --- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings @@ -8,8 +8,13 @@ True True True + True + True True + True True + True + True True True True From 166c1bc11702a4856df779cd3a43dfd2ed79ca75 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 15:19:51 +0100 Subject: [PATCH 134/166] Add endpoints for listing workflow commit strategies Introduced two API endpoints to list activity and workflow commit strategy registrations. Updated the project file to include the new directory structure for the endpoints. These endpoints provide unified responses from the respective registry implementations. --- .../Elsa.Workflows.Api.csproj | 4 ++ .../Activities/List/Endpoint.cs | 28 ++++++++++++ .../CommitStrategies/List/Endpoint.cs | 43 ------------------- .../Workflows/List/Endpoint.cs | 28 ++++++++++++ 4 files changed, 60 insertions(+), 43 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs delete mode 100644 src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs create mode 100644 src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs diff --git a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj index 4fff95dc9..94828c0a3 100644 --- a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj +++ b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs new file mode 100644 index 000000000..c7383256a --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs @@ -0,0 +1,28 @@ +using Elsa.Abstractions; +using Elsa.Models; +using Elsa.Workflows.CommitStates; + +namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Activities.List; + +/// +/// Represents an API endpoint that provides a list of registered workflow commit strategies. +/// +/// +/// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations +/// from a provided registry and returns them in a unified response. +/// +internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/descriptors/commit-strategies/activities"); + ConfigurePermissions("read:commit-strategies"); + } + + public override Task> ExecuteAsync(CancellationToken cancellationToken) + { + var descriptors = registry.ListActivityStrategyRegistrations().ToList(); + var response =new ListResponse(descriptors); + return Task.FromResult(response); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs deleted file mode 100644 index 2828013ca..000000000 --- a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/List/Endpoint.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using Elsa.Abstractions; -using Elsa.Extensions; -using Elsa.Models; -using Humanizer; - -namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.List; - -/// -/// Returns list of available implementations. -/// -internal class List(IEnumerable strategies) : ElsaEndpointWithoutRequest> -{ - public override void Configure() - { - Get("/descriptors/incident-strategies"); - ConfigurePermissions("read:incident-strategies"); - } - - public override Task> ExecuteAsync(CancellationToken cancellationToken) - { - var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList(); - var response =new ListResponse(descriptors); - return Task.FromResult(response); - } -} - -internal record IncidentStrategyDescriptor(string DisplayName, string Description, string TypeName) -{ - public static IncidentStrategyDescriptor FromStrategy(IIncidentStrategy strategy) - { - var type = strategy.GetType(); - var displayNameAttribute = type.GetCustomAttribute(); - var descriptionAttribute = type.GetCustomAttribute(); - var displayAttribute = type.GetCustomAttribute(); - var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize(); - var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? ""; - - return new IncidentStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName()); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs new file mode 100644 index 000000000..3ee03c549 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs @@ -0,0 +1,28 @@ +using Elsa.Abstractions; +using Elsa.Models; +using Elsa.Workflows.CommitStates; + +namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Workflows.List; + +/// +/// Represents an API endpoint that provides a list of registered workflow commit strategies. +/// +/// +/// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations +/// from a provided registry and returns them in a unified response. +/// +internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/descriptors/commit-strategies/workflows"); + ConfigurePermissions("read:commit-strategies"); + } + + public override Task> ExecuteAsync(CancellationToken cancellationToken) + { + var descriptors = registry.ListWorkflowStrategyRegistrations().ToList(); + var response =new ListResponse(descriptors); + return Task.FromResult(response); + } +} \ No newline at end of file From ef97ef415f4eea394a1f9a664000424614a9b9d4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 15:20:51 +0100 Subject: [PATCH 135/166] Update strategy registration methods to use 'Add' Replaced 'RegisterStrategy' with 'Add' in strategy registration methods for both workflows and activities. This simplifies the naming, enhances consistency, and aligns with modern API design principles. --- src/apps/Elsa.Server.Web/Program.cs | 22 ++++++------ .../CommitStates/CommitStrategiesFeature.cs | 36 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 03636999e..d95d56a20 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -215,19 +215,19 @@ services workflows.UseCommitStrategies(strategies => { // Workflow strategies. - strategies.RegisterStrategy(new DefaultWorkflowStrategy()); - strategies.RegisterStrategy(new WorkflowExecutingWorkflowStrategy()); - strategies.RegisterStrategy(new WorkflowExecutedWorkflowStrategy()); - strategies.RegisterStrategy(new ActivityExecutingWorkflowStrategy()); - strategies.RegisterStrategy(new ActivityExecutedWorkflowStrategy()); - strategies.RegisterStrategy("Every 10 seconds", PeriodicWorkflowStrategy.Create(TimeSpan.FromSeconds(10))); + strategies.Add(new DefaultWorkflowStrategy()); + strategies.Add(new WorkflowExecutingWorkflowStrategy()); + strategies.Add(new WorkflowExecutedWorkflowStrategy()); + strategies.Add(new ActivityExecutingWorkflowStrategy()); + strategies.Add(new ActivityExecutedWorkflowStrategy()); + strategies.Add("Every 10 seconds", PeriodicWorkflowStrategy.Create(TimeSpan.FromSeconds(10))); // Activity strategies. - strategies.RegisterStrategy(new DefaultActivityStrategy()); - strategies.RegisterStrategy(new CommitAlwaysActivityStrategy()); - strategies.RegisterStrategy(new CommitNeverActivityStrategy()); - strategies.RegisterStrategy(new ExecutingActivityStrategy()); - strategies.RegisterStrategy(new ExecutedActivityStrategy()); + strategies.Add(new DefaultActivityStrategy()); + strategies.Add(new CommitAlwaysActivityStrategy()); + strategies.Add(new CommitNeverActivityStrategy()); + strategies.Add(new ExecutingActivityStrategy()); + strategies.Add(new ExecutedActivityStrategy()); }); }) .UseWorkflowManagement(management => diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs index 19c8c2f13..bf0261574 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs @@ -8,72 +8,72 @@ namespace Elsa.Workflows.CommitStates; public class CommitStrategiesFeature(IModule module) : FeatureBase(module) { - public void RegisterStrategy(IWorkflowCommitStrategy strategy) + public void Add(IWorkflowCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string displayName, IWorkflowCommitStrategy strategy) + public void Add(string displayName, IWorkflowCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.DisplayName = displayName; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string displayName, string description, IWorkflowCommitStrategy strategy) + public void Add(string displayName, string description, IWorkflowCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.DisplayName = displayName; registration.Metadata.Description = description; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string name, string displayName, string description, IWorkflowCommitStrategy strategy) + public void Add(string name, string displayName, string description, IWorkflowCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.Name = name; registration.Metadata.DisplayName = displayName; registration.Metadata.Description = description; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(WorkflowCommitStrategyRegistration registration) + public void Add(WorkflowCommitStrategyRegistration registration) { Services.Configure(options => options.WorkflowCommitStrategies[registration.Metadata.Name] = registration); } - public void RegisterStrategy(IActivityCommitStrategy strategy) + public void Add(IActivityCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string displayName, IActivityCommitStrategy strategy) + public void Add(string displayName, IActivityCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.DisplayName = displayName; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string displayName, string description, IActivityCommitStrategy strategy) + public void Add(string displayName, string description, IActivityCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.DisplayName = displayName; registration.Metadata.Description = description; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(string name, string displayName, string description, IActivityCommitStrategy strategy) + public void Add(string name, string displayName, string description, IActivityCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); registration.Metadata.Name = name; registration.Metadata.DisplayName = displayName; registration.Metadata.Description = description; - RegisterStrategy(registration); + Add(registration); } - public void RegisterStrategy(ActivityCommitStrategyRegistration registration) + public void Add(ActivityCommitStrategyRegistration registration) { Services.Configure(options => options.ActivityCommitStrategies[registration.Metadata.Name] = registration); } From 01a41f6d627ffb55d092258cce4ac21379c2ce4d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 18:10:38 +0100 Subject: [PATCH 136/166] Refactor commit strategies to use AddStandardStrategies method Extracted commit strategies setup into a reusable AddStandardStrategies method for better code clarity and modularity. Simplified PeriodicWorkflowStrategy by replacing the factory method with a constructor-based interval initialization. These changes improve maintainability and reduce redundancy across the codebase. --- src/apps/Elsa.Server.Web/Program.cs | 16 ++-------------- .../CommitStates/CommitStrategiesFeature.cs | 18 ++++++++++++++++++ .../Workflows/PeriodicWorkflowStrategy.cs | 7 ++----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index d95d56a20..410588913 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -214,20 +214,8 @@ services workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing()); workflows.UseCommitStrategies(strategies => { - // Workflow strategies. - strategies.Add(new DefaultWorkflowStrategy()); - strategies.Add(new WorkflowExecutingWorkflowStrategy()); - strategies.Add(new WorkflowExecutedWorkflowStrategy()); - strategies.Add(new ActivityExecutingWorkflowStrategy()); - strategies.Add(new ActivityExecutedWorkflowStrategy()); - strategies.Add("Every 10 seconds", PeriodicWorkflowStrategy.Create(TimeSpan.FromSeconds(10))); - - // Activity strategies. - strategies.Add(new DefaultActivityStrategy()); - strategies.Add(new CommitAlwaysActivityStrategy()); - strategies.Add(new CommitNeverActivityStrategy()); - strategies.Add(new ExecutingActivityStrategy()); - strategies.Add(new ExecutedActivityStrategy()); + strategies.AddStandardStrategies(); + strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10))); }); }) .UseWorkflowManagement(management => diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs index bf0261574..0e43d71f5 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/CommitStrategiesFeature.cs @@ -1,6 +1,7 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.Workflows.CommitStates.Strategies; using Elsa.Workflows.CommitStates.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -8,6 +9,23 @@ namespace Elsa.Workflows.CommitStates; public class CommitStrategiesFeature(IModule module) : FeatureBase(module) { + public void AddStandardStrategies() + { + // Workflow commit strategies. + Add(new DefaultWorkflowStrategy()); + Add(new WorkflowExecutingWorkflowStrategy()); + Add(new WorkflowExecutedWorkflowStrategy()); + Add(new ActivityExecutingWorkflowStrategy()); + Add(new ActivityExecutedWorkflowStrategy()); + + // Activity commit strategies. + Add(new DefaultActivityStrategy()); + Add(new CommitAlwaysActivityStrategy()); + Add(new CommitNeverActivityStrategy()); + Add(new ExecutingActivityStrategy()); + Add(new ExecutedActivityStrategy()); + } + public void Add(IWorkflowCommitStrategy strategy) { var registration = ObjectRegistrationFactory.Describe(strategy); diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs index 2f35bb086..62f481667 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Strategies/Workflows/PeriodicWorkflowStrategy.cs @@ -10,13 +10,10 @@ namespace Elsa.Workflows.CommitStates.Strategies; /// [DisplayName("Periodic")] [Description("Determines whether a workflow state should be committed based on a specified time interval.")] -public class PeriodicWorkflowStrategy : IWorkflowCommitStrategy +public class PeriodicWorkflowStrategy(TimeSpan interval) : IWorkflowCommitStrategy { private static readonly object LastCommitPropertyKey = new(); - - public static PeriodicWorkflowStrategy Create(TimeSpan interval) => new() { Interval = interval }; - - public TimeSpan Interval { get; set; } + public TimeSpan Interval { get; } = interval; public CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context) { From 2402bacabab5a6a1c2811958285ef840682c8eef Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 18:11:35 +0100 Subject: [PATCH 137/166] Remove custom JSON converter for workflow commit strategies. Eliminated `WorkflowCommitStateStrategyJsonConverter` and its usage in the `IWorkflowCommitStrategy` interface. This simplifies serialization logic and reduces unnecessary complexity in the codebase. --- .../Contracts/IWorkflowCommitStrategy.cs | 4 -- ...orkflowCommitStateStrategyJsonConverter.cs | 60 ------------------- 2 files changed, 64 deletions(-) delete mode 100644 src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs index 639936c9a..074d4c857 100644 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/CommitStates/Contracts/IWorkflowCommitStrategy.cs @@ -1,9 +1,5 @@ -using System.Text.Json.Serialization; -using Elsa.Workflows.CommitStates.Serialization; - namespace Elsa.Workflows.CommitStates; -[JsonConverter(typeof(WorkflowCommitStateStrategyJsonConverter))] public interface IWorkflowCommitStrategy { CommitAction ShouldCommit(WorkflowCommitStateStrategyContext context); diff --git a/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs b/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs deleted file mode 100644 index 066cfe2df..000000000 --- a/src/modules/Elsa.Workflows.Core/CommitStates/Serialization/WorkflowCommitStateStrategyJsonConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Elsa.Workflows.CommitStates.Strategies; - -namespace Elsa.Workflows.CommitStates.Serialization -{ - public class WorkflowCommitStateStrategyJsonConverter : JsonConverter - { - public override IWorkflowCommitStrategy? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - throw new JsonException("Expected StartObject token"); - - // Read the JSON object - using var document = JsonDocument.ParseValue(ref reader); - var rootElement = document.RootElement; - - // Extract the type information - if (!rootElement.TryGetProperty("$type", out var typeProperty)) - return new DefaultWorkflowStrategy(); - - var typeName = typeProperty.GetString(); - if (string.IsNullOrEmpty(typeName)) throw new JsonException("The $type property is empty or null"); - - // Resolve the type from the type name - var type = Type.GetType(typeName); - if (type == null) throw new JsonException($"Could not resolve type: {typeName}"); - - // Ensure the type implements the expected interface - if (!typeof(IWorkflowCommitStrategy).IsAssignableFrom(type)) throw new JsonException($"The type {typeName} does not implement IWorkflowCommitStateStrategy"); - - // Deserialize the "value" object to the resolved type - if (!rootElement.TryGetProperty("value", out var valueProperty)) throw new JsonException("Could not find 'value' property in JSON payload"); - - var value = JsonSerializer.Deserialize(valueProperty.GetRawText(), type, options); - - // Ensure the deserialized object is an IWorkflowCommitStateStrategy - return value as IWorkflowCommitStrategy ?? throw new JsonException($"Deserialized object is not an IWorkflowCommitStateStrategy"); - } - - public override void Write(Utf8JsonWriter writer, IWorkflowCommitStrategy value, JsonSerializerOptions options) - { - if (value == null) throw new ArgumentNullException(nameof(value)); - - // Writing the type name for deserialization purposes. - var type = value.GetType(); - - writer.WriteStartObject(); - - // Serialize the type name to enable proper deserialization - writer.WriteString("$type", type.AssemblyQualifiedName); - - // Serialize the object using the default serializer - writer.WritePropertyName("value"); - JsonSerializer.Serialize(writer, value, type, options); - - writer.WriteEndObject(); - } - } -} \ No newline at end of file From 47a7c3171457851586a489349dfb9fd322bda469 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 18:32:15 +0100 Subject: [PATCH 138/166] Refactor commit strategies API to unify descriptor model. Standardized commit strategy descriptors across workflows and activities by introducing a shared `CommitStrategyDescriptor` model. Updated API endpoints and conversion logic to adopt this unified approach, improving consistency and reusability. Implemented new interfaces in the client API for listing commit strategies. --- .../Contracts/IIncidentStrategiesApi.cs | 25 +++++++++++++++++++ .../Models/CommitStrategyDescriptor.cs | 7 ++++++ .../Activities/List/Endpoint.cs | 9 ++++--- .../Endpoints/CommitStrategies/Models.cs | 16 ++++++++++++ .../Workflows/List/Endpoint.cs | 9 ++++--- 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs create mode 100644 src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Models.cs diff --git a/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs new file mode 100644 index 000000000..cfee71fcc --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Contracts/IIncidentStrategiesApi.cs @@ -0,0 +1,25 @@ +using Elsa.Api.Client.Resources.CommitStrategies.Models; +using Elsa.Api.Client.Shared.Models; +using Refit; + +namespace Elsa.Api.Client.Resources.CommitStrategies.Contracts; + +/// +/// Represents a client for the commit strategies API. +/// +public interface ICommitStrategiesApi +{ + /// + /// Lists workflow commit strategies. + /// + /// A list response containing activity commit strategy descriptors and their count. + [Get("/descriptors/commit-strategies/workflows")] + Task> ListWorkflowStrategiesAsync(CancellationToken cancellationToken = default); + + /// + /// Lists activity commit strategies. + /// + /// A list response containing activity commit strategy descriptors and their count. + [Get("/descriptors/commit-strategies/activities")] + Task> ListActivityStrategiesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs new file mode 100644 index 000000000..ad63d5931 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/CommitStrategies/Models/CommitStrategyDescriptor.cs @@ -0,0 +1,7 @@ +namespace Elsa.Api.Client.Resources.CommitStrategies.Models; + +/// +/// Represents a descriptor for a commit strategy, containing information such as its technical name, +/// display name, and description. +/// +public record CommitStrategyDescriptor(string Name, string DisplayName, string Description); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs index c7383256a..85fdcdcd0 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Activities/List/Endpoint.cs @@ -11,7 +11,7 @@ namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Activities.List; /// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations /// from a provided registry and returns them in a unified response. /// -internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> +internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> { public override void Configure() { @@ -19,10 +19,11 @@ internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutReque ConfigurePermissions("read:commit-strategies"); } - public override Task> ExecuteAsync(CancellationToken cancellationToken) + public override Task> ExecuteAsync(CancellationToken cancellationToken) { - var descriptors = registry.ListActivityStrategyRegistrations().ToList(); - var response =new ListResponse(descriptors); + var registrations = registry.ListActivityStrategyRegistrations().ToList(); + var descriptors = CommitStrategyDescriptor.FromStrategyMetadata(registrations.Select(x => x.Metadata)).ToList(); + var response =new ListResponse(descriptors); return Task.FromResult(response); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Models.cs new file mode 100644 index 000000000..0af9b5e50 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Models.cs @@ -0,0 +1,16 @@ +using Elsa.Workflows.CommitStates; + +namespace Elsa.Workflows.Api.Endpoints.CommitStrategies; + +internal record CommitStrategyDescriptor(string Name, string DisplayName, string Description) +{ + public static CommitStrategyDescriptor FromStrategyMetadata(CommitStrategyMetadata metadata) + { + return new(metadata.Name, metadata.DisplayName, metadata.Description); + } + + public static IEnumerable FromStrategyMetadata(IEnumerable metadata) + { + return metadata.Select(FromStrategyMetadata); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs index 3ee03c549..e7f3fc327 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/CommitStrategies/Workflows/List/Endpoint.cs @@ -11,7 +11,7 @@ namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Workflows.List; /// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations /// from a provided registry and returns them in a unified response. /// -internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> +internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest> { public override void Configure() { @@ -19,10 +19,11 @@ internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutReque ConfigurePermissions("read:commit-strategies"); } - public override Task> ExecuteAsync(CancellationToken cancellationToken) + public override Task> ExecuteAsync(CancellationToken cancellationToken) { - var descriptors = registry.ListWorkflowStrategyRegistrations().ToList(); - var response =new ListResponse(descriptors); + var registrations = registry.ListWorkflowStrategyRegistrations().ToList(); + var descriptors = CommitStrategyDescriptor.FromStrategyMetadata(registrations.Select(x => x.Metadata)).ToList(); + var response =new ListResponse(descriptors); return Task.FromResult(response); } } \ No newline at end of file From ab9aadf402570baff3d6bce86d4513f1d0d6005e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 21:24:07 +0100 Subject: [PATCH 139/166] `Refactor commit state behavior to use commit strategies` Replaced `ActivityCommitStateBehavior` with a more flexible commit strategy approach utilizing `CommitStrategyDescriptor`. Updated relevant APIs, services, and UI components to support the new model, enhancing configurability and maintainability. --- .../Extensions/ActivityExtensions.cs | 4 +-- .../DependencyInjectionExtensions.cs | 2 ++ .../Models/ActivityCommitStateBehavior.cs | 29 ------------------- .../Models/WorkflowOptions.cs | 2 +- 4 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs diff --git a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs index f39fc90bc..4990ea81a 100644 --- a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs @@ -187,10 +187,10 @@ public static class ActivityExtensions /// /// Gets the commit state behavior for the specified activity. /// - public static ActivityCommitStateBehavior GetCommitStateBehavior(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStateBehavior") ?? ActivityCommitStateBehavior.Default; + public static string? GetCommitStrategy(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStrategyName"); /// /// Sets the commit state behavior for the specified activity. /// - public static void SetCommitStateBehavior(this JsonObject activity, ActivityCommitStateBehavior value) => activity.SetProperty(JsonValue.Create(value.ToString()), "customProperties", "commitStateBehavior"); + public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName"); } \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs index e8bb1afe3..3e23b2e45 100644 --- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs @@ -2,6 +2,7 @@ using Elsa.Api.Client.Options; using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts; using Elsa.Api.Client.Resources.ActivityDescriptors.Contracts; using Elsa.Api.Client.Resources.ActivityExecutions.Contracts; +using Elsa.Api.Client.Resources.CommitStrategies.Contracts; using Elsa.Api.Client.Resources.Features.Contracts; using Elsa.Api.Client.Resources.Identity.Contracts; using Elsa.Api.Client.Resources.IncidentStrategies.Contracts; @@ -73,6 +74,7 @@ public static class DependencyInjectionExtensions services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); + services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs deleted file mode 100644 index 66d6dbdf6..000000000 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; - -public enum ActivityCommitStateBehavior -{ - /// - /// Never commit state, regardless of the workflow commit state options. - /// - Never, - - /// - /// Look at the workflow commit state options to determine if state should be committed. - /// - Default, - - /// - /// Commit state before the activity starts. - /// - Executing, - - /// - /// Commit state after the activity executes. - /// - Executed, - - /// - /// Commit state before the activity starts and after the activity executes. - /// - BeforeAndAfterExecution -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs index c3f38f381..07b5c882d 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs @@ -33,5 +33,5 @@ public class WorkflowOptions /// /// The options for committing workflow state. /// - public WorkflowCommitStateOptions CommitStateOptions { get; set; } = new(); + public string? CommitStrategyName { get; set; } } \ No newline at end of file From ca0dcd2f831f69fbe8f43c0966b947423b3b7e8a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 21:24:07 +0100 Subject: [PATCH 140/166] Refactor commit state behavior to use commit strategies Replaced `ActivityCommitStateBehavior` with a more flexible commit strategy approach utilizing `CommitStrategyDescriptor`. Updated relevant APIs, services, and UI components to support the new model, enhancing configurability and maintainability. --- .../Extensions/ActivityExtensions.cs | 4 +-- .../DependencyInjectionExtensions.cs | 2 ++ .../Models/ActivityCommitStateBehavior.cs | 29 ------------------- .../Models/WorkflowOptions.cs | 2 +- 4 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs diff --git a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs index f39fc90bc..4990ea81a 100644 --- a/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/ActivityExtensions.cs @@ -187,10 +187,10 @@ public static class ActivityExtensions /// /// Gets the commit state behavior for the specified activity. /// - public static ActivityCommitStateBehavior GetCommitStateBehavior(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStateBehavior") ?? ActivityCommitStateBehavior.Default; + public static string? GetCommitStrategy(this JsonObject activity) => activity.TryGetProperty("customProperties", "commitStrategyName"); /// /// Sets the commit state behavior for the specified activity. /// - public static void SetCommitStateBehavior(this JsonObject activity, ActivityCommitStateBehavior value) => activity.SetProperty(JsonValue.Create(value.ToString()), "customProperties", "commitStateBehavior"); + public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName"); } \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs index e8bb1afe3..3e23b2e45 100644 --- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs @@ -2,6 +2,7 @@ using Elsa.Api.Client.Options; using Elsa.Api.Client.Resources.ActivityDescriptorOptions.Contracts; using Elsa.Api.Client.Resources.ActivityDescriptors.Contracts; using Elsa.Api.Client.Resources.ActivityExecutions.Contracts; +using Elsa.Api.Client.Resources.CommitStrategies.Contracts; using Elsa.Api.Client.Resources.Features.Contracts; using Elsa.Api.Client.Resources.Identity.Contracts; using Elsa.Api.Client.Resources.IncidentStrategies.Contracts; @@ -73,6 +74,7 @@ public static class DependencyInjectionExtensions services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); + services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs deleted file mode 100644 index 66d6dbdf6..000000000 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityCommitStateBehavior.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; - -public enum ActivityCommitStateBehavior -{ - /// - /// Never commit state, regardless of the workflow commit state options. - /// - Never, - - /// - /// Look at the workflow commit state options to determine if state should be committed. - /// - Default, - - /// - /// Commit state before the activity starts. - /// - Executing, - - /// - /// Commit state after the activity executes. - /// - Executed, - - /// - /// Commit state before the activity starts and after the activity executes. - /// - BeforeAndAfterExecution -} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs index c3f38f381..07b5c882d 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowOptions.cs @@ -33,5 +33,5 @@ public class WorkflowOptions /// /// The options for committing workflow state. /// - public WorkflowCommitStateOptions CommitStateOptions { get; set; } = new(); + public string? CommitStrategyName { get; set; } } \ No newline at end of file From e54e116977b1aba28717b6584ca480e33ba5384e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 22:56:30 +0100 Subject: [PATCH 141/166] Fix activity output Updated `ActivityOutputRegister` to always return the last output value. Added new integration test to validate the functionality. --- .../Models/ActivityOutputRegister.cs | 2 +- .../ActivityOutputs/LoopingWorkflow.cs | 33 +++++++++++++++++++ .../Scenarios/ActivityOutputs/Tests.cs | 16 +++++++-- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs index ac72cf660..181aee62f 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityOutputRegister.cs @@ -79,7 +79,7 @@ public class ActivityOutputRegister var key = CreateActivityIdLookupKey(activityId, outputName); return !_recordsByActivityIdAndOutputName.TryGetValue(key, out var records) ? null - : records.FirstOrDefault()?.Value; + : records.LastOrDefault()?.Value; // Always return the last value. } /// diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs new file mode 100644 index 000000000..877048a3f --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs @@ -0,0 +1,33 @@ +using Elsa.Extensions; +using Elsa.Workflows.Activities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ActivityOutputs; + +public class LoopingWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var readCurrentValue = new Inline(context => context.GetVariable("CurrentValue")!); + + builder.Root = new ForEach( + [ + "Item 1", + "Item 2" + ]) + { + Body = new Sequence + { + Activities = + [ + readCurrentValue, + new WriteLine(context => + { + var currentValue = context.GetVariable("CurrentValue"); + var activityResult = context.GetActivityExecutionContext().GetResult(readCurrentValue); + return $"Current value: {currentValue}, Activity result: {activityResult}"; + }) + ] + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs index ce1d1bde4..5b7c0da30 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs @@ -24,8 +24,20 @@ public class Tests var lines = _capturingTextWriter.Lines.ToList(); Assert.Equal(new[] { - "The result of 4 and 6 is 10.", - "The last result is 10." + "The result of 4 and 6 is 10.", "The last result is 10." + }, lines); + } + + [Fact(DisplayName = "The last activity output is returned.")] + public async Task Test2() + { + await _services.PopulateRegistriesAsync(); + await _workflowRunner.RunAsync(); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] + { + "Current value: Item 1, Activity result: Item 1", + "Current value: Item 2, Activity result: Item 2" }, lines); } } \ No newline at end of file From 71c0a5396e2d4147dfc1b1cf824658111b998176 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 30 Jan 2025 23:29:46 +0100 Subject: [PATCH 142/166] Add "Item 3" to looping workflow test scenarios. Extended test data in `LoopingWorkflow` to include a third item, "Item 3". Updated assertions in `Tests.cs` to validate the new expected output for the additional item. --- .../Scenarios/ActivityOutputs/LoopingWorkflow.cs | 3 ++- .../Scenarios/ActivityOutputs/Tests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs index 877048a3f..a24f0e5d2 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/LoopingWorkflow.cs @@ -12,7 +12,8 @@ public class LoopingWorkflow : WorkflowBase builder.Root = new ForEach( [ "Item 1", - "Item 2" + "Item 2", + "Item 3" ]) { Body = new Sequence diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs index 5b7c0da30..285966ae7 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ActivityOutputs/Tests.cs @@ -37,7 +37,8 @@ public class Tests Assert.Equal(new[] { "Current value: Item 1, Activity result: Item 1", - "Current value: Item 2, Activity result: Item 2" + "Current value: Item 2, Activity result: Item 2", + "Current value: Item 3, Activity result: Item 3" }, lines); } } \ No newline at end of file From 26e2b1fb569a269e44050ba5c206d6bdf8d769e1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 31 Jan 2025 00:31:09 +0100 Subject: [PATCH 143/166] Add Oracle database support to the project This commit introduces Oracle database integration by adding necessary configurations, entity mappings, and Docker Compose setup. Oracle-specific mappings ensure compatibility with NCLOB for large data handling. The changes also include updates to appsettings, enum for SqlDatabaseProvider, and project references to support Oracle. --- docker/docker-compose.yml | 15 ++++++- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + .../Enums/SqlDatabaseProvider.cs | 1 + src/apps/Elsa.Server.Web/Program.cs | 14 ++++++- src/apps/Elsa.Server.Web/appsettings.json | 1 + .../SetupForOracle.cs | 4 +- .../ElsaDbContextBase.cs | 2 +- .../Modules/Alterations/SetupForOracle.cs | 9 ++-- .../Modules/Management/SetupForOracle.cs | 6 +-- .../Modules/Runtime/SetupForOracle.cs | 41 +++++++++++++------ 10 files changed, 69 insertions(+), 25 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5340c7c54..58f37a11b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -24,6 +24,19 @@ - "3306:3306" volumes: - mysql_data2:/var/lib/mysql + + oracle: + image: container-registry.oracle.com/database/free:latest + container_name: oracle + environment: + ORACLE_PDB: ORCLPDB1 + ORACLE_PWD: elsa + ports: + - "1521:1521" + - "5500:5500" + volumes: + - oracle-data-free:/opt/oracle/oradata + shm_size: '1g' mongodb: image: mongo:latest @@ -84,7 +97,6 @@ - ASPNETCORE_URLS=http://+:80 - Logging__LogLevel__Default=Information - elsa-server: build: context: ../. @@ -116,6 +128,7 @@ volumes: postgres-data: + oracle-data-free: mysql_data2: cockroachdb-data: mongodb_data: diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index badb50298..0c3c5f082 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -9,6 +9,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Enums/SqlDatabaseProvider.cs b/src/apps/Elsa.Server.Web/Enums/SqlDatabaseProvider.cs index cc2988042..651163385 100644 --- a/src/apps/Elsa.Server.Web/Enums/SqlDatabaseProvider.cs +++ b/src/apps/Elsa.Server.Web/Enums/SqlDatabaseProvider.cs @@ -6,5 +6,6 @@ public enum SqlDatabaseProvider Sqlite, MySql, PostgreSql, + Oracle, CockroachDb } \ No newline at end of file diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 199ff6cce..0e9985ad8 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -99,6 +99,7 @@ var identityTokenSection = identitySection.GetSection("Tokens"); var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!; var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!; var postgresConnectionString = configuration.GetConnectionString("PostgreSql")!; +var oracleConnectionString = configuration.GetConnectionString("Oracle")!; var mySqlConnectionString = configuration.GetConnectionString("MySql")!; var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb")!; var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!; @@ -195,6 +196,8 @@ services #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); + else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) + ef.UseOracle(oracleConnectionString); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -231,6 +234,8 @@ services #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); + else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) + ef.UseOracle(oracleConnectionString); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -276,7 +281,9 @@ services ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) - ef.UsePostgreSql(cockroachDbConnectionString!); + ef.UsePostgreSql(cockroachDbConnectionString); + else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) + ef.UseOracle(oracleConnectionString); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -411,7 +418,9 @@ services ef.UseMySql(mySqlConnectionString); #endif else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) - ef.UsePostgreSql(cockroachDbConnectionString!); + ef.UsePostgreSql(cockroachDbConnectionString); + else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) + ef.UseOracle(oracleConnectionString); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -614,6 +623,7 @@ services if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) ef.UseOracle(oracleConnectionString); #if !NET9_0 if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 221cdcf22..ae6820a3f 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -13,6 +13,7 @@ "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", "MySql": "Server=localhost;Database=elsa;Uid=admin;Pwd=password;", "PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", + "Oracle": "User Id=SYSTEM;Password=elsa;Data Source=oracle:1521/ORCLPDB1", "CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", "MongoDb": "mongodb://localhost:27017/elsa-workflows", "AzureServiceBus": "", diff --git a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs index 2f5444792..1dec01e7d 100644 --- a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs +++ b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs @@ -19,7 +19,7 @@ public class SetupForOracle : IEntityModelCreatingHandler // In order to use data more than 2000 char we have to use NCLOB. // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("SerializedSettings").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedAgentConfig").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedSettings").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedAgentConfig").HasColumnType("NCLOB"); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index eef02af6e..2a677bb6f 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -69,7 +69,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema var entityTypeHandlers = ServiceProvider.GetServices().ToList(); - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToList()) { foreach (var handler in entityTypeHandlers) { diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs index a9b7703bc..8c3b5b2a3 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs @@ -18,8 +18,11 @@ public class SetupForOracle : IEntityModelCreatingHandler // In order to use data more than 2000 char we have to use NCLOB. // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("SerializedAlterations").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedWorkflowInstanceIds").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedLog").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.Alterations); + modelBuilder.Entity().Ignore(x => x.WorkflowInstanceFilter); + modelBuilder.Entity().Property("SerializedAlterations").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedWorkflowInstanceFilter").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.Log); + modelBuilder.Entity().Property("SerializedLog").HasColumnType("NCLOB"); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs index c14e6f4e1..da338d428 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs @@ -18,8 +18,8 @@ public class SetupForOracle : IEntityModelCreatingHandler // In order to use data more than 2000 char we have to use NCLOB. // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("StringData").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("StringData").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs index 2a4351a38..9820dfbab 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs @@ -19,22 +19,37 @@ public class SetupForOracle : IEntityModelCreatingHandler // To use data more than 2000 char we have to use NCLOB. // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.ActivityState); + modelBuilder.Entity().Ignore(x => x.Payload); + modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedException").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedOutputs").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.ActivityState); + modelBuilder.Entity().Ignore(x => x.Exception); + modelBuilder.Entity().Ignore(x => x.Payload); + modelBuilder.Entity().Ignore(x => x.Outputs); + modelBuilder.Entity().Ignore(x => x.Properties); + modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedException").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedOutputs").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedProperties").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedMetadata").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.Payload); + modelBuilder.Entity().Ignore(x => x.Metadata); + modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedMetadata").HasColumnType("NCLOB"); + + modelBuilder.Entity().Ignore(x => x.Payload); + modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedInput").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedBookmarkPayload").HasColumnType("NCLOB"); - - modelBuilder.Entity().Property("SerializedValue").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.Input); + modelBuilder.Entity().Ignore(x => x.BookmarkPayload); + modelBuilder.Entity().Property("SerializedInput").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("SerializedBookmarkPayload").HasColumnType("NCLOB"); + + modelBuilder.Entity().Property("SerializedValue").HasColumnType("NCLOB"); } } \ No newline at end of file From 006e0bbbb4afe0b6688474b2ca2ee6cd4ef6ac5e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 31 Jan 2025 00:52:49 +0100 Subject: [PATCH 144/166] Ignore EF Core pending model changes warnings and update Oracle DSN. Added configuration to suppress EF Core PendingModelChangesWarning for better compatibility with newer .NET versions. Updated Oracle connection string to use "localhost" for consistency and clarity in appsettings.json. --- src/apps/Elsa.Server.Web/appsettings.json | 2 +- .../ElsaDbContextBase.cs | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index ae6820a3f..20591e80b 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -13,7 +13,7 @@ "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", "MySql": "Server=localhost;Database=elsa;Uid=admin;Pwd=password;", "PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", - "Oracle": "User Id=SYSTEM;Password=elsa;Data Source=oracle:1521/ORCLPDB1", + "Oracle": "User Id=SYSTEM;Password=elsa;Data Source=localhost:1521/FREE;", "CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", "MongoDb": "mongodb://localhost:27017/elsa-workflows", "AzureServiceBus": "", diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index 2a677bb6f..85407dff4 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -2,6 +2,7 @@ using Elsa.Common.Entities; using Elsa.Common.Multitenancy; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.DependencyInjection; namespace Elsa.EntityFrameworkCore; @@ -16,7 +17,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema EntityState.Added, EntityState.Modified, }; - + protected readonly IServiceProvider ServiceProvider; public string? TenantId { get; set; } @@ -43,14 +44,14 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema // ReSharper disable once VirtualMemberCallInConstructor Schema = !string.IsNullOrWhiteSpace(elsaDbContextOptions?.SchemaName) ? elsaDbContextOptions.SchemaName : ElsaSchema; - + var tenantAccessor = serviceProvider.GetService(); var tenantId = tenantAccessor?.Tenant?.Id; - - if(!string.IsNullOrWhiteSpace(tenantId)) + + if (!string.IsNullOrWhiteSpace(tenantId)) TenantId = tenantId; } - + /// public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { @@ -58,6 +59,13 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema return await base.SaveChangesAsync(cancellationToken); } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { +#if NET9_0_OR_GREATER + optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); +#endif + } + /// protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -66,7 +74,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema if (!Database.IsSqlite()) modelBuilder.HasDefaultSchema(Schema); } - + var entityTypeHandlers = ServiceProvider.GetServices().ToList(); foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToList()) @@ -77,7 +85,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema } } } - + private async Task OnBeforeSavingAsync(CancellationToken cancellationToken) { var handlers = ServiceProvider.GetServices().ToList(); From 6c0d1ea66c34f482252851d4cc8b80181f0148f1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 31 Jan 2025 19:51:24 +0100 Subject: [PATCH 145/166] Refactor database setup and EFCore provider configurations. Revised database initialization scripts to better handle Postgres and Oracle environments, introduced schema-specific configurations for Oracle EFCore, and cleaned up obsolete or redundant entity model setup. Streamlined project structure by relocating and renaming files for SQLite and Oracle EFCore setups, improving maintainability and readability. --- Elsa.sln | 8 +++- docker/docker-compose.yml | 9 ++-- docker/{init-db.sh => init-db-postgres.sh} | 0 docker/oracle-setup/setup.sql | 3 ++ src/apps/Elsa.Server.Web/Program.cs | 11 ++--- src/apps/Elsa.Server.Web/appsettings.json | 2 +- .../Feature.cs | 1 - .../SetupForOracle.cs | 25 ----------- .../CommonPersistenceFeature.cs | 1 - .../DbSchemaAwareMigrationAssembly.cs | 17 ++++---- .../Elsa.EntityFrameworkCore.Common.csproj | 1 - .../ElsaDbContextBase.cs | 4 +- .../PersistenceFeatureBase.cs | 9 ++-- .../Alterations/20241212211620_V3_3.cs | 2 +- .../Identity/20241212211936_V3_3.cs | 2 +- .../Migrations/Labels/20241212212100_V3_3.cs | 2 +- .../Management/20241212211817_V3_3.cs | 2 +- .../Migrations/Runtime/20250116193207_V3_3.cs | 2 +- .../Migrations/Tenants/20241212212227_V3_3.cs | 2 +- .../OracleProvidersExtensions.cs | 41 +++++++++++-------- .../SetupForAlterations.cs} | 5 +-- .../SetupForManagement.cs | 39 ++++++++++++++++++ .../SetupForRuntime.cs} | 5 +-- .../SetupForSqlite.cs | 2 +- .../SqliteProvidersExtensions.cs | 6 ++- .../Modules/Alterations/Feature.cs | 1 - .../Modules/Management/SetupForOracle.cs | 25 ----------- .../WorkflowDefinitionPersistenceFeature.cs | 1 + .../WorkflowInstancePersistenceFeature.cs | 2 +- .../WorkflowManagementPersistenceFeature.cs | 28 +++++-------- .../WorkflowRuntimePersistenceFeature.cs | 1 - 31 files changed, 127 insertions(+), 132 deletions(-) rename docker/{init-db.sh => init-db-postgres.sh} (100%) create mode 100755 docker/oracle-setup/setup.sql delete mode 100644 src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs rename src/modules/{Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs => Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs} (88%) create mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs rename src/modules/{Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs => Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs} (95%) rename src/modules/{Elsa.EntityFrameworkCore.Common/EntityHandlers => Elsa.EntityFrameworkCore.Sqlite}/SetupForSqlite.cs (95%) delete mode 100644 src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs diff --git a/Elsa.sln b/Elsa.sln index 619e260be..0091ff928 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -89,9 +89,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 docker\ElsaServer.Dockerfile = docker\ElsaServer.Dockerfile docker\ElsaServerAndStudio.Dockerfile = docker\ElsaServerAndStudio.Dockerfile docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile - docker\init-db.sh = docker\init-db.sh docker\otel-collector-config.yaml = docker\otel-collector-config.yaml docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml + docker\init-db-postgres.sh = docker\init-db-postgres.sh EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Elasticsearch", "src\modules\Elsa.Elasticsearch\Elsa.Elasticsearch.csproj", "{3246883E-2FA7-4B4A-BDC5-99039A2869BC}" @@ -387,6 +387,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.Ent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Kafka", "src\modules\Elsa.Kafka\Elsa.Kafka.csproj", "{BF934627-F531-44FB-BEC2-ECA801FF31E7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "oracle-setup", "oracle-setup", "{66E2E2CF-967F-4564-89E8-F46FA973C99B}" + ProjectSection(SolutionItems) = preProject + docker\oracle-setup\setup.sql = docker\oracle-setup\setup.sql + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -968,6 +973,7 @@ Global {2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37} {2CDF3E1C-267D-4198-B1C7-7E1F548FC120} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {BF934627-F531-44FB-BEC2-ECA801FF31E7} = {DD089B8B-DA73-492A-9010-F772D1C178DA} + {66E2E2CF-967F-4564-89E8-F46FA973C99B} = {986E5482-0482-448C-B9E4-EC67A9474B85} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 58f37a11b..8d76746a1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -27,16 +27,15 @@ oracle: image: container-registry.oracle.com/database/free:latest - container_name: oracle + container_name: oracle-db environment: - ORACLE_PDB: ORCLPDB1 ORACLE_PWD: elsa ports: - "1521:1521" - "5500:5500" volumes: - - oracle-data-free:/opt/oracle/oradata - shm_size: '1g' + - ./oracle-data-free1:/opt/oracle/oradata + - ./oracle-setup:/opt/oracle/scripts/setup mongodb: image: mongo:latest @@ -128,7 +127,7 @@ volumes: postgres-data: - oracle-data-free: + oracle-data-free1: mysql_data2: cockroachdb-data: mongodb_data: diff --git a/docker/init-db.sh b/docker/init-db-postgres.sh similarity index 100% rename from docker/init-db.sh rename to docker/init-db-postgres.sh diff --git a/docker/oracle-setup/setup.sql b/docker/oracle-setup/setup.sql new file mode 100755 index 000000000..311441d40 --- /dev/null +++ b/docker/oracle-setup/setup.sql @@ -0,0 +1,3 @@ +alter session set "_ORACLE_SCRIPT"=true; +CREATE USER ELSA IDENTIFIED BY elsa; +GRANT ALL PRIVILEGES TO ELSA; \ No newline at end of file diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 0e9985ad8..af30c1b66 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -9,6 +9,7 @@ using Elsa.Common.Serialization; using Elsa.Dapper.Extensions; using Elsa.Dapper.Services; using Elsa.DropIns.Extensions; +using Elsa.EntityFrameworkCore; using Elsa.EntityFrameworkCore.Extensions; using Elsa.EntityFrameworkCore.Modules.Alterations; using Elsa.EntityFrameworkCore.Modules.Identity; @@ -197,7 +198,7 @@ services else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) - ef.UseOracle(oracleConnectionString); + ef.UseOracle(oracleConnectionString, new ElsaDbContextOptions{ SchemaName = "ELSA"}); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -235,7 +236,7 @@ services else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString!); else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) - ef.UseOracle(oracleConnectionString); + ef.UseOracle(oracleConnectionString, new ElsaDbContextOptions{ SchemaName = "ELSA"}); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -283,7 +284,7 @@ services else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) - ef.UseOracle(oracleConnectionString); + ef.UseOracle(oracleConnectionString, new ElsaDbContextOptions{ SchemaName = "ELSA"}); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -420,7 +421,7 @@ services else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb) ef.UsePostgreSql(cockroachDbConnectionString); else if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) - ef.UseOracle(oracleConnectionString); + ef.UseOracle(oracleConnectionString, new ElsaDbContextOptions{ SchemaName = "ELSA"}); else ef.UseSqlite(sp => sp.GetSqliteConnectionString()); @@ -623,7 +624,7 @@ services if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) ef.UseSqlite(sqliteConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) ef.UseSqlServer(sqlServerConnectionString); if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) ef.UsePostgreSql(postgresConnectionString); - if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) ef.UseOracle(oracleConnectionString); + if (sqlDatabaseProvider == SqlDatabaseProvider.Oracle) ef.UseOracle(oracleConnectionString, new ElsaDbContextOptions{ SchemaName = "ELSA"}); #if !NET9_0 if (sqlDatabaseProvider == SqlDatabaseProvider.MySql) ef.UseMySql(mySqlConnectionString); diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 20591e80b..a1d35303b 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -13,7 +13,7 @@ "Sqlite": "Data Source=App_Data/elsa.sqlite.db;Cache=Shared;", "MySql": "Server=localhost;Database=elsa;Uid=admin;Pwd=password;", "PostgreSql": "Server=localhost;Username=elsa;Database=elsa;Port=5432;Password=elsa;SSLMode=Prefer;MaxPoolSize=2000;Timeout=60", - "Oracle": "User Id=SYSTEM;Password=elsa;Data Source=localhost:1521/FREE;", + "Oracle": "Data Source=(DESCRIPTION = (ADDRESS_LIST = (FAILOVER =ON) (LOAD_BALANCE = OFF) (ADDRESS = (PROTOCOL =TCP)(HOST=localhost)(PORT=1521))) (CONNECT_DATA = (SID= FREE) ));User Id=ELSA;Password=elsa;", "CockroachDb": "Host=localhost;Port=26257;Database=elsa;SslMode=Disable;Username=root;IncludeErrorDetail=true", "MongoDb": "mongodb://localhost:27017/elsa-workflows", "AzureServiceBus": "", diff --git a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/Feature.cs b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/Feature.cs index d5c99065e..67f4d0240 100644 --- a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/Feature.cs +++ b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/Feature.cs @@ -34,6 +34,5 @@ public class EFCoreAgentPersistenceFeature(IModule module) : PersistenceFeatureB AddEntityStore(); AddEntityStore(); AddEntityStore(); - Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs b/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs deleted file mode 100644 index 1dec01e7d..000000000 --- a/src/modules/Elsa.Agents.Persistence.EntityFrameworkCore/SetupForOracle.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Elsa.Agents.Persistence.Entities; -using Elsa.EntityFrameworkCore; -using Elsa.EntityFrameworkCore.Extensions; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Elsa.Agents.Persistence.EntityFrameworkCore; - -/// -/// Represents a class that handles entity model creation for SQLite databases. -/// -public class SetupForOracle : IEntityModelCreatingHandler -{ - /// - public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) - { - if(!dbContext.Database.IsOracle()) - return; - - // In order to use data more than 2000 char we have to use NCLOB. - // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("SerializedSettings").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedAgentConfig").HasColumnType("NCLOB"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/CommonPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore.Common/CommonPersistenceFeature.cs index 42f681a5d..835574f21 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/CommonPersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/CommonPersistenceFeature.cs @@ -13,6 +13,5 @@ public class CommonPersistenceFeature(IModule module) : FeatureBase(module) { Services.AddScoped(); Services.AddScoped(); - Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/DbSchemaAwareMigrationAssembly.cs b/src/modules/Elsa.EntityFrameworkCore.Common/DbSchemaAwareMigrationAssembly.cs index 21ab52188..5e31b710e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/DbSchemaAwareMigrationAssembly.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/DbSchemaAwareMigrationAssembly.cs @@ -10,17 +10,14 @@ namespace Elsa.EntityFrameworkCore; /// /// Class That enable Schema change for Migration /// -public class DbSchemaAwareMigrationAssembly : MigrationsAssembly +public class DbSchemaAwareMigrationAssembly( + ICurrentDbContext currentContext, + IDbContextOptions options, + IMigrationsIdGenerator idGenerator, + IDiagnosticsLogger logger) + : MigrationsAssembly(currentContext, options, idGenerator, logger) { - private readonly DbContext _context; - - public DbSchemaAwareMigrationAssembly(ICurrentDbContext currentContext, - IDbContextOptions options, IMigrationsIdGenerator idGenerator, - IDiagnosticsLogger logger) - : base(currentContext, options, idGenerator, logger) - { - _context = currentContext.Context; - } + private readonly DbContext _context = currentContext.Context; public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider) { diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj index 723566266..e3f4635d9 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj +++ b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj @@ -12,7 +12,6 @@ - diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index 85407dff4..4e14923f2 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -61,6 +61,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { + optionsBuilder.EnableSensitiveDataLogging(); #if NET9_0_OR_GREATER optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)); #endif @@ -71,8 +72,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema { if (!string.IsNullOrWhiteSpace(Schema)) { - if (!Database.IsSqlite()) - modelBuilder.HasDefaultSchema(Schema); + modelBuilder.HasDefaultSchema(Schema); } var entityTypeHandlers = ServiceProvider.GetServices().ToList(); diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs index 031f69397..5df4f3a07 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/PersistenceFeatureBase.cs @@ -39,11 +39,7 @@ public abstract class PersistenceFeatureBase : FeatureBase /// /// Gets or sets the callback used to configure the . /// - public Action DbContextOptionsBuilder = (_, options) => options - .UseElsaDbContextOptions(default) - .UseSqlite("Data Source=elsa.sqlite.db;Cache=Shared;", sqlite => sqlite - .MigrationsAssembly("Elsa.EntityFrameworkCore.Sqlite") - .MigrationsHistoryTable(ElsaDbContextBase.MigrationsHistoryTable, ElsaDbContextBase.ElsaSchema)); + public virtual Action DbContextOptionsBuilder { get; set; } = null!; public override void ConfigureHostedServices() { @@ -54,6 +50,9 @@ public abstract class PersistenceFeatureBase : FeatureBase /// public override void Apply() { + if(DbContextOptionsBuilder == null) + throw new InvalidOperationException("The DbContextOptionsBuilder must be configured."); + if (UseContextPooling) Services.AddPooledDbContextFactory(DbContextOptionsBuilder); else diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs index 97bd8dd6c..fb7daa451 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Alterations protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "AlterationJobs", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs index baed52c99..f9c5d3188 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs @@ -19,7 +19,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "Applications", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs index dbfb65ccd..af7faf037 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs @@ -19,7 +19,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "Labels", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs index 57217614d..e3ba33c57 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "WorkflowDefinitions", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs index a026586c5..133e73074 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "ActivityExecutionRecords", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs index 8ca9f1711..1860609ac 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs @@ -19,7 +19,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "Elsa"); + name: _schema.Schema); migrationBuilder.CreateTable( name: "Tenants", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs index 002b87541..191a08eb2 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs @@ -1,5 +1,10 @@ using System.Reflection; +using Elsa.EntityFrameworkCore.Modules.Alterations; +using Elsa.EntityFrameworkCore.Oracle; +using Elsa.Extensions; using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Oracle.EntityFrameworkCore.Infrastructure; // ReSharper disable once CheckNamespace @@ -12,49 +17,53 @@ namespace Elsa.EntityFrameworkCore.Extensions; public static class OracleProvidersExtensions { private static Assembly Assembly => typeof(OracleProvidersExtensions).Assembly; - - public static TFeature UseOracle(this PersistenceFeatureBase feature, - string connectionString, - ElsaDbContextOptions? options = null, - Action? configure = null) + + public static TFeature UseOracle(this PersistenceFeatureBase feature, + string connectionString, + ElsaDbContextOptions? options = null, + Action? configure = null) where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { return feature.UseOracle(Assembly, connectionString, options, configure); } - - public static TFeature UseOracle(this PersistenceFeatureBase feature, + + public static TFeature UseOracle(this PersistenceFeatureBase feature, Func connectionStringFunc, ElsaDbContextOptions? options = null, Action? configure = null - ) + ) where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { return feature.UseOracle(Assembly, connectionStringFunc, options, configure); } - - public static TFeature UseOracle(this PersistenceFeatureBase feature, - Assembly migrationsAssembly, + + public static TFeature UseOracle(this PersistenceFeatureBase feature, + Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options = null, Action? configure = null - ) + ) where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { return feature.UseOracle(migrationsAssembly, _ => connectionString, options, configure); } - - public static TFeature UseOracle(this PersistenceFeatureBase feature, - Assembly migrationsAssembly, + + public static TFeature UseOracle(this PersistenceFeatureBase feature, + Assembly migrationsAssembly, Func connectionStringFunc, ElsaDbContextOptions? options = null, Action? configure = null - ) + ) where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { + feature.Services.TryAddScopedImplementation(); + feature.Services.TryAddScopedImplementation(); + feature.Services.TryAddScopedImplementation(); + feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaOracle(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs similarity index 88% rename from src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs index 8c3b5b2a3..81dc4bff3 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/SetupForOracle.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs @@ -1,14 +1,13 @@ using Elsa.Alterations.Core.Entities; -using Elsa.EntityFrameworkCore.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace Elsa.EntityFrameworkCore.Modules.Alterations; +namespace Elsa.EntityFrameworkCore.Oracle; /// /// Represents a class that handles entity model creation for SQLite databases. /// -public class SetupForOracle : IEntityModelCreatingHandler +public class SetupForAlterations : IEntityModelCreatingHandler { /// public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs new file mode 100644 index 000000000..c7ba3e7bb --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs @@ -0,0 +1,39 @@ +using System.Linq.Expressions; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Elsa.EntityFrameworkCore.Oracle; + +/// +/// Represents a class that handles entity model creation for SQLite databases. +/// +public class SetupForManagement : IEntityModelCreatingHandler +{ + private static Expression> VersionToStringConverter => v => v != null ? v.ToString() : null; + private static Expression> StringToVersionConverter => v => v != null ? Version.Parse(v) : null; + + /// + public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) + { + if(!dbContext.Database.IsOracle()) + return; + + // In order to use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); + modelBuilder.Entity().Ignore(x => x.WorkflowState); + modelBuilder.Entity().Ignore(x => x.CustomProperties); + modelBuilder.Entity().Ignore(x => x.Variables); + modelBuilder.Entity().Ignore(x => x.Inputs); + modelBuilder.Entity().Ignore(x => x.Outputs); + modelBuilder.Entity().Ignore(x => x.Outcomes); + modelBuilder.Entity().Ignore(x => x.Options); + modelBuilder.Entity().Property(x => x.ToolVersion).HasConversion(VersionToStringConverter, StringToVersionConverter); + modelBuilder.Entity().Property("StringData").HasColumnType("NCLOB"); + modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); + modelBuilder.Entity().Property(x => x.Description).HasColumnType("NCLOB"); + modelBuilder.Entity().Property(x => x.MaterializerContext).HasColumnType("NCLOB"); + modelBuilder.Entity().Property(x => x.BinaryData).HasColumnType("BLOB"); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs similarity index 95% rename from src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs index 9820dfbab..1ccab2951 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/SetupForOracle.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs @@ -1,15 +1,14 @@ -using Elsa.EntityFrameworkCore.Extensions; using Elsa.KeyValues.Entities; using Elsa.Workflows.Runtime.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace Elsa.EntityFrameworkCore.Modules.Runtime; +namespace Elsa.EntityFrameworkCore.Oracle; /// /// Represents a class that handles entity model creation for SQLite databases. /// -public class SetupForOracle : IEntityModelCreatingHandler +public class SetupForRuntime : IEntityModelCreatingHandler { /// public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetupForSqlite.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/SetupForSqlite.cs similarity index 95% rename from src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetupForSqlite.cs rename to src/modules/Elsa.EntityFrameworkCore.Sqlite/SetupForSqlite.cs index 500c6f176..8d60ac962 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/EntityHandlers/SetupForSqlite.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/SetupForSqlite.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Elsa.EntityFrameworkCore.EntityHandlers; +namespace Elsa.EntityFrameworkCore.Sqlite; /// /// Represents a class that handles entity model creation for SQLite databases. diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs b/src/modules/Elsa.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs index 635abfac5..561b35428 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs @@ -1,7 +1,10 @@ using System.Reflection; using Elsa.EntityFrameworkCore.EntityHandlers; +using Elsa.EntityFrameworkCore.Sqlite; +using Elsa.Extensions; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; // ReSharper disable once CheckNamespace namespace Elsa.EntityFrameworkCore.Extensions; @@ -65,7 +68,8 @@ public static class SqliteProvidersExtensions where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { - feature.Module.Services.AddScoped(); + + feature.Module.Services.TryAddScopedImplementation(); feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaSqlite(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs index f05d8442d..de79b7815 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Alterations/Feature.cs @@ -28,6 +28,5 @@ public class EFCoreAlterationsPersistenceFeature(IModule module) : PersistenceFe base.Apply(); AddEntityStore(); AddEntityStore(); - Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs deleted file mode 100644 index da338d428..000000000 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/SetupForOracle.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Elsa.EntityFrameworkCore.Extensions; -using Elsa.Workflows.Management.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Elsa.EntityFrameworkCore.Modules.Management; - -/// -/// Represents a class that handles entity model creation for SQLite databases. -/// -public class SetupForOracle : IEntityModelCreatingHandler -{ - /// - public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) - { - if(!dbContext.Database.IsOracle()) - return; - - // In order to use data more than 2000 char we have to use NCLOB. - // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("StringData").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs index b84f3ef23..3b808a942 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs @@ -10,6 +10,7 @@ namespace Elsa.EntityFrameworkCore.Modules.Management; /// Configures the feature with an Entity Framework Core persistence provider. /// [DependsOn(typeof(WorkflowManagementFeature))] +[DependsOn(typeof(WorkflowDefinitionsFeature))] public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { /// diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs index 8858c9757..de019e808 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowInstancePersistenceFeature.cs @@ -10,6 +10,7 @@ namespace Elsa.EntityFrameworkCore.Modules.Management; /// Configures the feature with an Entity Framework Core persistence provider. /// [DependsOn(typeof(WorkflowManagementFeature))] +[DependsOn(typeof(WorkflowInstancesFeature))] public class EFCoreWorkflowInstancePersistenceFeature(IModule module) : PersistenceFeatureBase(module) { /// @@ -23,6 +24,5 @@ public class EFCoreWorkflowInstancePersistenceFeature(IModule module) : Persiste { base.Apply(); AddEntityStore(); - Services.AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs index 5699442c6..09e9e20b2 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowManagementPersistenceFeature.cs @@ -1,33 +1,27 @@ using Elsa.Features.Attributes; using Elsa.Features.Services; -using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Management.Features; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; namespace Elsa.EntityFrameworkCore.Modules.Management; /// /// Configures the and features with an Entity Framework Core persistence provider. /// -[DependsOn(typeof(WorkflowManagementFeature))] -[DependsOn(typeof(WorkflowInstancesFeature))] -[DependsOn(typeof(WorkflowDefinitionsFeature))] +[DependsOn(typeof(EFCoreWorkflowInstancePersistenceFeature))] +[DependsOn(typeof(EFCoreWorkflowDefinitionPersistenceFeature))] [PublicAPI] public class WorkflowManagementPersistenceFeature(IModule module) : PersistenceFeatureBase(module) { - /// - public override void Configure() + public override Action DbContextOptionsBuilder { - Module.Configure(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService()); - Module.Configure(feature => feature.WorkflowDefinitionStore = sp => sp.GetRequiredService()); - } - - /// - public override void Apply() - { - base.Apply(); - AddEntityStore(); - AddEntityStore(); + get => base.DbContextOptionsBuilder; + set + { + base.DbContextOptionsBuilder = value; + Module.Configure(x => x.DbContextOptionsBuilder = value); + Module.Configure(x => x.DbContextOptionsBuilder = value); + } } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs index f7b5a00be..a3ab463db 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowRuntimePersistenceFeature.cs @@ -41,6 +41,5 @@ public class EFCoreWorkflowRuntimePersistenceFeature(IModule module) : Persisten AddEntityStore(); AddEntityStore(); AddStore(); - Services.AddScoped(); } } \ No newline at end of file From 2bb3af1e5dce91ca977cc7d082629aa67c0d236c Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 1 Feb 2025 00:38:38 +0100 Subject: [PATCH 146/166] Fix Oracle migrations --- .../Extensions/ServiceCollectionExtensions.cs | 37 +++++++++++ .../ElsaDbContextBase.cs | 21 ++++--- .../ElsaDbContextOptions.cs | 27 ++++++++ .../RunMigrationsStartupTask.cs | 4 +- .../Configurations/Management.cs | 26 ++++++++ .../Configurations/Runtime.cs | 63 +++++++++++++++++++ .../DbContextFactories.cs | 3 +- .../DbContextOptionsBuilder.cs | 2 +- .../Elsa.EntityFrameworkCore.Oracle.csproj | 7 +++ ...ner.cs => 20250131233442_V3_3.Designer.cs} | 4 +- ...2211620_V3_3.cs => 20250131233442_V3_3.cs} | 0 .../AlterationsElsaDbContextModelSnapshot.cs | 2 +- ...ner.cs => 20250131233455_V3_3.Designer.cs} | 4 +- ...2211936_V3_3.cs => 20250131233455_V3_3.cs} | 2 +- .../IdentityElsaDbContextModelSnapshot.cs | 2 +- ...ner.cs => 20250131233459_V3_3.Designer.cs} | 4 +- ...2212100_V3_3.cs => 20250131233459_V3_3.cs} | 2 +- .../LabelsElsaDbContextModelSnapshot.cs | 2 +- ...ner.cs => 20250131233451_V3_3.Designer.cs} | 28 ++++----- ...2211817_V3_3.cs => 20250131233451_V3_3.cs} | 24 +++---- .../ManagementElsaDbContextModelSnapshot.cs | 26 ++++---- ...ner.cs => 20250131233446_V3_3.Designer.cs} | 28 ++++----- ...6193207_V3_3.cs => 20250131233446_V3_3.cs} | 24 +++---- .../RuntimeElsaDbContextModelSnapshot.cs | 26 ++++---- ...ner.cs => 20250131233503_V3_3.Designer.cs} | 4 +- ...2212227_V3_3.cs => 20250131233503_V3_3.cs} | 0 .../TenantsElsaDbContextModelSnapshot.cs | 2 +- .../OracleProvidersExtensions.cs | 35 ++++++++--- .../SetupForAlterations.cs | 27 -------- .../SetupForManagement.cs | 39 ------------ .../SetupForRuntime.cs | 54 ---------------- .../Modules/Identity/DbContext.cs | 4 +- .../Modules/Management/DbContext.cs | 3 +- .../Modules/Runtime/DbContext.cs | 4 +- .../Modules/Tenants/DbContext.cs | 2 +- .../Entities/BookmarkQueueItem.cs | 2 +- 36 files changed, 302 insertions(+), 242 deletions(-) create mode 100644 src/modules/Elsa.Common/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/Configurations/Management.cs create mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/Configurations/Runtime.cs rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/{20241212211620_V3_3.Designer.cs => 20250131233442_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/{20241212211620_V3_3.cs => 20250131233442_V3_3.cs} (100%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/{20241212211936_V3_3.Designer.cs => 20250131233455_V3_3.Designer.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/{20241212211936_V3_3.cs => 20250131233455_V3_3.cs} (99%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/{20241212212100_V3_3.Designer.cs => 20250131233459_V3_3.Designer.cs} (97%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/{20241212212100_V3_3.cs => 20250131233459_V3_3.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/{20241212211817_V3_3.Designer.cs => 20250131233451_V3_3.Designer.cs} (91%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/{20241212211817_V3_3.cs => 20250131233451_V3_3.cs} (90%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20250116193207_V3_3.Designer.cs => 20250131233446_V3_3.Designer.cs} (95%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/{20250116193207_V3_3.cs => 20250131233446_V3_3.cs} (98%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/{20241212212227_V3_3.Designer.cs => 20250131233503_V3_3.Designer.cs} (94%) rename src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/{20241212212227_V3_3.cs => 20250131233503_V3_3.cs} (100%) delete mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs delete mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs delete mode 100644 src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs diff --git a/src/modules/Elsa.Common/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Common/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..c5088a123 --- /dev/null +++ b/src/modules/Elsa.Common/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,37 @@ +namespace Elsa.Extensions; + +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds the service with a specific implementation type only if the combination + /// of service and implementation does not already exist in the service collection. + /// + public static IServiceCollection TryAddScopedImplementation( + this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + if (!services.Any(sd => sd.ServiceType == typeof(TService) && sd.ImplementationType == typeof(TImplementation))) + services.AddScoped(); + + return services; + } + + /// + /// Adds the service with a specific implementation factory only if the combination + /// of service and implementation already doesn't exist. + /// + public static IServiceCollection TryAddScopedImplementation( + this IServiceCollection services, Func implementationFactory) + where TService : class + { + if (services.All(sd => sd.ServiceType != typeof(TService))) + services.AddScoped(implementationFactory); + + return services; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs index 4e14923f2..5b9b7557b 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextBase.cs @@ -18,7 +18,8 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema EntityState.Modified, }; - protected readonly IServiceProvider ServiceProvider; + protected IServiceProvider ServiceProvider { get; } + private readonly ElsaDbContextOptions? _elsaDbContextOptions; public string? TenantId { get; set; } /// @@ -40,10 +41,10 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema protected ElsaDbContextBase(DbContextOptions options, IServiceProvider serviceProvider) : base(options) { ServiceProvider = serviceProvider; - var elsaDbContextOptions = options.FindExtension()?.Options; - + _elsaDbContextOptions = options.FindExtension()?.Options; + // ReSharper disable once VirtualMemberCallInConstructor - Schema = !string.IsNullOrWhiteSpace(elsaDbContextOptions?.SchemaName) ? elsaDbContextOptions.SchemaName : ElsaSchema; + Schema = !string.IsNullOrWhiteSpace(_elsaDbContextOptions?.SchemaName) ? _elsaDbContextOptions.SchemaName : ElsaSchema; var tenantAccessor = serviceProvider.GetService(); var tenantId = tenantAccessor?.Tenant?.Id; @@ -70,19 +71,19 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - if (!string.IsNullOrWhiteSpace(Schema)) - { + if (!string.IsNullOrWhiteSpace(Schema)) modelBuilder.HasDefaultSchema(Schema); - } + + var additionalConfigurations = _elsaDbContextOptions?.GetModelConfigurations(this); + + additionalConfigurations?.Invoke(modelBuilder); var entityTypeHandlers = ServiceProvider.GetServices().ToList(); foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToList()) { - foreach (var handler in entityTypeHandlers) - { + foreach (var handler in entityTypeHandlers) handler.Handle(this, modelBuilder, entityType); - } } } diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptions.cs b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptions.cs index ad185a396..cd6af7271 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/ElsaDbContextOptions.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; namespace Elsa.EntityFrameworkCore; @@ -22,4 +23,30 @@ public class ElsaDbContextOptions /// The assembly name containing the migrations. /// public string? MigrationsAssemblyName { get; set; } + + public IDictionary> ProviderSpecificConfigurations { get; set; } = new Dictionary>(); + + public void ConfigureModel(Action configure) where TDbContext : DbContext + { + ConfigureModel(typeof(TDbContext), configure); + } + + public void ConfigureModel(Type dbContextType, Action configure) + { + if (!ProviderSpecificConfigurations.TryGetValue(dbContextType, out var configurations)) + ProviderSpecificConfigurations[dbContextType] = configurations = _ => { }; + + configurations += configure; + ProviderSpecificConfigurations[dbContextType] = configurations; + } + + public Action GetModelConfigurations(DbContext dbContext) + { + return GetModelConfigurations(dbContext.GetType()); + } + + public Action GetModelConfigurations(Type dbContextType) + { + return ProviderSpecificConfigurations.TryGetValue(dbContextType, out var providerConfigurations) ? providerConfigurations : _ => { }; + } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsStartupTask.cs b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsStartupTask.cs index f55e3b647..45f9c4a5e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsStartupTask.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Common/RunMigrationsStartupTask.cs @@ -16,7 +16,7 @@ public class RunMigrationsStartupTask(IDbContextFactory /// , IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // In order to use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Property("StringData").HasColumnType("NCLOB"); + builder.Property("Data").HasColumnType("NCLOB"); + builder.Property(x => x.Description).HasColumnType("NCLOB"); + builder.Property(x => x.MaterializerContext).HasColumnType("NCLOB"); + builder.Property(x => x.BinaryData).HasColumnType("BLOB"); + } + + public void Configure(EntityTypeBuilder builder) + { + // In order to use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Property("Data").HasColumnType("NCLOB"); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Configurations/Runtime.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Configurations/Runtime.cs new file mode 100644 index 000000000..20eecc6f5 --- /dev/null +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Configurations/Runtime.cs @@ -0,0 +1,63 @@ +using Elsa.Workflows.Runtime.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Elsa.EntityFrameworkCore.Oracle.Configurations; + +public class Runtime : + IEntityTypeConfiguration, + IEntityTypeConfiguration, + IEntityTypeConfiguration, + IEntityTypeConfiguration, + IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + // To use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Property("SerializedActivityState").HasColumnType("NCLOB"); + builder.Property("SerializedException").HasColumnType("NCLOB"); + builder.Property("SerializedPayload").HasColumnType("NCLOB"); + builder.Property("SerializedOutputs").HasColumnType("NCLOB"); + builder.Property("SerializedProperties").HasColumnType("NCLOB"); + } + + /// + public void Configure(EntityTypeBuilder builder) + { + // To use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + // modelBuilder.Entity().Ignore(x => x.Payload); + // modelBuilder.Entity().Ignore(x => x.Metadata); + builder.Property("SerializedPayload").HasColumnType("NCLOB"); + builder.Property("SerializedMetadata").HasColumnType("NCLOB"); + } + + /// + public void Configure(EntityTypeBuilder builder) + { + // To use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Property("SerializedPayload").HasColumnType("NCLOB"); + } + + /// + public void Configure(EntityTypeBuilder builder) + { + // To use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Property("SerializedActivityState").HasColumnType("NCLOB"); + builder.Property("SerializedPayload").HasColumnType("NCLOB"); + } + + public void Configure(EntityTypeBuilder builder) + { + // To use data more than 2000 char we have to use NCLOB. + // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). + builder.Ignore(x => x.Input); + builder.Ignore(x => x.BookmarkPayload); + builder.Property("SerializedInput").HasColumnType("NCLOB"); + builder.Property("SerializedBookmarkPayload").HasColumnType("NCLOB"); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextFactories.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextFactories.cs index 6270c9d91..d2fa8f28f 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextFactories.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextFactories.cs @@ -35,6 +35,7 @@ public class OracleDesignTimeDbContextFactory : DesignTimeDbContextF { protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) { - builder.UseElsaOracle(GetType().Assembly, connectionString); + var options = new ElsaDbContextOptions().Configure(); + builder.UseElsaOracle(GetType().Assembly, connectionString, options); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextOptionsBuilder.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextOptionsBuilder.cs index 1aa7e716d..f35133910 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextOptionsBuilder.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/DbContextOptionsBuilder.cs @@ -13,7 +13,7 @@ public static class DbContextOptionsBuilderExtensions /// /// Configures Entity Framework Core with Oracle. /// - public static DbContextOptionsBuilder UseElsaOracle(this DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options = default, Action? configure = default) => + public static DbContextOptionsBuilder UseElsaOracle(this DbContextOptionsBuilder builder, Assembly migrationsAssembly, string connectionString, ElsaDbContextOptions? options = null, Action? configure = null) => builder .UseElsaDbContextOptions(options) .UseOracle(connectionString, db => diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj index c4ecf6716..662dab74a 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj @@ -20,5 +20,12 @@ + + + + + + + diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20250131233442_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20250131233442_V3_3.Designer.cs index 812a1ef52..c16d8aac2 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20250131233442_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Alterations { [DbContext(typeof(AlterationsElsaDbContext))] - [Migration("20241212211620_V3_3")] + [Migration("20250131233442_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Alterations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20250131233442_V3_3.cs similarity index 100% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20241212211620_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/20250131233442_V3_3.cs diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/AlterationsElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/AlterationsElsaDbContextModelSnapshot.cs index 7a7b4f739..617db1282 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/AlterationsElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Alterations/AlterationsElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Alterations #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.Designer.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.Designer.cs index 5948fc317..9b9a8c12d 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.Designer.cs @@ -11,7 +11,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity { [DbContext(typeof(IdentityElsaDbContext))] - [Migration("20241212211936_V3_3")] + [Migration("20250131233455_V3_3")] partial class V3_3 { /// @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.cs similarity index 99% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.cs index f9c5d3188..37d23e329 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20241212211936_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/20250131233455_V3_3.cs @@ -19,7 +19,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: _schema.Schema); + _schema.Schema); migrationBuilder.CreateTable( name: "Applications", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs index 6b6a40e5a..aabcce6cb 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Identity/IdentityElsaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Identity #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.Designer.cs similarity index 97% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.Designer.cs index d87cdc60c..8f95ac067 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.Designer.cs @@ -11,7 +11,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels { [DbContext(typeof(LabelsElsaDbContext))] - [Migration("20241212212100_V3_3")] + [Migration("20250131233459_V3_3")] partial class V3_3 { /// @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.cs index af7faf037..414471594 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20241212212100_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/20250131233459_V3_3.cs @@ -19,7 +19,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: _schema.Schema); + _schema.Schema); migrationBuilder.CreateTable( name: "Labels", diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/LabelsElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/LabelsElsaDbContextModelSnapshot.cs index 8ff391a49..6d050f7c2 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/LabelsElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Labels/LabelsElsaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Labels #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.Designer.cs similarity index 91% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.Designer.cs index 018b9a4d1..1e05309c4 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management { [DbContext(typeof(ManagementElsaDbContext))] - [Migration("20241212211817_V3_3")] + [Migration("20250131233451_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -32,35 +32,35 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(450)"); b.Property("BinaryData") - .HasColumnType("RAW(2000)"); + .HasColumnType("BLOB"); b.Property("CreatedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("DefinitionId") .IsRequired() .HasColumnType("NVARCHAR2(450)"); b.Property("Description") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("IsLatest") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsPublished") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsReadonly") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsSystem") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("MaterializerContext") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("MaterializerName") .IsRequired() @@ -73,7 +73,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(2000)"); b.Property("StringData") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -82,7 +82,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(2000)"); b.Property("UsableAsActivity") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("Version") .HasColumnType("NUMBER(10)"); @@ -129,7 +129,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("DataCompressionAlgorithm") .HasColumnType("NVARCHAR2(2000)"); @@ -149,7 +149,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NUMBER(10)"); b.Property("IsSystem") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("Name") .HasColumnType("NVARCHAR2(450)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.cs similarity index 90% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.cs index e3ba33c57..aa899af2c 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20241212211817_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/20250131233451_V3_3.cs @@ -30,22 +30,22 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management Id = table.Column(type: "NVARCHAR2(450)", nullable: false), DefinitionId = table.Column(type: "NVARCHAR2(450)", nullable: false), Name = table.Column(type: "NVARCHAR2(450)", nullable: true), - Description = table.Column(type: "NVARCHAR2(2000)", nullable: true), + Description = table.Column(type: "NCLOB", nullable: true), ToolVersion = table.Column(type: "NVARCHAR2(2000)", nullable: true), ProviderName = table.Column(type: "NVARCHAR2(2000)", nullable: true), MaterializerName = table.Column(type: "NVARCHAR2(2000)", nullable: false), - MaterializerContext = table.Column(type: "NVARCHAR2(2000)", nullable: true), - StringData = table.Column(type: "NVARCHAR2(2000)", nullable: true), - BinaryData = table.Column(type: "RAW(2000)", nullable: true), - IsReadonly = table.Column(type: "NUMBER(1)", nullable: false), - IsSystem = table.Column(type: "NUMBER(1)", nullable: false), - Data = table.Column(type: "NVARCHAR2(2000)", nullable: true), - UsableAsActivity = table.Column(type: "NUMBER(1)", nullable: true), + MaterializerContext = table.Column(type: "NCLOB", nullable: true), + StringData = table.Column(type: "NCLOB", nullable: true), + BinaryData = table.Column(type: "BLOB", nullable: true), + IsReadonly = table.Column(type: "BOOLEAN", nullable: false), + IsSystem = table.Column(type: "BOOLEAN", nullable: false), + Data = table.Column(type: "NCLOB", nullable: true), + UsableAsActivity = table.Column(type: "BOOLEAN", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true), CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), Version = table.Column(type: "NUMBER(10)", nullable: false), - IsLatest = table.Column(type: "NUMBER(1)", nullable: false), - IsPublished = table.Column(type: "NUMBER(1)", nullable: false) + IsLatest = table.Column(type: "BOOLEAN", nullable: false), + IsPublished = table.Column(type: "BOOLEAN", nullable: false) }, constraints: table => { @@ -67,11 +67,11 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management CorrelationId = table.Column(type: "NVARCHAR2(450)", nullable: true), Name = table.Column(type: "NVARCHAR2(450)", nullable: true), IncidentCount = table.Column(type: "NUMBER(10)", nullable: false), - IsSystem = table.Column(type: "NUMBER(1)", nullable: false), + IsSystem = table.Column(type: "BOOLEAN", nullable: false), CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), UpdatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), FinishedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true), - Data = table.Column(type: "NVARCHAR2(2000)", nullable: true), + Data = table.Column(type: "NCLOB", nullable: true), DataCompressionAlgorithm = table.Column(type: "NVARCHAR2(2000)", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs index d28aebf43..325fa4f68 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -29,35 +29,35 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(450)"); b.Property("BinaryData") - .HasColumnType("RAW(2000)"); + .HasColumnType("BLOB"); b.Property("CreatedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("DefinitionId") .IsRequired() .HasColumnType("NVARCHAR2(450)"); b.Property("Description") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("IsLatest") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsPublished") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsReadonly") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("IsSystem") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("MaterializerContext") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("MaterializerName") .IsRequired() @@ -70,7 +70,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(2000)"); b.Property("StringData") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -79,7 +79,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NVARCHAR2(2000)"); b.Property("UsableAsActivity") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("Version") .HasColumnType("NUMBER(10)"); @@ -126,7 +126,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("DataCompressionAlgorithm") .HasColumnType("NVARCHAR2(2000)"); @@ -146,7 +146,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Management .HasColumnType("NUMBER(10)"); b.Property("IsSystem") - .HasColumnType("NUMBER(1)"); + .HasColumnType("BOOLEAN"); b.Property("Name") .HasColumnType("NVARCHAR2(450)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.Designer.cs similarity index 95% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.Designer.cs index 3abc3e068..d7fd0359e 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.Designer.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime { [DbContext(typeof(RuntimeElsaDbContext))] - [Migration("20250116193207_V3_3")] + [Migration("20250131233446_V3_3")] partial class V3_3 { /// @@ -21,7 +21,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -75,22 +75,22 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("BOOLEAN"); b.Property("SerializedActivityState") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedActivityStateCompressionAlgorithm") .HasColumnType("NVARCHAR2(2000)"); b.Property("SerializedException") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedOutputs") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedProperties") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("StartedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); @@ -223,10 +223,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedMetadata") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -273,7 +273,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -346,10 +346,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NUMBER(19)"); b.Property("SerializedActivityState") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("Source") .HasColumnType("NVARCHAR2(2000)"); @@ -457,10 +457,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedBookmarkPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedInput") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.cs similarity index 98% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.cs index 133e73074..c470348e0 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250116193207_V3_3.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/20250131233446_V3_3.cs @@ -38,12 +38,12 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime HasBookmarks = table.Column(type: "BOOLEAN", nullable: false), Status = table.Column(type: "NVARCHAR2(450)", nullable: false), CompletedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: true), - SerializedActivityState = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedActivityState = table.Column(type: "NCLOB", nullable: true), SerializedActivityStateCompressionAlgorithm = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedException = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedOutputs = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedPayload = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedProperties = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedException = table.Column(type: "NCLOB", nullable: true), + SerializedOutputs = table.Column(type: "NCLOB", nullable: true), + SerializedPayload = table.Column(type: "NCLOB", nullable: true), + SerializedProperties = table.Column(type: "NCLOB", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, constraints: table => @@ -84,8 +84,8 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime ActivityInstanceId = table.Column(type: "NVARCHAR2(450)", nullable: true), CorrelationId = table.Column(type: "NVARCHAR2(2000)", nullable: true), CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), - SerializedMetadata = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedPayload = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedMetadata = table.Column(type: "NCLOB", nullable: true), + SerializedPayload = table.Column(type: "NCLOB", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, constraints: table => @@ -118,7 +118,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime Name = table.Column(type: "NVARCHAR2(450)", nullable: false), ActivityId = table.Column(type: "NVARCHAR2(2000)", nullable: false), Hash = table.Column(type: "NVARCHAR2(450)", nullable: true), - SerializedPayload = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedPayload = table.Column(type: "NCLOB", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, constraints: table => @@ -148,8 +148,8 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime EventName = table.Column(type: "NVARCHAR2(450)", nullable: true), Message = table.Column(type: "NVARCHAR2(2000)", nullable: true), Source = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedActivityState = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedPayload = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedActivityState = table.Column(type: "NCLOB", nullable: true), + SerializedPayload = table.Column(type: "NCLOB", nullable: true), TenantId = table.Column(type: "NVARCHAR2(450)", nullable: true) }, constraints: table => @@ -170,8 +170,8 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime ActivityInstanceId = table.Column(type: "NVARCHAR2(450)", nullable: true), CreatedAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), ExpiresAt = table.Column(type: "TIMESTAMP(7) WITH TIME ZONE", nullable: false), - SerializedBookmarkPayload = table.Column(type: "NVARCHAR2(2000)", nullable: true), - SerializedInput = table.Column(type: "NVARCHAR2(2000)", nullable: true), + SerializedBookmarkPayload = table.Column(type: "NCLOB", nullable: true), + SerializedInput = table.Column(type: "NCLOB", nullable: true), TenantId = table.Column(type: "NVARCHAR2(2000)", nullable: true) }, constraints: table => diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 5e1520286..e0adad632 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -72,22 +72,22 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("BOOLEAN"); b.Property("SerializedActivityState") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedActivityStateCompressionAlgorithm") .HasColumnType("NVARCHAR2(2000)"); b.Property("SerializedException") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedOutputs") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedProperties") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("StartedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); @@ -220,10 +220,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedMetadata") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -270,7 +270,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -343,10 +343,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NUMBER(19)"); b.Property("SerializedActivityState") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("Source") .HasColumnType("NVARCHAR2(2000)"); @@ -454,10 +454,10 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime .HasColumnType("NVARCHAR2(450)"); b.Property("SerializedBookmarkPayload") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("SerializedInput") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("NCLOB"); b.Property("TenantId") .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.Designer.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20250131233503_V3_3.Designer.cs similarity index 94% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.Designer.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20250131233503_V3_3.Designer.cs index a02ded2ad..447cd1eac 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.Designer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20250131233503_V3_3.Designer.cs @@ -11,7 +11,7 @@ using Oracle.EntityFrameworkCore.Metadata; namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants { [DbContext(typeof(TenantsElsaDbContext))] - [Migration("20241212212227_V3_3")] + [Migration("20250131233503_V3_3")] partial class V3_3 { /// @@ -20,7 +20,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20250131233503_V3_3.cs similarity index 100% rename from src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20241212212227_V3_3.cs rename to src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/20250131233503_V3_3.cs diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/TenantsElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/TenantsElsaDbContextModelSnapshot.cs index 464fe8f3f..79041524c 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/TenantsElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Migrations/Tenants/TenantsElsaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Tenants #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("ProductVersion", "8.0.12") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs index 191a08eb2..8d800c577 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/OracleProvidersExtensions.cs @@ -1,10 +1,10 @@ using System.Reflection; -using Elsa.EntityFrameworkCore.Modules.Alterations; -using Elsa.EntityFrameworkCore.Oracle; -using Elsa.Extensions; +using Elsa.EntityFrameworkCore.Modules.Management; +using Elsa.EntityFrameworkCore.Modules.Runtime; +using Elsa.EntityFrameworkCore.Oracle.Configurations; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime.Entities; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Oracle.EntityFrameworkCore.Infrastructure; // ReSharper disable once CheckNamespace @@ -60,11 +60,28 @@ public static class OracleProvidersExtensions where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { - feature.Services.TryAddScopedImplementation(); - feature.Services.TryAddScopedImplementation(); - feature.Services.TryAddScopedImplementation(); - + options ??= new(); + options.Configure(); feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaOracle(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } + + public static ElsaDbContextOptions Configure(this ElsaDbContextOptions options) + { + var management = new Management(); + var runtime = new Runtime(); + + options.ConfigureModel(modelBuilder => modelBuilder + .ApplyConfiguration(management) + .ApplyConfiguration(management)); + + options.ConfigureModel(modelBuilder => modelBuilder + .ApplyConfiguration(runtime) + .ApplyConfiguration(runtime) + .ApplyConfiguration(runtime) + .ApplyConfiguration(runtime) + .ApplyConfiguration(runtime)); + + return options; + } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs deleted file mode 100644 index 81dc4bff3..000000000 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForAlterations.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Elsa.Alterations.Core.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Elsa.EntityFrameworkCore.Oracle; - -/// -/// Represents a class that handles entity model creation for SQLite databases. -/// -public class SetupForAlterations : IEntityModelCreatingHandler -{ - /// - public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) - { - if(!dbContext.Database.IsOracle()) - return; - - // In order to use data more than 2000 char we have to use NCLOB. - // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Ignore(x => x.Alterations); - modelBuilder.Entity().Ignore(x => x.WorkflowInstanceFilter); - modelBuilder.Entity().Property("SerializedAlterations").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedWorkflowInstanceFilter").HasColumnType("NCLOB"); - modelBuilder.Entity().Ignore(x => x.Log); - modelBuilder.Entity().Property("SerializedLog").HasColumnType("NCLOB"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs deleted file mode 100644 index c7ba3e7bb..000000000 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForManagement.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq.Expressions; -using Elsa.Workflows.Management.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Elsa.EntityFrameworkCore.Oracle; - -/// -/// Represents a class that handles entity model creation for SQLite databases. -/// -public class SetupForManagement : IEntityModelCreatingHandler -{ - private static Expression> VersionToStringConverter => v => v != null ? v.ToString() : null; - private static Expression> StringToVersionConverter => v => v != null ? Version.Parse(v) : null; - - /// - public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) - { - if(!dbContext.Database.IsOracle()) - return; - - // In order to use data more than 2000 char we have to use NCLOB. - // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); - modelBuilder.Entity().Ignore(x => x.WorkflowState); - modelBuilder.Entity().Ignore(x => x.CustomProperties); - modelBuilder.Entity().Ignore(x => x.Variables); - modelBuilder.Entity().Ignore(x => x.Inputs); - modelBuilder.Entity().Ignore(x => x.Outputs); - modelBuilder.Entity().Ignore(x => x.Outcomes); - modelBuilder.Entity().Ignore(x => x.Options); - modelBuilder.Entity().Property(x => x.ToolVersion).HasConversion(VersionToStringConverter, StringToVersionConverter); - modelBuilder.Entity().Property("StringData").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("Data").HasColumnType("NCLOB"); - modelBuilder.Entity().Property(x => x.Description).HasColumnType("NCLOB"); - modelBuilder.Entity().Property(x => x.MaterializerContext).HasColumnType("NCLOB"); - modelBuilder.Entity().Property(x => x.BinaryData).HasColumnType("BLOB"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs b/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs deleted file mode 100644 index 1ccab2951..000000000 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/SetupForRuntime.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Elsa.KeyValues.Entities; -using Elsa.Workflows.Runtime.Entities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace Elsa.EntityFrameworkCore.Oracle; - -/// -/// Represents a class that handles entity model creation for SQLite databases. -/// -public class SetupForRuntime : IEntityModelCreatingHandler -{ - /// - public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) - { - if (!dbContext.Database.IsOracle()) - return; - - // To use data more than 2000 char we have to use NCLOB. - // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - - modelBuilder.Entity().Ignore(x => x.ActivityState); - modelBuilder.Entity().Ignore(x => x.Payload); - modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - - modelBuilder.Entity().Ignore(x => x.ActivityState); - modelBuilder.Entity().Ignore(x => x.Exception); - modelBuilder.Entity().Ignore(x => x.Payload); - modelBuilder.Entity().Ignore(x => x.Outputs); - modelBuilder.Entity().Ignore(x => x.Properties); - modelBuilder.Entity().Property("SerializedActivityState").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedException").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedOutputs").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedProperties").HasColumnType("NCLOB"); - - modelBuilder.Entity().Ignore(x => x.Payload); - modelBuilder.Entity().Ignore(x => x.Metadata); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedMetadata").HasColumnType("NCLOB"); - - modelBuilder.Entity().Ignore(x => x.Payload); - modelBuilder.Entity().Property("SerializedPayload").HasColumnType("NCLOB"); - - modelBuilder.Entity().Ignore(x => x.Input); - modelBuilder.Entity().Ignore(x => x.BookmarkPayload); - modelBuilder.Entity().Property("SerializedInput").HasColumnType("NCLOB"); - modelBuilder.Entity().Property("SerializedBookmarkPayload").HasColumnType("NCLOB"); - - modelBuilder.Entity().Property("SerializedValue").HasColumnType("NCLOB"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/DbContext.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/DbContext.cs index c6a2ccf06..21e5a43fc 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/DbContext.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Identity/DbContext.cs @@ -31,11 +31,11 @@ public class IdentityElsaDbContext : ElsaDbContextBase /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(modelBuilder); - var config = new Configurations(); modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); + + base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs index f0ae46821..79ab138fb 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs @@ -28,12 +28,13 @@ public class ManagementElsaDbContext : ElsaDbContextBase /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(modelBuilder); modelBuilder.Ignore(); modelBuilder.Ignore(); var config = new Configurations(); modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); + + base.OnModelCreating(modelBuilder); } } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DbContext.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DbContext.cs index 8977b797f..c03030bf1 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DbContext.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/DbContext.cs @@ -57,8 +57,6 @@ public class RuntimeElsaDbContext : ElsaDbContextBase /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - base.OnModelCreating(modelBuilder); - var config = new Configurations(); modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); @@ -67,5 +65,7 @@ public class RuntimeElsaDbContext : ElsaDbContextBase modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); modelBuilder.ApplyConfiguration(config); + + base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Tenants/DbContext.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Tenants/DbContext.cs index 90d4dad28..aaa6b23ac 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Tenants/DbContext.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Tenants/DbContext.cs @@ -19,7 +19,7 @@ public class TenantsElsaDbContext : ElsaDbContextBase /// /// The alteration plans. /// - public DbSet Tenants { get; set; } = default!; + public DbSet Tenants { get; set; } = null!; /// protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/modules/Elsa.Workflows.Runtime/Entities/BookmarkQueueItem.cs b/src/modules/Elsa.Workflows.Runtime/Entities/BookmarkQueueItem.cs index a39d1feae..f7a867f73 100644 --- a/src/modules/Elsa.Workflows.Runtime/Entities/BookmarkQueueItem.cs +++ b/src/modules/Elsa.Workflows.Runtime/Entities/BookmarkQueueItem.cs @@ -54,7 +54,7 @@ public class BookmarkQueueItem : Entity /// public BookmarkFilter CreateBookmarkFilter() { - return new BookmarkFilter + return new() { WorkflowInstanceId = WorkflowInstanceId, CorrelationId = CorrelationId, From 69cdc41782e6eaf132bbf5d6eb5d7cdb5fd44b0f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 1 Feb 2025 00:49:19 +0100 Subject: [PATCH 147/166] Remove obsolete migration files from Oracle project. The migration files `20250131185451_V3_3_2` and related metadata were removed from the project configuration. This cleanup ensures the project references only necessary and relevant resources. --- .../Elsa.EntityFrameworkCore.Oracle.csproj | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj index 662dab74a..c4ecf6716 100644 --- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj +++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj @@ -20,12 +20,5 @@ - - - - - - - From cc39f122ebcaf9e220705b8e193512289696f2ad Mon Sep 17 00:00:00 2001 From: Matt Vance Date: Mon, 3 Feb 2025 16:14:42 -0800 Subject: [PATCH 148/166] Added Alterations API to Client API library and updated server API comments --- .../Alterations/Contracts/IAlteration.cs | 6 +++ .../Alterations/Contracts/IAlterationsApi.cs | 52 +++++++++++++++++++ .../Alterations/Enums/ActivityStatus.cs | 32 ++++++++++++ .../Alterations/Enums/AlterationJobStatus.cs | 27 ++++++++++ .../Alterations/Enums/AlterationPlanStatus.cs | 37 +++++++++++++ .../Alterations/Models/ActivityFilter.cs | 34 ++++++++++++ .../Alterations/Models/AlterationBase.cs | 8 +++ .../Alterations/Models/AlterationJob.cs | 45 ++++++++++++++++ .../Alterations/Models/AlterationLog.cs | 13 +++++ .../Alterations/Models/AlterationLogEntry.cs | 12 +++++ .../Alterations/Models/AlterationPlan.cs | 41 +++++++++++++++ .../Models/AlterationPlanParams.cs | 24 +++++++++ .../AlterationWorkflowInstanceFilter.cs | 45 ++++++++++++++++ .../Alterations/Models/CancelActivity.cs | 17 ++++++ .../Resources/Alterations/Models/Migrate.cs | 12 +++++ .../Alterations/Models/ModifyVariable.cs | 18 +++++++ .../Models/RunAlterationsResult.cs | 27 ++++++++++ .../Alterations/Models/ScheduleActivity.cs | 17 ++++++ .../Alterations/Requests/BulkRetryRequest.cs | 17 ++++++ .../Responses/BulkRetryResponse.cs | 14 +++++ .../Alterations/Responses/DryRunResponse.cs | 12 +++++ .../Responses/GetAlterationPlanResponse.cs | 19 +++++++ .../Alterations/Responses/RunRequest.cs | 19 +++++++ .../Alterations/Responses/RunResponse.cs | 14 +++++ .../Alterations/Responses/SubmitResponse.cs | 12 +++++ .../Models/AlterationPlanParams.cs | 2 +- .../Endpoints/Alterations/DryRun/Endpoint.cs | 2 +- .../Endpoints/Alterations/Get/Endpoint.cs | 2 +- .../Endpoints/Alterations/Run/Endpoint.cs | 2 +- .../Endpoints/Alterations/Submit/Endpoint.cs | 2 +- 30 files changed, 579 insertions(+), 5 deletions(-) create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlterationsApi.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Enums/ActivityStatus.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationJobStatus.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationPlanStatus.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/ActivityFilter.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationJob.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLog.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLogEntry.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationWorkflowInstanceFilter.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/RunAlterationsResult.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Requests/BulkRetryRequest.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/BulkRetryResponse.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/DryRunResponse.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/GetAlterationPlanResponse.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunResponse.cs create mode 100644 src/clients/Elsa.Api.Client/Resources/Alterations/Responses/SubmitResponse.cs diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs new file mode 100644 index 000000000..0ce1f7a50 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlteration.cs @@ -0,0 +1,6 @@ +namespace Elsa.Api.Client.Resources.Alterations.Contracts; + +/// +/// Marker interface for all alteration classes +/// +public interface IAlteration; \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlterationsApi.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlterationsApi.cs new file mode 100644 index 000000000..6e212ec36 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Contracts/IAlterationsApi.cs @@ -0,0 +1,52 @@ +using Elsa.Api.Client.Resources.Alterations.Models; +using Elsa.Api.Client.Resources.Alterations.Requests; +using Elsa.Api.Client.Resources.Alterations.Responses; +using Refit; + +namespace Elsa.Api.Client.Resources.Alterations.Contracts; + +/// +/// Represents a client for the alterations API. Requires the Elsa.Alterations feature. +/// +public interface IAlterationsApi +{ + /// + /// Returns an alteration plan and its associated jobs. + /// + /// The ID of the alteration plan to return. + /// The cancellation token. + [Get("/alterations/{id}")] + Task GetAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Determines which workflow instances a "Submit" request would target without actually running an alteration + /// + /// The requested workflow filter to dry run + /// The cancellation token. + [Post("/alterations/dry-run")] + Task DryRun(AlterationWorkflowInstanceFilter request, CancellationToken cancellationToken = default); + + /// + /// Submits an alteration plan and a filter for workflows instances to be executed against + /// + /// The alterations and filter to submit + /// The cancellation token. + [Post("/alterations/submit")] + Task Submit(AlterationPlanParams request, CancellationToken cancellationToken = default); + + /// + /// Runs an alteration plan and a list of workflow Instance Ids to be executed against + /// + /// The alterations and workflowInstanceIds to execute + /// The cancellation token. + [Post("/alterations/run")] + Task Run(RunRequest request, CancellationToken cancellationToken = default); + + /// + /// Retries the specified workflow instances. + /// + /// The request containing the selection of workflow instances to retry. + /// The cancellation token. + [Post("/alterations/workflows/retry")] + Task BulkRetryAsync(BulkRetryRequest request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/ActivityStatus.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/ActivityStatus.cs new file mode 100644 index 000000000..a4693e6ae --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/ActivityStatus.cs @@ -0,0 +1,32 @@ +namespace Elsa.Api.Client.Resources.Alterations.Enums; + +/// +/// Represents the status of an activity. +/// +public enum ActivityStatus +{ + /// + /// The activity is in the Pending state. + /// + Pending, + + /// + /// The activity is in the Running state. Note that event if an activity is running, it may not be executing. + /// + Running, + + /// + /// The activity is in the Completed state. + /// + Completed, + + /// + /// The activity is in the Canceled state. + /// + Canceled, + + /// + /// The activity is in the Faulted state. + /// + Faulted +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationJobStatus.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationJobStatus.cs new file mode 100644 index 000000000..49e65a99a --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationJobStatus.cs @@ -0,0 +1,27 @@ +namespace Elsa.Api.Client.Resources.Alterations.Enums; + +/// +/// The status of an alteration plan for a workflow instance. +/// +public enum AlterationJobStatus +{ + /// + /// The plan is pending execution. + /// + Pending, + + /// + /// The plan is currently being executed. + /// + Running, + + /// + /// The plan has been completed. + /// + Completed, + + /// + /// The job has failed. + /// + Failed +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationPlanStatus.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationPlanStatus.cs new file mode 100644 index 000000000..80ded4c2e --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Enums/AlterationPlanStatus.cs @@ -0,0 +1,37 @@ +namespace Elsa.Api.Client.Resources.Alterations.Enums; + +/// +/// The status of an alteration plan. +/// +public enum AlterationPlanStatus +{ + /// + /// The plan is pending execution. + /// + Pending, + + /// + /// The plan is currently generating jobs. + /// + Generating, + + /// + /// The plan is currently dispatching jobs. + /// + Dispatching, + + /// + /// The plan is currently being executed. + /// + Running, + + /// + /// The plan has been completed. + /// + Completed, + + /// + /// The plan has failed. + /// + Failed +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ActivityFilter.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ActivityFilter.cs new file mode 100644 index 000000000..37fdec5ee --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ActivityFilter.cs @@ -0,0 +1,34 @@ +using Elsa.Api.Client.Resources.Alterations.Enums; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// A filter for activities within a workflow instance +/// +public class ActivityFilter +{ + /// + /// The ID of the activity. + /// + public string? ActivityId { get; set; } + + /// + /// The ID of the activity instance. + /// + public string? ActivityInstanceId { get; set; } + + /// + /// The node ID of the activity. + /// + public string? NodeId { get; set; } + + /// + /// The name of the activity. + /// + public string? Name { get; set; } + + /// + /// The status of the activity. + /// + public ActivityStatus? Status { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs new file mode 100644 index 000000000..18615eca9 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationBase.cs @@ -0,0 +1,8 @@ +using Elsa.Api.Client.Resources.Alterations.Contracts; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// A base class for all IAlterations. +/// +public abstract class AlterationBase : IAlteration; \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationJob.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationJob.cs new file mode 100644 index 000000000..d756c23d7 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationJob.cs @@ -0,0 +1,45 @@ +using Elsa.Api.Client.Resources.Alterations.Enums; +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Represents the execution of the plan for an individual workflow instance. +/// +public class AlterationJob : Entity +{ + /// + /// The ID of the plan that this job belongs to. + /// + public string PlanId { get; set; } = default!; + + /// + /// The ID of the workflow instance that this job applies to. + /// + public string WorkflowInstanceId { get; set; } = default!; + + /// + /// The status of the job. + /// + public AlterationJobStatus Status { get; set; } + + /// + /// The serialized log of the job. + /// + public ICollection? Log { get; set; } = new List(); + + /// + /// The date and time at which the job was created. + /// + public DateTimeOffset CreatedAt { get; set; } + + /// + /// The date and time at which the job was started. + /// + public DateTimeOffset? StartedAt { get; set; } + + /// + /// The date and time at which the job was completed. + /// + public DateTimeOffset? CompletedAt { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLog.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLog.cs new file mode 100644 index 000000000..c39d44c12 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLog.cs @@ -0,0 +1,13 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Represents a log of alterations. +/// +public class AlterationLog +{ + + /// + /// The log entries. + /// + public ICollection LogEntries { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLogEntry.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLogEntry.cs new file mode 100644 index 000000000..9c661275b --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationLogEntry.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Logging; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// An individual log entry about an alteration +/// +/// +/// +/// +/// +public record AlterationLogEntry(string Message, LogLevel LogLevel, DateTimeOffset Timestamp, string? EventName = null); \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs new file mode 100644 index 000000000..993858cf5 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlan.cs @@ -0,0 +1,41 @@ +using Elsa.Api.Client.Resources.Alterations.Contracts; +using Elsa.Api.Client.Resources.Alterations.Enums; +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// A plan that contains a list of alterations to be applied to a set of workflow instances. +/// +public class AlterationPlan : Entity +{ + /// + /// The alterations to be applied. + /// + public ICollection Alterations { get; set; } = new List(); + + /// + /// The IDs of the workflow instances that this plan applies to. + /// + public AlterationWorkflowInstanceFilter WorkflowInstanceFilter { get; set; } = new(); + + /// + /// The status of the plan. + /// + public AlterationPlanStatus Status { get; set; } + + /// + /// The date and time at which the plan was created. + /// + public DateTimeOffset CreatedAt { get; set; } + + /// + /// The date and time at which the plan was started. + /// + public DateTimeOffset? StartedAt { get; set; } + + /// + /// The date and time at which the plan was completed. + /// + public DateTimeOffset? CompletedAt { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs new file mode 100644 index 000000000..f9fc5a1c1 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationPlanParams.cs @@ -0,0 +1,24 @@ +using Elsa.Api.Client.Resources.Alterations.Contracts; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Represents the execution of an alteration plan against a set of workflow instances defined by the given filter +/// +public class AlterationPlanParams +{ + /// + /// The unique identifier for the alteration plan. If not specified, a new ID will be generated. + /// + public string? Id { get; set; } + + /// + /// The alterations to be applied. + /// + public ICollection Alterations { get; set; } = new List(); + + /// + /// The IDs of the workflow instances that this plan applies to. + /// + public AlterationWorkflowInstanceFilter Filter { get; set; } = new(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationWorkflowInstanceFilter.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationWorkflowInstanceFilter.cs new file mode 100644 index 000000000..6f4419114 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/AlterationWorkflowInstanceFilter.cs @@ -0,0 +1,45 @@ +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Represents a filter for workflow instances. +/// +public class AlterationWorkflowInstanceFilter +{ + /// + /// The IDs of the workflow instances that this plan applies to. + /// + public IEnumerable? WorkflowInstanceIds { get; set; } + + /// + /// The correlation IDs of the workflow instances that this plan applies to. + /// + public IEnumerable? CorrelationIds { get; set; } + + /// + /// A collection of timestamp filters used for filtering data based on specified timestamp columns and operators. + /// + public IEnumerable? TimestampFilters { get; set; } + + /// + /// The IDs of the workflow definitions that this plan applies to. + /// + public IEnumerable? DefinitionVersionIds { get; set; } + + /// + /// Whether the workflow instances to match have incidents. + /// + public bool? HasIncidents { get; set; } + + /// + /// Whether the workflow instances to match are system workflows. Defaults to false. + /// + public bool? IsSystem { get; set; } = false; + + /// + /// Represents a collection of filters for activities. + /// + public IEnumerable? ActivityFilters { get; set; } + +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs new file mode 100644 index 000000000..2500e7d97 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/CancelActivity.cs @@ -0,0 +1,17 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Cancels a workflow instance activity during an alteration +/// +public class CancelActivity : AlterationBase +{ + /// + /// The ID of the activity to be cancelled. If not specified, the activity instance ID will be used. + /// + public string? ActivityId { get; set; } + + /// + /// The ID of the activity instance to be cancelled. If specified, overrides . + /// + public string? ActivityInstanceId { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs new file mode 100644 index 000000000..1a4dc3b69 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/Migrate.cs @@ -0,0 +1,12 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Migrates a workflow instance to a newer version in an alteration. +/// +public class Migrate : AlterationBase +{ + /// + /// The target version to upgrade to. + /// + public int TargetVersion { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs new file mode 100644 index 000000000..60ab9f6ba --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ModifyVariable.cs @@ -0,0 +1,18 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Modifies a variable in a workflow instance alteration +/// +public class ModifyVariable : AlterationBase +{ + /// + /// The ID of the variable to modify. + /// + public string VariableId { get; set; } = default!; + + /// + /// The new value of the variable. + /// + public object? Value { get; set; } + +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/RunAlterationsResult.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/RunAlterationsResult.cs new file mode 100644 index 000000000..756240be2 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/RunAlterationsResult.cs @@ -0,0 +1,27 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// The result of running a series of alterations. +/// +public class RunAlterationsResult +{ + /// + /// The ID of the workflow instance that was altered. + /// + public string WorkflowInstanceId { get; set; } = string.Empty; + + /// + /// A log of the alterations that were run. + /// + public AlterationLog Log { get; set; } = new(); + + /// + /// A flag indicating whether the workflow has scheduled work. + /// + public bool WorkflowHasScheduledWork { get; set; } + + /// + /// A flag indicating whether the alterations have succeeded. + /// + public bool IsSuccessful { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs new file mode 100644 index 000000000..2b0f77f6d --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Models/ScheduleActivity.cs @@ -0,0 +1,17 @@ +namespace Elsa.Api.Client.Resources.Alterations.Models; + +/// +/// Schedules an activity for execution in an alteration. +/// +public class ScheduleActivity : AlterationBase +{ + /// + /// The ID of the next activity to be scheduled. If not specified, the ActivityInstanceId will be used. + /// + public string? ActivityId { get; set; } + + /// + /// The ID of the activity instance to be scheduled. If not specified, the ActivityId will be used. + /// + public string? ActivityInstanceId { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Requests/BulkRetryRequest.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Requests/BulkRetryRequest.cs new file mode 100644 index 000000000..2fbc9fdef --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Requests/BulkRetryRequest.cs @@ -0,0 +1,17 @@ +namespace Elsa.Api.Client.Resources.Alterations.Requests; + +/// +/// Represents a request to bulk retry workflow instances. +/// +public class BulkRetryRequest +{ + /// + /// The IDs of the workflow instances that have incidents to be retried. + /// + public ICollection WorkflowInstanceIds { get; set; } = new List(); + + /// + /// An optional list of explicitly specified activity IDs to retry. If omitted, all faulted activities will be retried. + /// + public ICollection? ActivityIds { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/BulkRetryResponse.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/BulkRetryResponse.cs new file mode 100644 index 000000000..532e517c9 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/BulkRetryResponse.cs @@ -0,0 +1,14 @@ +using Elsa.Api.Client.Resources.Alterations.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// Represents a response to bulk retry workflow instances. +/// +public class BulkRetryResponse +{ + /// + /// The alterations that resulted from the bulk retry request + /// + public ICollection Results { get;set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/DryRunResponse.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/DryRunResponse.cs new file mode 100644 index 000000000..6eb0d2bea --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/DryRunResponse.cs @@ -0,0 +1,12 @@ +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// The response to the DryRun request +/// +public class DryRunResponse +{ + /// + /// The list of workflow instance IDs that would be affected by a "Submit" request + /// + public ICollection WorkflowInstanceIds { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/GetAlterationPlanResponse.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/GetAlterationPlanResponse.cs new file mode 100644 index 000000000..c3fc2fb51 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/GetAlterationPlanResponse.cs @@ -0,0 +1,19 @@ +using Elsa.Api.Client.Resources.Alterations.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// The response from the "Get" alteration plan endpoint +/// +public class GetAlterationPlanResponse +{ + /// + /// The alteration plan mathching the provided ID + /// + public AlterationPlan Plan { get; set; } = new(); + + /// + /// The list of jobs that exist for that AlterationPlan + /// + public ICollection Jobs { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs new file mode 100644 index 000000000..fdaf341e8 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunRequest.cs @@ -0,0 +1,19 @@ +using Elsa.Api.Client.Resources.Alterations.Contracts; + +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// A plan that contains a list of alterations to be applied to a set of workflow instances. +/// +public class RunRequest +{ + /// + /// The alterations to be applied. + /// + public ICollection Alterations { get; set; } = new List(); + + /// + /// The IDs of the workflow instances that this plan applies to. + /// + public ICollection WorkflowInstanceIds { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunResponse.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunResponse.cs new file mode 100644 index 000000000..0d65d891e --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/RunResponse.cs @@ -0,0 +1,14 @@ +using Elsa.Api.Client.Resources.Alterations.Models; + +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// The response to the Run endpoint +/// +public class RunResponse +{ + /// + /// The alteration results of a Run request + /// + private ICollection Results { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/SubmitResponse.cs b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/SubmitResponse.cs new file mode 100644 index 000000000..82c5cd1e3 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Alterations/Responses/SubmitResponse.cs @@ -0,0 +1,12 @@ +namespace Elsa.Api.Client.Resources.Alterations.Responses; + +/// +/// The response to the "Submit" endpoint +/// +public class SubmitResponse +{ + /// + /// The ID of the alteration plan created as part of the Submit request + /// + public string PlanId { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations.Core/Models/AlterationPlanParams.cs b/src/modules/Elsa.Alterations.Core/Models/AlterationPlanParams.cs index dec58d038..08c266c4c 100644 --- a/src/modules/Elsa.Alterations.Core/Models/AlterationPlanParams.cs +++ b/src/modules/Elsa.Alterations.Core/Models/AlterationPlanParams.cs @@ -18,7 +18,7 @@ public class AlterationPlanParams public ICollection Alterations { get; set; } = new List(); /// - /// The IDs of the workflow instances that this plan applies to. + /// The filter used to determine which workflow instances that this plan applies to. /// public AlterationWorkflowInstanceFilter Filter { get; set; } = new(); } \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs index eb9461dff..cbf0d5471 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; namespace Elsa.Alterations.Endpoints.Alterations.DryRun; /// -/// Executes an alteration plan. +/// Determines which workflow instances a "Submit" request would target without actually running an alteration. /// [PublicAPI] public class DryRun(IWorkflowInstanceFinder workflowInstanceFinder) : ElsaEndpoint diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/Get/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/Get/Endpoint.cs index c1615636f..7c4be8ebe 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/Get/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/Get/Endpoint.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; namespace Elsa.Alterations.Endpoints.Alterations.Get; /// -/// Executes an alteration plan. +/// Gets an alteration plan and its associated jobs. /// [PublicAPI] public class Get : ElsaEndpointWithoutRequest diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs index 4301682db..a1d090e5e 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/Run/Endpoint.cs @@ -5,7 +5,7 @@ using JetBrains.Annotations; namespace Elsa.Alterations.Endpoints.Alterations.Run; /// -/// Executes an alteration plan. +/// Executes an alteration plan by targeting workflow instances by ID. /// [PublicAPI] public class Run : ElsaEndpoint diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs index 1fbcb9db4..e7ba1233f 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs @@ -8,7 +8,7 @@ using JetBrains.Annotations; namespace Elsa.Alterations.Endpoints.Alterations.Submit; /// -/// Executes an alteration plan. +/// Submits an alteration plan to be executed targeting workflow instances by a filter. /// [PublicAPI] public class Submit : ElsaEndpoint From 73ab3c98591a18a0d373722ffce9b2a8a3119cc0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 5 Feb 2025 15:08:46 +0100 Subject: [PATCH 149/166] Simplify Oracle setup directory structure Moved Oracle setup scripts to a more intuitive directory (`docker/setup`) and updated corresponding volume mappings in `docker-compose.yml`. Adjusted `.gitignore` to exclude the new `docker/data/` directory. This change improves organization and clarity for Oracle-related files. --- .gitignore | 2 ++ docker/docker-compose.yml | 4 ++-- docker/{ => setup}/oracle-setup/setup.sql | 0 3 files changed, 4 insertions(+), 2 deletions(-) rename docker/{ => setup}/oracle-setup/setup.sql (100%) diff --git a/.gitignore b/.gitignore index f772d8ea0..d63bbd6bb 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,5 @@ unlist.sh # build artifacts /artifacts + +/docker/data/ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8d76746a1..137a6cc77 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -34,8 +34,8 @@ - "1521:1521" - "5500:5500" volumes: - - ./oracle-data-free1:/opt/oracle/oradata - - ./oracle-setup:/opt/oracle/scripts/setup + - ./data/oracle-data:/opt/oracle/oradata + - ./setup/oracle-setup:/opt/oracle/scripts/setup mongodb: image: mongo:latest diff --git a/docker/oracle-setup/setup.sql b/docker/setup/oracle-setup/setup.sql similarity index 100% rename from docker/oracle-setup/setup.sql rename to docker/setup/oracle-setup/setup.sql From cf2ac05e92e5f3e922052385cea29ba92696836f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 10:44:04 +0100 Subject: [PATCH 150/166] Refactor ArgumentJsonConverter to improve type handling. Enhanced type handling by distinguishing arrays, collections, and single types. Added support for "isCollection" metadata and adjusted serialization/deserialization logic to ensure accurate mapping of element types and aliases. This improves clarity and flexibility in JSON representation. --- .../Serialization/ArgumentJsonConverter.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs index 81f5a0d42..fc829f41c 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs @@ -28,10 +28,18 @@ public class ArgumentJsonConverter : JsonConverter newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); var jsonObject = (JsonObject)JsonSerializer.SerializeToNode(value, value.GetType(), newOptions)!; - var isArray = value.Type.IsCollectionType(); - jsonObject["isArray"] = isArray; - jsonObject["type"] = _wellKnownTypeRegistry.GetAliasOrDefault(isArray ? value.Type.GetCollectionElementType() : value.Type); - + var typeName = value.Type; + var typeAlias = _wellKnownTypeRegistry.TryGetAlias(typeName, out var alias) ? alias : null; + var isArray = typeName.IsArray; + var isCollection = typeName.IsCollectionType(); + var elementTypeName = isArray ? typeName.GetElementType() : isCollection ? typeName.GenericTypeArguments[0] : typeName; + var elementTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementTypeName); + var finalTypeAlias = isArray || isCollection ? elementTypeAlias : typeAlias; + + if(isArray) jsonObject["isArray"] = isArray; + if(isCollection) jsonObject["isCollection"] = isCollection; + + jsonObject["type"] = finalTypeAlias; JsonSerializer.Serialize(writer, jsonObject, newOptions); } @@ -40,11 +48,12 @@ public class ArgumentJsonConverter : JsonConverter { var jsonObject = (JsonObject)JsonNode.Parse(ref reader)!; var isArray = jsonObject["isArray"]?.GetValue() ?? false; - var typeName = jsonObject["type"]!.GetValue(); - var type = _wellKnownTypeRegistry.GetTypeOrDefault(typeName); + var isCollection = jsonObject["isCollection"]?.GetValue() ?? false; + var typeAlias = jsonObject["type"]!.GetValue(); + var type = _wellKnownTypeRegistry.GetTypeOrDefault(typeAlias); - if (isArray) - type = type.MakeArrayType(); + if (isArray) type = type.MakeArrayType(); + if (isCollection) type = type.MakeGenericType(type.GenericTypeArguments[0]); var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); From babf1125ff5cd3d30de9975979f11635fe79c28e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 10:56:43 +0100 Subject: [PATCH 151/166] Refactor ArgumentJsonConverter to improve type alias handling Introduced logic to handle type aliases more accurately for arrays and collections. Enhanced the final type alias determination by accommodating cases where type aliases are present. Cleaned up unnecessary whitespace for better code readability. --- .../Serialization/ArgumentJsonConverter.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs index fc829f41c..2af9c2334 100644 --- a/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Api/Serialization/ArgumentJsonConverter.cs @@ -20,13 +20,13 @@ public class ArgumentJsonConverter : JsonConverter { _wellKnownTypeRegistry = wellKnownTypeRegistry; } - + /// public override void Write(Utf8JsonWriter writer, ArgumentDefinition value, JsonSerializerOptions options) { var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is ArgumentJsonConverterFactory); - + var jsonObject = (JsonObject)JsonSerializer.SerializeToNode(value, value.GetType(), newOptions)!; var typeName = value.Type; var typeAlias = _wellKnownTypeRegistry.TryGetAlias(typeName, out var alias) ? alias : null; @@ -34,11 +34,12 @@ public class ArgumentJsonConverter : JsonConverter var isCollection = typeName.IsCollectionType(); var elementTypeName = isArray ? typeName.GetElementType() : isCollection ? typeName.GenericTypeArguments[0] : typeName; var elementTypeAlias = _wellKnownTypeRegistry.GetAliasOrDefault(elementTypeName); - var finalTypeAlias = isArray || isCollection ? elementTypeAlias : typeAlias; - - if(isArray) jsonObject["isArray"] = isArray; - if(isCollection) jsonObject["isCollection"] = isCollection; - + var isAliasedArray = (isArray || isCollection) && typeAlias != null; + var finalTypeAlias = isArray || isCollection ? typeAlias ?? elementTypeAlias : elementTypeAlias; + + if (isArray && !isAliasedArray) jsonObject["isArray"] = isArray; + if (isCollection) jsonObject["isCollection"] = isCollection; + jsonObject["type"] = finalTypeAlias; JsonSerializer.Serialize(writer, jsonObject, newOptions); } From 59849bc1bb220fa348e1a660fe3681bf365c58c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20Vasile=20Vu=C8=99can?= Date: Thu, 6 Feb 2025 12:19:26 +0200 Subject: [PATCH 152/166] Exposed incidents in the Elsa.Api.Client --- .../Models/ActivityIncident.cs | 51 +++++++++++++++++++ .../WorkflowInstances/Models/WorkflowState.cs | 5 ++ 2 files changed, 56 insertions(+) create mode 100644 src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ActivityIncident.cs diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ActivityIncident.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ActivityIncident.cs new file mode 100644 index 000000000..078275008 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/ActivityIncident.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Serialization; + +namespace Elsa.Api.Client.Resources.WorkflowInstances.Models; + +/// +/// Holds information about an activity incident. +/// +public class ActivityIncident +{ + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public ActivityIncident() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the activity that caused the incident. + /// The type of the activity that caused the incident. + /// The message of the incident. + /// The exception that caused the incident. + /// The timestamp of the incident. + public ActivityIncident(string activityId, string activityType, string message, ExceptionState? exception, DateTimeOffset timestamp) + { + ActivityId = activityId; + ActivityType = activityType; + Message = message; + Exception = exception; + Timestamp = timestamp; + } + + /// The ID of the activity that caused the incident. + public string ActivityId { get; init; } = default!; + + /// The type of the activity that caused the incident. + public string ActivityType { get; init; } = default!; + + /// The message of the incident. + public string Message { get; init; } = default!; + + /// The exception that caused the incident. + public ExceptionState? Exception { get; init; } + + /// + /// The timestamp of the incident. + /// + public DateTimeOffset Timestamp { get; init; } +} diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/WorkflowState.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/WorkflowState.cs index 1a7c2a7b4..48f1391d8 100644 --- a/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/WorkflowState.cs +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowInstances/Models/WorkflowState.cs @@ -38,6 +38,11 @@ public class WorkflowState : Entity /// public ICollection Bookmarks { get; set; } = new List(); + /// + /// A collection of incidents that may have occurred during execution. + /// + public ICollection Incidents { get; set; } = new List(); + /// /// The serialized workflow state, if any. /// From 7a758f88502108dccfd9959747eec14d409d3171 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 16:39:38 +0100 Subject: [PATCH 153/166] Fix multitenancy support in Hangfire services and jobs Refactored Hangfire-related services and jobs to include tenant context management using ITenantAccessor and ITenantFinder. Updated job constructors and method signatures to support tenant-specific execution. Improved exception-throwing syntax for better readability in BackgroundActivityInvoker. --- .../Implementations/DefaultTenantAccessor.cs | 2 +- .../Jobs/ExecuteBackgroundActivityJob.cs | 22 ++++----- .../Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs | 11 +++-- .../Elsa.Hangfire/Jobs/RunWorkflowJob.cs | 9 ++-- .../HangfireBackgroundActivityScheduler.cs | 9 ++-- .../Services/HangfireWorkflowScheduler.cs | 45 +++++++++---------- .../Services/BackgroundActivityInvoker.cs | 4 +- 7 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs index fd8a699ba..146dfd451 100644 --- a/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs +++ b/src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs @@ -11,7 +11,7 @@ public class DefaultTenantAccessor : ITenantAccessor public Tenant? Tenant { get => CurrentTenantField.Value; - internal set => CurrentTenantField.Value = value; + private set => CurrentTenantField.Value = value; } public IDisposable PushContext(Tenant? tenant) diff --git a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs index 5a9f2622c..0909cba4c 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs @@ -1,28 +1,22 @@ +using Elsa.Common.Multitenancy; using Elsa.Workflows.Runtime; +using JetBrains.Annotations; namespace Elsa.Hangfire.Jobs; /// /// A job that executes a background activity. /// -public class ExecuteBackgroundActivityJob +[UsedImplicitly] +public class ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { - private readonly IBackgroundActivityInvoker _backgroundActivityInvoker; - - /// - /// Initializes a new instance of the class. - /// - /// - public ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker) - { - _backgroundActivityInvoker = backgroundActivityInvoker; - } - /// /// Executes the job. /// - public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) + public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, string? tenantId, CancellationToken cancellationToken = default) { - await _backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); + await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs index c0bc31706..6794264c8 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs @@ -1,4 +1,5 @@ -using Elsa.Scheduling; +using Elsa.Common.Multitenancy; +using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; @@ -7,16 +8,18 @@ namespace Elsa.Hangfire.Jobs; /// /// A job that resumes a workflow. /// -public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime) +public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { /// /// Executes the job. /// - /// The name of the job. /// The workflow request. + /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(string name, ScheduleExistingWorkflowInstanceRequest request, CancellationToken cancellationToken) + public async Task ExecuteAsync(ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken); var runRequest = new RunWorkflowInstanceRequest { diff --git a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs index 708d75a44..29e38f012 100644 --- a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Multitenancy; using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; @@ -7,16 +8,18 @@ namespace Elsa.Hangfire.Jobs; /// /// A job that resumes a workflow. /// -public class RunWorkflowJob(IWorkflowRuntime workflowRuntime) +public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor) { /// /// Executes the job. /// - /// The name of the job. /// The workflow request. + /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(string name, ScheduleNewWorkflowInstanceRequest request, CancellationToken cancellationToken) + public async Task ExecuteAsync(ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { + var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; + using var scope = tenantAccessor.PushContext(tenant); var client = await workflowRuntime.CreateClientAsync(cancellationToken); var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest { diff --git a/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs b/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs index 798d6cfd2..ab3ac2089 100644 --- a/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs +++ b/src/modules/Elsa.Hangfire/Services/HangfireBackgroundActivityScheduler.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Multitenancy; using Elsa.Hangfire.Jobs; using Elsa.Hangfire.States; using Elsa.Workflows.Runtime; @@ -9,11 +10,12 @@ namespace Elsa.Hangfire.Services; /// /// Invokes activities from a background worker within the context of its workflow instance using Hangfire. /// -public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient) : IBackgroundActivityScheduler +public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient, ITenantAccessor tenantAccessor) : IBackgroundActivityScheduler { public Task CreateAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) { - var jobId = backgroundJobClient.Create(x => x.ExecuteAsync(scheduledBackgroundActivity, CancellationToken.None), new PendingState()); + var tenantId = tenantAccessor.Tenant?.Id; + var jobId = backgroundJobClient.Create(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None), new PendingState()); return Task.FromResult(jobId); } @@ -28,7 +30,8 @@ public class HangfireBackgroundActivityScheduler(IBackgroundJobClient background /// public Task ScheduleAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) { - var jobId = backgroundJobClient.Enqueue(x => x.ExecuteAsync(scheduledBackgroundActivity, CancellationToken.None)); + var tenantId = tenantAccessor.Tenant?.Id; + var jobId = backgroundJobClient.Enqueue(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None)); return Task.FromResult(jobId); } diff --git a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs index 802555b3c..58ffab6c9 100644 --- a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs +++ b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Multitenancy; using Elsa.Hangfire.Extensions; using Elsa.Hangfire.Jobs; using Elsa.Scheduling; @@ -9,33 +10,25 @@ namespace Elsa.Hangfire.Services; /// /// An implementation of that uses Hangfire. /// -public class HangfireWorkflowScheduler : IWorkflowScheduler +public class HangfireWorkflowScheduler( + IBackgroundJobClient backgroundJobClient, + IRecurringJobManager recurringJobManager, + ITenantAccessor tenantAccessor, + JobStorage jobStorage) : IWorkflowScheduler { - private readonly IBackgroundJobClient _backgroundJobClient; - private readonly IRecurringJobManager _recurringJobManager; - private readonly JobStorage _jobStorage; - - /// - /// Initializes a new instance of the class. - /// - public HangfireWorkflowScheduler(IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager, JobStorage jobStorage) - { - _backgroundJobClient = backgroundJobClient; - _recurringJobManager = recurringJobManager; - _jobStorage = jobStorage; - } - /// public ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { - _backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at); + var tenantId = tenantAccessor.Tenant?.Id; + backgroundJobClient.Schedule(job => job.ExecuteAsync(request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } /// public ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { - _backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at); + var tenantId = tenantAccessor.Tenant?.Id; + backgroundJobClient.Schedule(job => job.ExecuteAsync(request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } @@ -54,14 +47,16 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler /// public ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { - _recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, CancellationToken.None), cronExpression); + var tenantId = tenantAccessor.Tenant?.Id; + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } /// public ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { - _recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, CancellationToken.None), cronExpression); + var tenantId = tenantAccessor.Tenant?.Id; + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } @@ -75,18 +70,18 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler private void DeleteJobByTaskName(string taskName) { var scheduledJobIds = GetScheduledJobIds(taskName); - foreach (var jobId in scheduledJobIds) _backgroundJobClient.Delete(jobId); + foreach (var jobId in scheduledJobIds) backgroundJobClient.Delete(jobId); var queuedJobsIds = GetQueuedJobIds(taskName); - foreach (var jobId in queuedJobsIds) _backgroundJobClient.Delete(jobId); + foreach (var jobId in queuedJobsIds) backgroundJobClient.Delete(jobId); var recurringJobIds = GetRecurringJobIds(taskName); - foreach (var jobId in recurringJobIds) _recurringJobManager.RemoveIfExists(jobId); + foreach (var jobId in recurringJobIds) recurringJobManager.RemoveIfExists(jobId); } private IEnumerable GetScheduledJobIds(string taskName) { - return _jobStorage.EnumerateScheduledJobs(taskName) + return jobStorage.EnumerateScheduledJobs(taskName) .Select(x => x.Key) .Distinct() .ToList(); @@ -94,7 +89,7 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler private IEnumerable GetQueuedJobIds(string taskName) { - return _jobStorage.EnumerateQueuedJobs("default", taskName) + return jobStorage.EnumerateQueuedJobs("default", taskName) .Select(x => x.Key) .Distinct() .ToList(); @@ -102,7 +97,7 @@ public class HangfireWorkflowScheduler : IWorkflowScheduler private IEnumerable GetRecurringJobIds(string taskName) { - using var connection = _jobStorage.GetConnection(); + using var connection = jobStorage.GetConnection(); var jobs = connection.GetRecurringJobs().Where(x => x.Job.Type == typeof(TJob) && (string)x.Job.Args[0] == taskName); return jobs.Select(x => x.Id).Distinct().ToList(); } diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs index c2541fe79..b5082642f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundActivityInvoker.cs @@ -29,10 +29,10 @@ public class BackgroundActivityInvoker( { var workflowInstanceId = scheduledBackgroundActivity.WorkflowInstanceId; var workflowInstance = await workflowInstanceManager.FindByIdAsync(workflowInstanceId, cancellationToken); - if (workflowInstance == null) throw new Exception("Workflow instance not found"); + if (workflowInstance == null) throw new("Workflow instance not found"); var workflowState = workflowInstance.WorkflowState; var workflow = await workflowDefinitionService.FindWorkflowGraphAsync(workflowInstance.DefinitionVersionId, cancellationToken); - if (workflow == null) throw new Exception("Workflow definition not found"); + if (workflow == null) throw new("Workflow definition not found"); var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(serviceProvider, workflow, workflowState, cancellationToken: cancellationToken); var activityNodeId = scheduledBackgroundActivity.ActivityNodeId; var activityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.NodeId == activityNodeId); From 1f78dbae57fa8108d96f81436e8b45e7dd38557b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 16:58:01 +0100 Subject: [PATCH 154/166] Add taskName parameter to job execution methods This change updates job scheduling and execution methods to include a taskName parameter. It ensures a unique identifier is passed for each job, improving tracking and consistency in job handling. Additionally, documentation comments were updated to reflect this new parameter. --- src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs | 4 +++- src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs | 4 +++- .../Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs index 6794264c8..4b4d19de8 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs @@ -13,10 +13,12 @@ public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder t /// /// Executes the job. /// + /// A unique name for this job. /// The workflow request. /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) + // ReSharper disable once UnusedParameter.Global + public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; using var scope = tenantAccessor.PushContext(tenant); diff --git a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs index 29e38f012..54b32dd3c 100644 --- a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs @@ -13,10 +13,12 @@ public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tena /// /// Executes the job. /// + /// A unique name for this job. /// The workflow request. /// The ID of the current tenant scheduling this job. /// The cancellation token. - public async Task ExecuteAsync(ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) + // ReSharper disable once UnusedParameter.Global + public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; using var scope = tenantAccessor.PushContext(tenant); diff --git a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs index 58ffab6c9..743af48e1 100644 --- a/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs +++ b/src/modules/Elsa.Hangfire/Services/HangfireWorkflowScheduler.cs @@ -20,7 +20,7 @@ public class HangfireWorkflowScheduler( public ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { var tenantId = tenantAccessor.Tenant?.Id; - backgroundJobClient.Schedule(job => job.ExecuteAsync(request, tenantId, CancellationToken.None), at); + backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } @@ -28,7 +28,7 @@ public class HangfireWorkflowScheduler( public ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default) { var tenantId = tenantAccessor.Tenant?.Id; - backgroundJobClient.Schedule(job => job.ExecuteAsync(request, tenantId, CancellationToken.None), at); + backgroundJobClient.Schedule(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at); return ValueTask.CompletedTask; } @@ -48,7 +48,7 @@ public class HangfireWorkflowScheduler( public ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { var tenantId = tenantAccessor.Tenant?.Id; - recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(request, tenantId, CancellationToken.None), cronExpression); + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } @@ -56,7 +56,7 @@ public class HangfireWorkflowScheduler( public ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default) { var tenantId = tenantAccessor.Tenant?.Id; - recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(request, tenantId, CancellationToken.None), cronExpression); + recurringJobManager.AddOrUpdate(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression); return ValueTask.CompletedTask; } From 1081960e6d5d8b4ab1408dcc38fdca7ec19ef266 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 17:09:56 +0100 Subject: [PATCH 155/166] Add 'patch/*' branch to workflow triggers Included 'patch/*' as a trigger for the GitHub Actions workflow to ensure automated processes cover patch branches. This aligns with the existing structure for branch-specific workflows. --- .github/workflows/packages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 189b2242c..101dd71b4 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -6,6 +6,7 @@ on: - 'main' - 'bug/*' - 'perf/*' + - 'patch/*' release: types: [ prereleased, published ] env: From 3800db783a61d0af63d2d5f54ef693b9c627264d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 18:24:01 +0100 Subject: [PATCH 156/166] Update branch filter in release condition for GitHub Actions Replaced 'origin/main' with 'origin/patch/3.3.2-rc2' in the workflow's branch filter. This ensures the release process targets the correct branch during specific GitHub events. --- .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 101dd71b4..1864ebee4 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -43,7 +43,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/main + git branch --remote --contains | grep origin/patch/3.3.2-rc2 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} From 9e395c2bceb593ba8cd4fcf36843ed37893ec7fa Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 18:40:03 +0100 Subject: [PATCH 157/166] Update branch check in release workflow Replaces the hardcoded branch reference `patch/3.3.2-rc2` with `patch/3.3.2` to align with the proper naming convention. This ensures the workflow correctly identifies the intended branch during release events. --- .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 1864ebee4..8d2f51ad2 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -43,7 +43,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/patch/3.3.2-rc2 + git branch --remote --contains | grep origin/patch/3.3.2 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} From 5d21bcb4a99211f6dd1121a9d9039829f27040a6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 19:32:08 +0100 Subject: [PATCH 158/166] Improve null handling and add job retry configuration Updated null checks for job types in Hangfire extensions to prevent potential runtime errors. Added Hangfire's `AutomaticRetry` attribute to RunWorkflowJob and ResumeWorkflowJob to configure retry behavior. Simplified object initialization in WorkflowDefinitions List endpoint. --- src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs | 4 ++-- src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs | 2 ++ src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs | 2 ++ .../Endpoints/WorkflowDefinitions/List/Endpoint.cs | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs b/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs index 64ed51057..5036635eb 100644 --- a/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs +++ b/src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs @@ -23,7 +23,7 @@ public static class JobStorageExtensions { scheduledJobs = api.ScheduledJobs(skip, take); - var jobs = scheduledJobs.FindAll(x => x.Value.Job.Type == typeof(RunWorkflowJob) || x.Value.Job.Type == typeof(ResumeWorkflowJob)); + var jobs = scheduledJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob)); foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == name)) yield return job; @@ -45,7 +45,7 @@ public static class JobStorageExtensions { enqueuedJobs = api.EnqueuedJobs(queueName, skip, take); - var jobs = enqueuedJobs.FindAll(x => x.Value.Job.Type == typeof(RunWorkflowJob) || x.Value.Job.Type == typeof(ResumeWorkflowJob)); + var jobs = enqueuedJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob)); foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == taskName)) yield return job; diff --git a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs index 4b4d19de8..0290bb702 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs @@ -2,6 +2,7 @@ using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; +using Hangfire; namespace Elsa.Hangfire.Jobs; @@ -18,6 +19,7 @@ public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder t /// The ID of the current tenant scheduling this job. /// The cancellation token. // ReSharper disable once UnusedParameter.Global + [AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)] public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; diff --git a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs index 54b32dd3c..9520abc58 100644 --- a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs @@ -2,6 +2,7 @@ using Elsa.Common.Multitenancy; using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; +using Hangfire; namespace Elsa.Hangfire.Jobs; @@ -18,6 +19,7 @@ public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tena /// The ID of the current tenant scheduling this job. /// The cancellation token. // ReSharper disable once UnusedParameter.Global + [AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)] public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken) { var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null; diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs index 230460257..77cf9deae 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/List/Endpoint.cs @@ -33,7 +33,7 @@ internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker li { var versionOptions = string.IsNullOrWhiteSpace(request.VersionOptions) ? default(VersionOptions?) : VersionOptions.FromString(request.VersionOptions); - return new WorkflowDefinitionFilter + return new() { IsSystem = request.IsSystem, VersionOptions = versionOptions, From 9aec0f05794477f29f621595a8270404236af22a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 6 Feb 2025 21:06:41 +0100 Subject: [PATCH 159/166] Update base version and fix branch matching logic Updated the base_version to 3.4.0 to align with the latest release. Adjusted the branch matching logic to use 'origin/main' instead of the specific 'origin/patch/3.3.2' to improve maintainability and adaptability. --- .github/workflows/packages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 8d2f51ad2..b661aee6f 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -10,7 +10,7 @@ on: release: types: [ prereleased, published ] env: - base_version: '3.3.2' + base_version: '3.4.0' feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json' nuget_feed_source: 'https://api.nuget.org/v3/index.json' @@ -43,7 +43,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/patch/3.3.2 + git branch --remote --contains | grep origin/main else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} From f7c5c8349382c5f18bae869ff5d3493b827c1779 Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Fri, 7 Feb 2025 02:55:29 +0000 Subject: [PATCH 160/166] Resolves Directory.Packages.props formatting error. --- Directory.Packages.props | 159 +----------------- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 15 +- 2 files changed, 11 insertions(+), 163 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ae237c860..8d70a9957 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,162 +1,9 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -======= true true - - + + @@ -214,6 +61,7 @@ + @@ -221,6 +69,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 424dda867..7ac6ef99a 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -1,4 +1,4 @@ - + @@ -9,9 +9,8 @@ - - + @@ -66,11 +65,11 @@ - - - - - + + + + + From fe038f9a12379cfccbcb7aba3bde7a9c97307d0a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 7 Feb 2025 12:48:30 +0100 Subject: [PATCH 161/166] Add obsolete ExecuteAsync overloads for job classes Introduce `[Obsolete]` ExecuteAsync overloads in job classes to support legacy calls while encouraging migration to preferred methods. `[UsedImplicitly]` annotations ensure these methods are retained for compatibility. --- .../Jobs/ExecuteBackgroundActivityJob.cs | 10 ++++++++++ .../Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs | 16 ++++++++++++++++ .../Elsa.Hangfire/Jobs/RunWorkflowJob.cs | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs index 0909cba4c..8af4fb8da 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ExecuteBackgroundActivityJob.cs @@ -19,4 +19,14 @@ public class ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundA using var scope = tenantAccessor.PushContext(tenant); await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); } + + /// + /// Executes the job. + /// + [Obsolete("Use the other overload.")] + [UsedImplicitly] + public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default) + { + await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs index 0290bb702..aac9941e2 100644 --- a/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs @@ -3,6 +3,7 @@ using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; using Hangfire; +using JetBrains.Annotations; namespace Elsa.Hangfire.Jobs; @@ -34,4 +35,19 @@ public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder t }; await client.RunInstanceAsync(runRequest, cancellationToken); } + + [Obsolete("Use the other overload.")] + [UsedImplicitly] + public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, CancellationToken cancellationToken) + { + var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken); + var runRequest = new RunWorkflowInstanceRequest + { + BookmarkId = request.BookmarkId, + ActivityHandle = request.ActivityHandle, + Input = request.Input, + Properties = request.Properties + }; + await client.RunInstanceAsync(runRequest, cancellationToken); + } } diff --git a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs index 9520abc58..94a979e28 100644 --- a/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs +++ b/src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs @@ -3,6 +3,7 @@ using Elsa.Scheduling; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Messages; using Hangfire; +using JetBrains.Annotations; namespace Elsa.Hangfire.Jobs; @@ -36,4 +37,21 @@ public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tena }; await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken); } + + [Obsolete("Use the other overload.")] + [UsedImplicitly] + public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, CancellationToken cancellationToken) + { + var client = await workflowRuntime.CreateClientAsync(cancellationToken); + var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest + { + WorkflowDefinitionHandle = request.WorkflowDefinitionHandle, + TriggerActivityId = request.TriggerActivityId, + CorrelationId = request.CorrelationId, + Input = request.Input, + Properties = request.Properties, + ParentId = request.ParentId + }; + await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken); + } } \ No newline at end of file From 23bc29e73dcfb7ecdeba758e84bb633fedd8c9b4 Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Fri, 7 Feb 2025 16:34:22 +0000 Subject: [PATCH 162/166] Removes Built In SQL Editor Configuration --- .../Activities}/SqlCodeOptionsProvider.cs | 2 +- src/modules/Elsa.Sql/Activities/SqlCommand.cs | 4 +++- src/modules/Elsa.Sql/Activities/SqlQuery.cs | 4 +++- .../Elsa.Sql/Activities/SqlSingleValue.cs | 4 +++- src/modules/Elsa.Sql/Features/SqlFeature.cs | 4 ++++ .../Features/WorkflowsFeature.cs | 3 --- .../Elsa.Workflows.Core/UIHints/InputUIHints.cs | 1 - .../UIHints/SqlEditor/SqlEditorUIHintHandler.cs | 16 ---------------- 8 files changed, 14 insertions(+), 24 deletions(-) rename src/modules/{Elsa.Workflows.Core/UIHints/SqlEditor => Elsa.Sql/Activities}/SqlCodeOptionsProvider.cs (86%) delete mode 100644 src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs diff --git a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs b/src/modules/Elsa.Sql/Activities/SqlCodeOptionsProvider.cs similarity index 86% rename from src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs rename to src/modules/Elsa.Sql/Activities/SqlCodeOptionsProvider.cs index 588ea20cf..f61bad42f 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlCodeOptionsProvider.cs +++ b/src/modules/Elsa.Sql/Activities/SqlCodeOptionsProvider.cs @@ -2,7 +2,7 @@ using System.Reflection; using Elsa.Workflows.UIHints.CodeEditor; // ReSharper disable once CheckNamespace -namespace Elsa.Workflows.UIHints.SqlEditor; +namespace Elsa.Sql.Activities; internal class SqlCodeOptionsProvider : CodeEditorOptionsProviderBase { diff --git a/src/modules/Elsa.Sql/Activities/SqlCommand.cs b/src/modules/Elsa.Sql/Activities/SqlCommand.cs index 0038e1aeb..da287a5db 100644 --- a/src/modules/Elsa.Sql/Activities/SqlCommand.cs +++ b/src/modules/Elsa.Sql/Activities/SqlCommand.cs @@ -44,7 +44,9 @@ public class SqlCommand : Activity /// [Input( Description = "Command to run against the database.", - UIHint = InputUIHints.SqlEditor)] + UIHint = InputUIHints.CodeEditor, + UIHandler = typeof(SqlCodeOptionsProvider) + )] public Input Command { get; set; } = default!; diff --git a/src/modules/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/Elsa.Sql/Activities/SqlQuery.cs index 4a2653055..27d9c34a1 100644 --- a/src/modules/Elsa.Sql/Activities/SqlQuery.cs +++ b/src/modules/Elsa.Sql/Activities/SqlQuery.cs @@ -45,7 +45,9 @@ public class SqlQuery : Activity /// [Input( Description = "Query to run against the database.", - UIHint = InputUIHints.SqlEditor)] + UIHint = InputUIHints.CodeEditor, + UIHandler = typeof(SqlCodeOptionsProvider) + )] public Input Query { get; set; } = default!; diff --git a/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs index 037863911..0c5909bd9 100644 --- a/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs +++ b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs @@ -44,7 +44,9 @@ public class SqlSingleValue : Activity /// [Input( Description = "Query to run against the database.", - UIHint = InputUIHints.SqlEditor)] + UIHint = InputUIHints.CodeEditor, + UIHandler = typeof(SqlCodeOptionsProvider) + )] public Input Query { get; set; } = default!; diff --git a/src/modules/Elsa.Sql/Features/SqlFeature.cs b/src/modules/Elsa.Sql/Features/SqlFeature.cs index b0e4cbab0..e2593b641 100644 --- a/src/modules/Elsa.Sql/Features/SqlFeature.cs +++ b/src/modules/Elsa.Sql/Features/SqlFeature.cs @@ -1,6 +1,7 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Services; +using Elsa.Sql.Activities; using Elsa.Sql.Contracts; using Elsa.Sql.Factory; using Elsa.Sql.Implimentations; @@ -51,7 +52,10 @@ public class SqlFeature : FeatureBase }) .AddSingleton() + // Providers + .AddScoped() .AddScoped() .AddScoped(); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index 9a1464533..ffafd87cb 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -24,7 +24,6 @@ using Elsa.Workflows.Services; using Elsa.Workflows.UIHints.CheckList; using Elsa.Workflows.UIHints.Dropdown; using Elsa.Workflows.UIHints.JsonEditor; -using Elsa.Workflows.UIHints.SqlEditor; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Features; @@ -232,13 +231,11 @@ public class WorkflowsFeature : FeatureBase .AddScoped() .AddScoped() .AddScoped() - .AddScoped() // UI property handlers. .AddScoped() .AddScoped() .AddScoped() - .AddScoped() // Logger state generators. .AddSingleton(WorkflowLoggerStateGenerator) diff --git a/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs b/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs index 6616ea606..8465e1bbf 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs +++ b/src/modules/Elsa.Workflows.Core/UIHints/InputUIHints.cs @@ -21,6 +21,5 @@ public static class InputUIHints public const string OutputPicker = "output-picker"; public const string OutcomePicker = "outcome-picker"; public const string JsonEditor = "json-editor"; - public const string SqlEditor = "sql-editor"; public const string DynamicOutcomes = "dynamic-outcomes"; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs b/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs deleted file mode 100644 index 642bc6878..000000000 --- a/src/modules/Elsa.Workflows.Core/UIHints/SqlEditor/SqlEditorUIHintHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Reflection; - -namespace Elsa.Workflows.UIHints.SqlEditor; - -/// -public class SqlEditorUIHintHandler : IUIHintHandler -{ - /// - public string UIHint => InputUIHints.SqlEditor; - - /// - public ValueTask> GetPropertyUIHandlersAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken) - { - return new([typeof(SqlCodeOptionsProvider)]); - } -} \ No newline at end of file From 8952f06dded8d73f5a41b5f465aab3317154bc67 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 9 Feb 2025 08:12:12 +0100 Subject: [PATCH 163/166] Add missing request properties when starting workflow Added support for `Properties` and `ParentId` when creating workflow instances. This fixes an issue where the Properties were not propagated to the workflow instance being created. --- .../Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs index 3778ee265..db557bbbc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs @@ -31,7 +31,9 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio CorrelationId = request.CorrelationId, Input = request.Input, TriggerActivityId = request.TriggerActivityId, - ActivityHandle = request.ActivityHandle + ActivityHandle = request.ActivityHandle, + Properties = request.Properties, + ParentId = request.ParentId }; var runWorkflowResponse = await workflowClient.CreateAndRunInstanceAsync(createWorkflowInstanceRequest, cancellationToken); From 2f90bbe49269c8a09332fe63ef7217996bde951e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 9 Feb 2025 08:15:06 +0100 Subject: [PATCH 164/166] Remove duplicate Kafka compose entry and add oracle setup. Duplicate reference to `docker-compose-kafka.yml` was removed to tidy up the solution file. Added --- Elsa.sln | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Elsa.sln b/Elsa.sln index 12626cd95..3bd4eac22 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -84,14 +84,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 ProjectSection(SolutionItems) = preProject docker\.dockerignore = docker\.dockerignore docker\docker-compose-datadog.yml = docker\docker-compose-datadog.yml - docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml docker\docker-compose.yml = docker\docker-compose.yml docker\ElsaServer-Datadog.Dockerfile = docker\ElsaServer-Datadog.Dockerfile docker\ElsaServer.Dockerfile = docker\ElsaServer.Dockerfile docker\ElsaServerAndStudio.Dockerfile = docker\ElsaServerAndStudio.Dockerfile docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile docker\otel-collector-config.yaml = docker\otel-collector-config.yaml - docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml docker\init-db-postgres.sh = docker\init-db-postgres.sh EndProjectSection EndProject @@ -399,6 +397,7 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.Sqlite", "src\modules\Elsa.Sql.Sqlite\Elsa.Sql.Sqlite.csproj", "{FA5E857F-B173-4B5D-8049-B817A210DEF5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.SqlServer", "src\modules\Elsa.Sql.SqlServer\Elsa.Sql.SqlServer.csproj", "{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "oracle-setup", "oracle-setup", "{66E2E2CF-967F-4564-89E8-F46FA973C99B}" ProjectSection(SolutionItems) = preProject docker\oracle-setup\setup.sql = docker\oracle-setup\setup.sql From d83bb3b8341d6de6ac68e4277d7ee8eda79a2a6f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 9 Feb 2025 08:24:55 +0100 Subject: [PATCH 165/166] Add customizable tenant header support for HTTP routing Introduced `MultitenancyHttpOptions` to configure the tenant header name used for HTTP routing. Updated `HeaderTenantResolver` to utilize this option and modified the server setup to allow overriding the default header name. This enables more flexible multitenancy configurations. --- src/apps/Elsa.Server.Web/Program.cs | 6 ++++- .../Features/MultitenantHttpRoutingFeature.cs | 24 +++++++++++++++++++ .../Options/MultitenancyHttpOptions.cs | 6 +++++ .../Resolvers/HeaderTenantResolver.cs | 7 ++++-- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/modules/Elsa.Tenants.AspNetCore/Options/MultitenancyHttpOptions.cs diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index e22f276fd..32fdbfc5c 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -659,7 +659,11 @@ services } }); - elsa.UseTenantHttpRouting(); + elsa.UseTenantHttpRouting(tenantHttpRouting => + { + // Override the tenant header name with a custom one. + tenantHttpRouting.WithTenantHeader("X-Company-Id"); + }); } elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns")); diff --git a/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs b/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs index 2def1e4d6..bdf0df3b8 100644 --- a/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs +++ b/src/modules/Elsa.Tenants.AspNetCore/Features/MultitenantHttpRoutingFeature.cs @@ -3,6 +3,7 @@ using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Http.Features; +using Elsa.Tenants.AspNetCore.Options; using Elsa.Tenants.AspNetCore.Services; using Elsa.Tenants.Features; using Microsoft.Extensions.DependencyInjection; @@ -13,6 +14,26 @@ namespace Elsa.Tenants.AspNetCore.Features; [DependencyOf(typeof(TenantsFeature))] public class MultitenantHttpRoutingFeature(IModule module) : FeatureBase(module) { + private Action _configureMultitenancyHttpOptions = _ => { }; + + /// + /// Configures the MultitenantHttpRoutingFeature to use a specific tenant header name. + /// + /// The name of the HTTP header used to identify the tenant. + /// The current instance of for fluent configuration. + public MultitenantHttpRoutingFeature WithTenantHeader(string headerName) => WithMultitenancyHttpOptions(options => options.TenantHeaderName = headerName); + + /// + /// Configures the MultitenantHttpRoutingFeature with custom multitenancy HTTP options. + /// + /// The action to configure . + /// The current instance of for fluent configuration. + public MultitenantHttpRoutingFeature WithMultitenancyHttpOptions(Action configure) + { + _configureMultitenancyHttpOptions = configure; + return this; + } + public override void Configure() { Module.Configure(feature => @@ -24,6 +45,9 @@ public class MultitenantHttpRoutingFeature(IModule module) : FeatureBase(module) public override void Apply() { + // Multitenancy HTTP options. + Services.Configure(_configureMultitenancyHttpOptions); + // Tenant resolvers. Services .AddScoped() diff --git a/src/modules/Elsa.Tenants.AspNetCore/Options/MultitenancyHttpOptions.cs b/src/modules/Elsa.Tenants.AspNetCore/Options/MultitenancyHttpOptions.cs new file mode 100644 index 000000000..877400eb6 --- /dev/null +++ b/src/modules/Elsa.Tenants.AspNetCore/Options/MultitenancyHttpOptions.cs @@ -0,0 +1,6 @@ +namespace Elsa.Tenants.AspNetCore.Options; + +public class MultitenancyHttpOptions +{ + public string TenantHeaderName { get; set; } = "X-Tenant-Id"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Tenants.AspNetCore/Resolvers/HeaderTenantResolver.cs b/src/modules/Elsa.Tenants.AspNetCore/Resolvers/HeaderTenantResolver.cs index 06d36e387..697049285 100644 --- a/src/modules/Elsa.Tenants.AspNetCore/Resolvers/HeaderTenantResolver.cs +++ b/src/modules/Elsa.Tenants.AspNetCore/Resolvers/HeaderTenantResolver.cs @@ -1,12 +1,14 @@ using Elsa.Common.Multitenancy; +using Elsa.Tenants.AspNetCore.Options; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace Elsa.Tenants.AspNetCore; /// /// Resolves the tenant based on the header in the request. /// -public class HeaderTenantResolver(IHttpContextAccessor httpContextAccessor) : TenantResolverBase +public class HeaderTenantResolver(IHttpContextAccessor httpContextAccessor, IOptions options) : TenantResolverBase { protected override TenantResolverResult Resolve(TenantResolverContext context) { @@ -15,7 +17,8 @@ public class HeaderTenantResolver(IHttpContextAccessor httpContextAccessor) : Te if (httpContext == null) return Unresolved(); - var tenantId = httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault(); + var headerName = options.Value.TenantHeaderName; + var tenantId = httpContext.Request.Headers[headerName].FirstOrDefault(); return AutoResolve(tenantId); } From 689fd6fecf4ceea3d45104f07704f70fa75b18d0 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 9 Feb 2025 08:36:39 +0100 Subject: [PATCH 166/166] Remove duplicate Kafka compose entry and add oracle setup. Duplicate reference to `docker-compose-kafka.yml` was removed to tidy up the solution file. Added --- Elsa.sln | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Elsa.sln b/Elsa.sln index 12626cd95..3bd4eac22 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -84,14 +84,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54 ProjectSection(SolutionItems) = preProject docker\.dockerignore = docker\.dockerignore docker\docker-compose-datadog.yml = docker\docker-compose-datadog.yml - docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml docker\docker-compose.yml = docker\docker-compose.yml docker\ElsaServer-Datadog.Dockerfile = docker\ElsaServer-Datadog.Dockerfile docker\ElsaServer.Dockerfile = docker\ElsaServer.Dockerfile docker\ElsaServerAndStudio.Dockerfile = docker\ElsaServerAndStudio.Dockerfile docker\ElsaStudio.Dockerfile = docker\ElsaStudio.Dockerfile docker\otel-collector-config.yaml = docker\otel-collector-config.yaml - docker\docker-compose-kafka.yml = docker\docker-compose-kafka.yml docker\init-db-postgres.sh = docker\init-db-postgres.sh EndProjectSection EndProject @@ -399,6 +397,7 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.Sqlite", "src\modules\Elsa.Sql.Sqlite\Elsa.Sql.Sqlite.csproj", "{FA5E857F-B173-4B5D-8049-B817A210DEF5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.SqlServer", "src\modules\Elsa.Sql.SqlServer\Elsa.Sql.SqlServer.csproj", "{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "oracle-setup", "oracle-setup", "{66E2E2CF-967F-4564-89E8-F46FA973C99B}" ProjectSection(SolutionItems) = preProject docker\oracle-setup\setup.sql = docker\oracle-setup\setup.sql