From fca51abf52a9df51016483c1e22d333823f63be1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 12 Oct 2023 13:37:32 +0200 Subject: [PATCH 1/3] WIP --- .../Services/JintJavaScriptEvaluator.cs | 5 +- .../Contexts/ActivityExecutionContext.cs | 2 +- .../ActivityExecutionContextExtensions.cs | 9 +- .../DefaultActivityExecutionMapper.cs | 94 +++++++++---------- 4 files changed, 58 insertions(+), 52 deletions(-) diff --git a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs index e7b7f2ba6..0fbe131d0 100644 --- a/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs +++ b/src/modules/Elsa.JavaScript/Services/JintJavaScriptEvaluator.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Contracts; @@ -45,8 +46,8 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator { var engine = await GetConfiguredEngine(configureEngine, context, cancellationToken); var result = ExecuteExpressionAndGetResult(engine, expression); - - return result; + + return result.ConvertTo(returnType); } private async Task GetConfiguredEngine(Action? configureEngine, ExpressionExecutionContext context, CancellationToken cancellationToken) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index be888d22a..46c738142 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -176,7 +176,7 @@ public class ActivityExecutionContext : IExecutionContext public IDictionary JournalData { get; } = new Dictionary(); /// - /// Stores the evaluated inputs for the current activity. + /// Stores the evaluated inputs, serialized, for the current activity for historical purposes. /// public IDictionary ActivityState { get; set; } = new Dictionary(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index ccfb0b2cb..1c254ecd5 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -233,9 +233,14 @@ public static class ActivityExecutionContextExtensions value = input; } - // Store the input value in the activity state. + // Store the serialized input value in the activity state. + // Serializing the value ensures we store a copy of the value and not a reference to the input, which may change over time. if (inputDescriptor.IsSerializable != false) - context.ActivityState[inputDescriptor.Name] = value!; + { + var serializedValue = JsonSerializer.SerializeToElement(value); + //var serializedValue = context.GetRequiredService() + context.ActivityState[inputDescriptor.Name] = serializedValue; + } return value; } diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs index 0d611f1da..7890fdc82 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs @@ -14,58 +14,58 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper public ActivityExecutionRecord Map(ActivityExecutionContext source) { // Get any outcomes that were added to the activity execution context. - var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default; - var payload = new Dictionary(); - - if(outcomes != null) - payload.Add("Outcomes", outcomes); - - // Get any outputs that were added to the activity execution context. - var activity = source.Activity; - var expressionExecutionContext = source.ExpressionExecutionContext; - var activityDescriptor = source.ActivityDescriptor; - var outputDescriptors = activityDescriptor.Outputs; - - var outputs = outputDescriptors.ToDictionary(x => x.Name, x => - { - if(x.IsSerializable == false) - return "(not serializable)"; - - var cachedValue = activity.GetOutput(expressionExecutionContext, x.Name); - - if(cachedValue != default) - return cachedValue; - - if(x.ValueGetter(activity) is Output output && source.TryGet(output.MemoryBlockReference(), out var outputValue)) - return outputValue; - - return default; - }); - - return new ActivityExecutionRecord - { - Id = source.Id, - ActivityId = source.Activity.Id, - WorkflowInstanceId = source.WorkflowExecutionContext.Id, - ActivityType = source.Activity.Type, - ActivityName = source.Activity.Name, - ActivityState = source.ActivityState, - Outputs = outputs, - Payload = payload, - Exception = ExceptionState.FromException(source.Exception), - ActivityTypeVersion = source.Activity.Version, - StartedAt = source.StartedAt, - HasBookmarks = source.Bookmarks.Any(), - Status = GetAggregateStatus(source), - CompletedAt = source.CompletedAt - }; + var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default; + var payload = new Dictionary(); + + if (outcomes != null) + payload.Add("Outcomes", outcomes); + + // Get any outputs that were added to the activity execution context. + var activity = source.Activity; + var expressionExecutionContext = source.ExpressionExecutionContext; + var activityDescriptor = source.ActivityDescriptor; + var outputDescriptors = activityDescriptor.Outputs; + + var outputs = outputDescriptors.ToDictionary(x => x.Name, x => + { + if (x.IsSerializable == false) + return "(not serializable)"; + + var cachedValue = activity.GetOutput(expressionExecutionContext, x.Name); + + if (cachedValue != default) + return cachedValue; + + if (x.ValueGetter(activity) is Output output && source.TryGet(output.MemoryBlockReference(), out var outputValue)) + return outputValue; + + return default; + }); + + return new ActivityExecutionRecord + { + Id = source.Id, + ActivityId = source.Activity.Id, + WorkflowInstanceId = source.WorkflowExecutionContext.Id, + ActivityType = source.Activity.Type, + ActivityName = source.Activity.Name, + ActivityState = source.ActivityState, + Outputs = outputs, + Payload = payload, + Exception = ExceptionState.FromException(source.Exception), + ActivityTypeVersion = source.Activity.Version, + StartedAt = source.StartedAt, + HasBookmarks = source.Bookmarks.Any(), + Status = GetAggregateStatus(source), + CompletedAt = source.CompletedAt + }; } - + private 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; From 2d3c13f74563467573642bba56c9c48e5a9cf138 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 13 Oct 2023 10:17:34 +0200 Subject: [PATCH 2/3] Update SafeSerializer to serialize to JsonElement --- .../Elsa.Workflows.Core/Contracts/ISafeSerializer.cs | 7 +++++++ .../Extensions/ActivityExecutionContextExtensions.cs | 5 ++--- .../Elsa.Workflows.Core/Services/SafeSerializer.cs | 9 +++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs index a88363dd8..50716972c 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace Elsa.Workflows.Core.Contracts; /// @@ -9,6 +11,11 @@ public interface ISafeSerializer /// Serializes the specified state. /// ValueTask SerializeAsync(object? value, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified state to a object. + /// + ValueTask SerializeToElementAsync(object? value, CancellationToken cancellationToken = default); /// /// Deserializes the specified state. diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 1c254ecd5..1115f0482 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -237,8 +237,7 @@ public static class ActivityExecutionContextExtensions // Serializing the value ensures we store a copy of the value and not a reference to the input, which may change over time. if (inputDescriptor.IsSerializable != false) { - var serializedValue = JsonSerializer.SerializeToElement(value); - //var serializedValue = context.GetRequiredService() + var serializedValue = await context.GetRequiredService().SerializeToElementAsync(value); context.ActivityState[inputDescriptor.Name] = serializedValue; } @@ -294,7 +293,7 @@ public static class ActivityExecutionContextExtensions current = current.ParentActivityExecutionContext; } } - + /// /// Returns a flattened list of the current context's descendants. /// diff --git a/src/modules/Elsa.Workflows.Core/Services/SafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Services/SafeSerializer.cs index 002757569..abb8f2ac5 100644 --- a/src/modules/Elsa.Workflows.Core/Services/SafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Services/SafeSerializer.cs @@ -30,6 +30,15 @@ public class SafeSerializer : ISafeSerializer return JsonSerializer.Serialize(value, options); } + /// + public async ValueTask SerializeToElementAsync(object? value, CancellationToken cancellationToken = default) + { + var options = CreateOptions(); + var notification = new SafeSerializerSerializing(value, options); + await _notificationSender.SendAsync(notification, cancellationToken); + return JsonSerializer.SerializeToElement(value, options); + } + /// public async ValueTask DeserializeAsync(string json, CancellationToken cancellationToken = default) { From fa183031987f3f6389c4f8912a681500e8eb8261 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 13 Oct 2023 12:17:00 +0200 Subject: [PATCH 3/3] Handle conversions from object arrays to strings --- src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index 9b0e7067e..fbbc11518 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -130,6 +130,10 @@ public static class ObjectConverter } } + if (typeof(IEnumerable).IsAssignableFrom(underlyingSourceType)) + if (underlyingTargetType == typeof(string)) + return JsonSerializer.Serialize(value); + var targetTypeConverter = TypeDescriptor.GetConverter(underlyingTargetType); if (targetTypeConverter.CanConvertFrom(underlyingSourceType))