From 83d0e14248d8ff92e33b0f06b7730c3dda664b10 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 16 Aug 2024 13:16:47 +0200 Subject: [PATCH 1/5] Add new HTTP workflow tests and enhance middleware Introduce tests for resuming specific HTTP workflow instances. Enhanced `HttpWorkflowsMiddleware` to handle workflow instance IDs and correlation IDs for more precise control over workflow activation and resumption. --- .../Middleware/HttpWorkflowsMiddleware.cs | 25 ++++++------ .../Elsa.Workflows.ComponentTests.csproj | 6 +-- ...ResumeSpecificHttpWorkflowInstanceTests.cs | 40 +++++++++++++++++++ .../Workflows/SimpleHttpApiWorkflow.cs | 30 ++++++++++++++ .../{ => Workflows}/http-hello-world.json | 0 5 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/ResumeSpecificHttpWorkflowInstanceTests.cs create mode 100644 test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/SimpleHttpApiWorkflow.cs rename test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/{ => Workflows}/http-hello-world.json (100%) diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs index 4df72c5db..0ec830789 100644 --- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs @@ -69,6 +69,8 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions(); + var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, cancellationToken); + var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, cancellationToken); var bookmarkHash = ComputeBookmarkHash(serviceProvider, matchingPath, method); var lookupResult = await httpWorkflowLookupService.FindWorkflowAsync(bookmarkHash, cancellationToken); @@ -76,7 +78,7 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions 1) + if (triggers.Count > 1) { await HandleMultipleWorkflowsFoundAsync(httpContext, () => triggers.Select(x => new { @@ -85,16 +87,16 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions 1) { @@ -109,7 +111,7 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions input) + private async Task StartWorkflowAsync(HttpContext httpContext, StoredTrigger trigger, WorkflowGraph workflowGraph, string? workflowInstanceId, string? correlationId, Dictionary? input) { var serviceProvider = httpContext.RequestServices; var cancellationToken = httpContext.RequestAborted; @@ -144,8 +146,6 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions { var cancellationTokens = new CancellationTokens(ct, ct); - var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, httpContext.RequestAborted); - var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, httpContext.RequestAborted); var startParams = new StartWorkflowHostParams { Input = input, @@ -160,7 +160,7 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions input) + private async Task ResumeWorkflowAsync(HttpContext httpContext, StoredBookmark bookmark, string? correlationId, IDictionary input) { var serviceProvider = httpContext.RequestServices; var cancellationToken = httpContext.RequestAborted; @@ -193,7 +193,6 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions { - var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, ct); var cancellationTokens = new CancellationTokens(ct, ct); var resumeParams = new ResumeWorkflowHostParams { @@ -219,12 +218,14 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions> FindBookmarksAsync(IServiceProvider serviceProvider, string bookmarkHash, CancellationToken cancellationToken) + private async Task> FindBookmarksAsync(IServiceProvider serviceProvider, string bookmarkHash, string? workflowInstanceId, string? correlationId, CancellationToken cancellationToken) { var bookmarkStore = serviceProvider.GetRequiredService(); var bookmarkFilter = new BookmarkFilter { - Hash = bookmarkHash + Hash = bookmarkHash, + WorkflowInstanceId = workflowInstanceId, + CorrelationId = correlationId }; return await bookmarkStore.FindManyAsync(bookmarkFilter, cancellationToken); } diff --git a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj index deb69e04d..eece3a52f 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj +++ b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj @@ -30,9 +30,6 @@ Always - - Always - Always @@ -81,6 +78,9 @@ Always + + Always + diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/ResumeSpecificHttpWorkflowInstanceTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/ResumeSpecificHttpWorkflowInstanceTests.cs new file mode 100644 index 000000000..0f2708c68 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/ResumeSpecificHttpWorkflowInstanceTests.cs @@ -0,0 +1,40 @@ +using System.Net; +using Elsa.Workflows.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows; + +public class ResumeSpecificHttpWorkflowInstanceTests(App app) : AppComponentTest(app) +{ + [Theory] + [InlineData("workflowInstanceId")] + [InlineData("correlationId")] + public async Task ResumingSpecificWorkflow_ShouldResumeSpecifiedWorkflow(string identifierKey) + { + var client = WorkflowServer.CreateHttpWorkflowClient(); + + // Start 3 instances. + var workflowInstanceId1 = await StartWorkflowAsync(client, identifierKey); + var workflowInstanceId2 = await StartWorkflowAsync(client, identifierKey); + var workflowInstanceId3 = await StartWorkflowAsync(client, identifierKey); + + // Resume the 2nd instance. + var response = await ResumeWorkflowAsync(client, identifierKey, workflowInstanceId2); + + // Response should be OK. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + private async Task StartWorkflowAsync(HttpClient client, string identifierKey) + { + var identityGenerator = Scope.ServiceProvider.GetRequiredService(); + var identifierValue = identityGenerator.GenerateId(); + await client.GetStringAsync($"simple-http-api/start?{identifierKey}={identifierValue}"); + return identifierValue; + } + + private async Task ResumeWorkflowAsync(HttpClient client, string identifierKey, string identifierValue) + { + return await client.GetAsync($"simple-http-api/resume?{identifierKey}={identifierValue}"); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/SimpleHttpApiWorkflow.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/SimpleHttpApiWorkflow.cs new file mode 100644 index 000000000..be17a0bad --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/SimpleHttpApiWorkflow.cs @@ -0,0 +1,30 @@ +using Elsa.Http; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows.Workflows; + +public class SimpleHttpApiWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new Sequence + { + Activities = + [ + new HttpEndpoint + { + Path = new("simple-http-api/start"), + SupportedMethods = new([HttpMethods.Get]), + CanStartWorkflow = true + }, + new HttpEndpoint + { + Path = new("simple-http-api/resume"), + SupportedMethods = new([HttpMethods.Get]) + } + ] + }; + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/http-hello-world.json b/test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/http-hello-world.json similarity index 100% rename from test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/http-hello-world.json rename to test/component/Elsa.Workflows.ComponentTests/Scenarios/HttpWorkflows/Workflows/http-hello-world.json From 2ad3287dba219d56301cb4f50c77a32e300a7e3a Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 16 Aug 2024 21:49:52 +0200 Subject: [PATCH 2/5] Update version to 3.2.0-rc6 in GitHub Actions workflow Changed the fallback version in the GitHub Actions packages workflow from 3.2.0-rc5 to 3.2.0-rc6. This ensures that the workflows use the updated release candidate version for deployments. --- .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 3054dc114..549e11d6e 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -62,7 +62,7 @@ jobs: TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV else - echo "VERSION=3.2.0-rc5.${{github.run_number}}" >> $GITHUB_ENV + echo "VERSION=3.2.0-rc6.${{github.run_number}}" >> $GITHUB_ENV fi - name: Set up JDK 17 uses: actions/setup-java@v2 From 8a417ca765d86262d57afd6afcc2d518009c7f8f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 19 Aug 2024 09:13:45 +0200 Subject: [PATCH 3/5] Remove ExpandoObject support from JintJavaScriptEvaluator (#5913) Removed the `ConverterHelper` and `ExpandoObjectConverter` classes, along with their integration in `JintJavaScriptEvaluator`. Updated unit tests to skip unsupported functionality and added references to related issues for context. --- .../Helpers/ConverterHelper.cs | 59 ------------------- .../ExpandoObjectConverter.cs | 22 ------- .../Services/JintJavaScriptEvaluator.cs | 2 +- .../JavaScriptListsAndArrays/Tests.cs | 6 +- 4 files changed, 5 insertions(+), 84 deletions(-) delete mode 100644 src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs delete mode 100644 src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs diff --git a/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs b/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs deleted file mode 100644 index 44e7645b7..000000000 --- a/src/modules/Elsa.JavaScript/Helpers/ConverterHelper.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections; -using Elsa.Extensions; -using Jint; -using Jint.Native; -using Jint.Native.Object; -using Jint.Runtime.Descriptors; - -namespace Elsa.JavaScript.Helpers; - -internal static class ConverterHelper -{ - public static ObjectInstance ConvertToJsObject(Engine engine, IDictionary expando) - { - var jsObject = engine.Intrinsics.Object.Construct([]); - - foreach (var kvp in expando) - { - var value = kvp.Value; - var jsValue = ConvertToJsValue(engine, value); - var propertyDescriptor = new PropertyDescriptor(jsValue, true, true, true); - jsObject.DefineOwnProperty(kvp.Key, propertyDescriptor); - } - - return jsObject; - } - - private static JsValue ConvertToJsValue(Engine engine, object? value) - { - if (value == null) - return JsValue.Null; - - if (value is IDictionary dict) - return ConvertToJsObject(engine, dict); - - var valueType = value.GetType(); - if (valueType.IsCollectionType()) - { - var list = (ICollection)value; - var jsArray = engine.Intrinsics.Array.Construct(list.Count); - var index = 0; - - foreach (var item in list) - jsArray.Set(index++, ConvertToJsValue(engine, item), true); - - return jsArray; - } - - if (value is string str) - return JsValue.FromObject(engine, str); - - if (value is int or double or float or decimal) - return JsValue.FromObject(engine, Convert.ToDouble(value)); - - if (value is bool b) - return JsValue.FromObject(engine, b); - - return JsValue.FromObject(engine, value); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs b/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs deleted file mode 100644 index 23976a57c..000000000 --- a/src/modules/Elsa.JavaScript/ObjectConverters/ExpandoObjectConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Dynamic; -using Elsa.JavaScript.Helpers; -using Jint; -using Jint.Native; -using Jint.Runtime.Interop; - -namespace Elsa.JavaScript.ObjectConverters; - -internal class ExpandoObjectConverter : IObjectConverter -{ - public bool TryConvert(Engine engine, object value, out JsValue result) - { - if (value is ExpandoObject expandoObject) - { - result = ConverterHelper.ConvertToJsObject(engine, expandoObject); - return true; - } - - result = JsValue.Null; - return false; - } -} \ 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 3d2cbea84..4a6651ccb 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -81,7 +81,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification private void ConfigureObjectConverters(Jint.Options options) { - options.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]); + options.Interop.ObjectConverters.AddRange([new ByteArrayConverter()]); } private void ConfigureArgumentGetters(Engine engine, ExpressionEvaluatorOptions options) diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs index 161d9e51c..397677c62 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JavaScriptListsAndArrays/Tests.cs @@ -80,9 +80,11 @@ public class Tests Assert.Equal(43, magicNumber); } - [Theory(DisplayName = "Can sort array and list properties as mutable arrays")] + [Theory(DisplayName = "Can sort array and list properties as mutable arrays", Skip = "Not supported yet. See https://github.com/elsa-workflows/elsa-core/issues/5912")] [MemberData(nameof(ArraySortEnumerableData))] - // See also: https://github.com/sebastienros/jint/issues/1942 + // See also: + // - https://github.com/sebastienros/jint/issues/1942 + // - https://github.com/elsa-workflows/elsa-core/issues/5912 public async Task Test5(IEnumerable collection) { dynamic dynamicObject = new ExpandoObject(); From 302292255c84d7a1949ef97afb95d9bc5baeb479 Mon Sep 17 00:00:00 2001 From: Raymond den Haan Date: Tue, 20 Aug 2024 12:37:48 +0200 Subject: [PATCH 4/5] Introduce UniqueKeyConstraintViolationException Refactor DataProcessingException to remove unique key logic and create a dedicated UniqueKeyConstraintViolationException. Update DbExceptionTransformer to use the new exception for unique key violations, improving code clarity and separation of concerns. --- .../Handlers/DbExceptionTransformer.cs | 5 +++-- .../Exceptions/DataProcessingException.cs | 6 +----- .../Exceptions/UniqueKeyConstraintViolationException.cs | 4 ++++ 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Exceptions/UniqueKeyConstraintViolationException.cs diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionTransformer.cs b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionTransformer.cs index b822bc3e8..cca8723af 100644 --- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionTransformer.cs +++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Handlers/DbExceptionTransformer.cs @@ -20,8 +20,9 @@ public class DbExceptionTransformer : IDbExceptionHandler Date: Fri, 30 Aug 2024 13:02:41 +0300 Subject: [PATCH 5/5] Add alteration logs to workflow execution logs (#5897) (#5911) --- .../Contexts/AlterationHandlerContext.cs | 9 +++++---- .../Elsa.Alterations.Core/Models/AlterationLog.cs | 7 ++++--- .../Elsa.Alterations.Core/Models/AlterationLogEntry.cs | 3 ++- .../Middleware/Workflows/RunAlterationsMiddleware.cs | 4 ++++ src/modules/Elsa.MongoDb/Common/MongoDbStore.cs | 3 +++ 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/modules/Elsa.Alterations.Core/Contexts/AlterationHandlerContext.cs b/src/modules/Elsa.Alterations.Core/Contexts/AlterationHandlerContext.cs index b883fcdb4..f583f6cd3 100644 --- a/src/modules/Elsa.Alterations.Core/Contexts/AlterationHandlerContext.cs +++ b/src/modules/Elsa.Alterations.Core/Contexts/AlterationHandlerContext.cs @@ -74,11 +74,12 @@ public class AlterationContext /// /// Logs a message. /// + /// The event name to log. /// The message to log. /// The log level. - public void Log(string message, LogLevel logLevel = LogLevel.Information) + public void Log(string eventName, string message, LogLevel logLevel = LogLevel.Information) { - AlterationLog.Add(message, logLevel); + AlterationLog.Add(message, logLevel, eventName); } /// @@ -104,7 +105,7 @@ public class AlterationContext public void Succeed(string message) { HasSucceeded = true; - Log(message, LogLevel.Information); + Log($"Alteration {Alteration.GetType().Name} succeeded", message, LogLevel.Information); } /// @@ -123,6 +124,6 @@ public class AlterationContext public void Fail(string? message = default) { HasFailed = true; - Log(message ?? $"{Alteration.GetType().Name} failed", LogLevel.Error); + Log($"Alteration {Alteration.GetType().Name} failed", message ?? $"{Alteration.GetType().Name} failed", LogLevel.Error); } } \ No newline at end of file diff --git a/src/modules/Elsa.Alterations.Core/Models/AlterationLog.cs b/src/modules/Elsa.Alterations.Core/Models/AlterationLog.cs index 1130d39e7..727f8c87b 100644 --- a/src/modules/Elsa.Alterations.Core/Models/AlterationLog.cs +++ b/src/modules/Elsa.Alterations.Core/Models/AlterationLog.cs @@ -24,15 +24,16 @@ public class AlterationLog /// Gets the log entries. /// public IReadOnlyCollection LogEntries => _logEntries.ToList().AsReadOnly(); - + /// /// Adds a log entry. /// /// The message. /// The log level. - public void Add(string message, LogLevel logLevel = LogLevel.Information) + /// The event that generated the log entry. + public void Add(string message, LogLevel logLevel = LogLevel.Information, string? eventName = null) { - var entry = new AlterationLogEntry(message, logLevel, _systemClock.UtcNow); + var entry = new AlterationLogEntry(message, logLevel, _systemClock.UtcNow, eventName); _logEntries.Add(entry); } diff --git a/src/modules/Elsa.Alterations.Core/Models/AlterationLogEntry.cs b/src/modules/Elsa.Alterations.Core/Models/AlterationLogEntry.cs index af8b7d8ae..77b772c65 100644 --- a/src/modules/Elsa.Alterations.Core/Models/AlterationLogEntry.cs +++ b/src/modules/Elsa.Alterations.Core/Models/AlterationLogEntry.cs @@ -8,4 +8,5 @@ namespace Elsa.Alterations.Core.Models; /// The log message. /// The log level. /// The timestamp when the log entry was created. -public record AlterationLogEntry(string Message, LogLevel LogLevel, DateTimeOffset Timestamp); \ No newline at end of file +/// The event that generated the log entry. +public record AlterationLogEntry(string Message, LogLevel LogLevel, DateTimeOffset Timestamp, string? EventName = null); \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Middleware/Workflows/RunAlterationsMiddleware.cs b/src/modules/Elsa.Alterations/Middleware/Workflows/RunAlterationsMiddleware.cs index 140c598fb..b33889575 100644 --- a/src/modules/Elsa.Alterations/Middleware/Workflows/RunAlterationsMiddleware.cs +++ b/src/modules/Elsa.Alterations/Middleware/Workflows/RunAlterationsMiddleware.cs @@ -50,5 +50,9 @@ internal class RunAlterationsMiddleware(WorkflowMiddlewareDelegate next, IEnumer // Execute commit handlers. foreach (var commitAction in commitActions) await commitAction(); + + // Add alteration logs to the workflow execution log. + foreach (var alterationLogEntry in log.LogEntries) + workflowExecutionContext.AddExecutionLogEntry(alterationLogEntry.EventName ?? alterationLogEntry.Message, alterationLogEntry.Message); } } \ No newline at end of file diff --git a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs index 5c34e049a..4056706a8 100644 --- a/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs +++ b/src/modules/Elsa.MongoDb/Common/MongoDbStore.cs @@ -45,6 +45,9 @@ public class MongoDbStore where TDocument : class /// The cancellation token. public async Task AddManyAsync(IEnumerable documents, CancellationToken cancellationToken = default) { + if (!documents.Any()) + return; + await _collection.InsertManyAsync(documents, new InsertManyOptions(), cancellationToken); }