From ba4645ea62c63a1aa45f49ee4a38d9bab5de8e2f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 6 Jan 2025 15:34:43 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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() + }; } ///