From 2e1ed995dad0d9dd4ca1c1aa226a02f2b752e05e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 27 Sep 2024 12:59:59 +0200 Subject: [PATCH] Add synchronous serialization methods and mark async as obsolete Implemented synchronous methods for serialization and deserialization while marking the asynchronous methods as obsolete across various storage and serialization services. This includes updates to handle serialization within the Save and Load methods, enhancing performance by avoiding unnecessary Task usage. --- src/bundles/Elsa.Server.Web/SampleWorkflow.cs | 41 +++++++++ src/bundles/Elsa.Server.Web/appsettings.json | 2 +- .../DapperActivityExecutionRecordStore.cs | 32 +++---- .../Runtime/ActivityExecutionLogStore.cs | 4 +- .../Runtime/WorkflowExecutionLogStore.cs | 4 +- .../WorkflowInstances/Export/Endpoint.cs | 6 +- .../WorkflowInstances/Import/Endpoint.cs | 4 +- .../Contracts/ISafeSerializer.cs | 28 ++++++ .../Contracts/IWorkflowStateSerializer.cs | 88 +++++++++++++++++-- .../ActivityExecutionContextExtensions.cs | 4 +- .../JsonWorkflowStateSerializer.cs | 74 ++++++++++++---- .../Serializers/SafeSerializer.cs | 36 ++++++-- .../SerializerEncodingTests.cs | 20 +---- 13 files changed, 264 insertions(+), 79 deletions(-) create mode 100644 src/bundles/Elsa.Server.Web/SampleWorkflow.cs diff --git a/src/bundles/Elsa.Server.Web/SampleWorkflow.cs b/src/bundles/Elsa.Server.Web/SampleWorkflow.cs new file mode 100644 index 000000000..44c8a15ca --- /dev/null +++ b/src/bundles/Elsa.Server.Web/SampleWorkflow.cs @@ -0,0 +1,41 @@ +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Scheduling.Activities; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Server.Web; + +public class SampleWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder workflow) + { + // The WithVariable method ensures that the created variable will be added to the Workflow's Variables collection, which is required for persistent variables. + var variable1 = workflow.WithVariable("Foo").WithWorkflowStorage(); + + workflow.Variables = + [ + variable1 + ]; + + workflow.Root = new Sequence + { + Activities = + { + new StartAt(DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5)) + { + CanStartWorkflow = true + }, + new WriteLine(variable1), + new SetVariable + { + Variable = variable1, + Value = new (Literal.From("Bar")) + }, + new Delay(TimeSpan.FromSeconds(1)), + new WriteLine(variable1) + } + }; + } +} \ No newline at end of file diff --git a/src/bundles/Elsa.Server.Web/appsettings.json b/src/bundles/Elsa.Server.Web/appsettings.json index a6d14c54d..549fea290 100644 --- a/src/bundles/Elsa.Server.Web/appsettings.json +++ b/src/bundles/Elsa.Server.Web/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Debug", + "Default": "Warning", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "OpenTelemetry": "Debug" diff --git a/src/modules/Elsa.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs b/src/modules/Elsa.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs index 6ba1e6757..aba418fc6 100644 --- a/src/modules/Elsa.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs +++ b/src/modules/Elsa.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs @@ -38,14 +38,14 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore /// public async Task SaveAsync(ActivityExecutionRecord record, CancellationToken cancellationToken = default) { - var mappedRecord = await Map(record, cancellationToken); + var mappedRecord = Map(record, cancellationToken); await _store.SaveAsync(mappedRecord, PrimaryKeyName, cancellationToken); } /// public async Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) { - var mappedRecords = await Task.WhenAll(records.Select(async x => await Map(x, cancellationToken))); + var mappedRecords = records.Select(x => Map(x, cancellationToken)); await _store.SaveManyAsync(mappedRecords, PrimaryKeyName, cancellationToken); } @@ -53,21 +53,21 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore public async Task FindAsync(ActivityExecutionRecordFilter filter, CancellationToken cancellationToken = default) { var record = await _store.FindAsync(q => ApplyFilter(q, filter), cancellationToken); - return record == null ? null : await MapAsync(record, cancellationToken); + return record == null ? null : Map(record, cancellationToken); } /// public async Task> FindManyAsync(ActivityExecutionRecordFilter filter, ActivityExecutionRecordOrder order, CancellationToken cancellationToken = default) { var records = await _store.FindManyAsync(q => ApplyFilter(q, filter), order.KeySelector.GetPropertyName(), order.Direction, cancellationToken); - return await Task.WhenAll(records.Select(async x => await MapAsync(x, cancellationToken))); + return records.Select( x => Map(x, cancellationToken)).ToList(); } /// public async Task> FindManyAsync(ActivityExecutionRecordFilter filter, CancellationToken cancellationToken = default) { var records = await _store.FindManyAsync(q => ApplyFilter(q, filter), cancellationToken); - return await Task.WhenAll(records.Select(async x => await MapAsync(x, cancellationToken))); + return records.Select( x => Map(x, cancellationToken)).ToList(); } /// @@ -115,7 +115,7 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore } } - private async ValueTask Map(ActivityExecutionRecord source, CancellationToken cancellationToken) + private ActivityExecutionRecordRecord Map(ActivityExecutionRecord source, CancellationToken cancellationToken) { return new ActivityExecutionRecordRecord { @@ -130,15 +130,15 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore HasBookmarks = source.HasBookmarks, Status = source.Status.ToString(), ActivityTypeVersion = source.ActivityTypeVersion, - SerializedActivityState = source.ActivityState != null ? await _safeSerializer.SerializeAsync(source.ActivityState, cancellationToken) : null, - SerializedPayload = source.Payload != null ? await _safeSerializer.SerializeAsync(source.Payload, cancellationToken) : null, - SerializedOutputs = source.Outputs?.Any() == true ? await _safeSerializer.SerializeAsync(source.Outputs, cancellationToken) : null, + SerializedActivityState = source.ActivityState != null ? _safeSerializer.Serialize(source.ActivityState, cancellationToken) : null, + SerializedPayload = source.Payload != null ? _safeSerializer.Serialize(source.Payload, cancellationToken) : null, + SerializedOutputs = source.Outputs?.Any() == true ? _safeSerializer.Serialize(source.Outputs, cancellationToken) : null, SerializedException = source.Exception != null ? _payloadSerializer.Serialize(source.Exception) : null, - SerializedProperties = source.Properties.Any() ? await _safeSerializer.SerializeAsync(source.Properties, cancellationToken) : null + SerializedProperties = source.Properties.Any() ? _safeSerializer.Serialize(source.Properties, cancellationToken) : null }; } - private async ValueTask MapAsync(ActivityExecutionRecordRecord source, CancellationToken cancellationToken) + private ActivityExecutionRecord Map(ActivityExecutionRecordRecord source, CancellationToken cancellationToken) { return new ActivityExecutionRecord { @@ -153,11 +153,11 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore HasBookmarks = source.HasBookmarks, Status = Enum.Parse(source.Status), ActivityTypeVersion = source.ActivityTypeVersion, - ActivityState = source.SerializedActivityState != null ? _payloadSerializer.Deserialize>(source.SerializedActivityState) : default, - Payload = source.SerializedPayload != null ? await _safeSerializer.DeserializeAsync>(source.SerializedPayload, cancellationToken) : default, - Outputs = source.SerializedOutputs != null ? await _safeSerializer.DeserializeAsync>(source.SerializedOutputs, cancellationToken) : default, - Exception = source.SerializedException != null ? _payloadSerializer.Deserialize(source.SerializedException) : default, - Properties = source.SerializedProperties != null ? await _safeSerializer.DeserializeAsync>(source.SerializedProperties, cancellationToken) : default + ActivityState = source.SerializedActivityState != null ? _payloadSerializer.Deserialize>(source.SerializedActivityState) : null, + Payload = source.SerializedPayload != null ? _safeSerializer.Deserialize>(source.SerializedPayload, cancellationToken) : null, + Outputs = source.SerializedOutputs != null ? _safeSerializer.Deserialize>(source.SerializedOutputs, cancellationToken) : null, + Exception = source.SerializedException != null ? _payloadSerializer.Deserialize(source.SerializedException) : null, + Properties = source.SerializedProperties != null ? _safeSerializer.Deserialize>(source.SerializedProperties, cancellationToken) : null }; } diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs index da49cb3f1..68652db65 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/ActivityExecutionLogStore.cs @@ -83,12 +83,12 @@ public class EFCoreActivityExecutionStore( { entity = entity.SanitizeLogMessage(); var compressionAlgorithm = options.Value.CompressionAlgorithm ?? nameof(None); - var serializedActivityState = entity.ActivityState != null ? await safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : null; + var serializedActivityState = entity.ActivityState != null ? safeSerializer.Serialize(entity.ActivityState, cancellationToken) : null; var compressedSerializedActivityState = serializedActivityState != null ? await compressionCodecResolver.Resolve(compressionAlgorithm).CompressAsync(serializedActivityState, cancellationToken) : null; dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = compressedSerializedActivityState; dbContext.Entry(entity).Property("SerializedActivityStateCompressionAlgorithm").CurrentValue = compressionAlgorithm; - dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs?.Any() == true ? await safeSerializer.SerializeAsync(entity.Outputs, cancellationToken) : null; + dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs?.Any() == true ? safeSerializer.Serialize(entity.Outputs, cancellationToken) : null; dbContext.Entry(entity).Property("SerializedProperties").CurrentValue = entity.Properties.Any() ? payloadSerializer.Serialize(entity.Properties) : null; dbContext.Entry(entity).Property("SerializedException").CurrentValue = entity.Exception != null ? payloadSerializer.Serialize(entity.Exception) : null; dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload?.Any() == true ? payloadSerializer.Serialize(entity.Payload) : null; diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs index 5ff56801b..6c6aa563b 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs @@ -87,8 +87,8 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken) { entity = entity.SanitizeLogMessage(); - dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? await _safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : default; - dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? await _safeSerializer.SerializeAsync(entity.Payload, cancellationToken) : default; + dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? _safeSerializer.Serialize(entity.ActivityState, cancellationToken) : null; + dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? _safeSerializer.Serialize(entity.Payload, cancellationToken) : null; } private async ValueTask OnLoadAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity, CancellationToken cancellationToken) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Export/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Export/Endpoint.cs index 5a925a207..4ce168a0b 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Export/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Export/Endpoint.cs @@ -121,10 +121,10 @@ internal class Export : ElsaEndpointWithMapper var executionLogRecords = request.IncludeWorkflowExecutionLog ? await LoadWorkflowExecutionLogRecordsAsync(workflowState.Id, cancellationToken) : default; var activityExecutionLogRecords = request.IncludeActivityExecutionLog ? await LoadActivityExecutionLogRecordsAsync(workflowState.Id, cancellationToken) : default; var bookmarks = request.IncludeBookmarks ? await LoadBookmarksAsync(workflowState.Id, cancellationToken) : null; - var workflowStateElement = await _workflowStateSerializer.SerializeToElementAsync(workflowState, cancellationToken); + var workflowStateElement = _workflowStateSerializer.SerializeToElement(workflowState, cancellationToken); var bookmarksElement = bookmarks != null ? SerializeBookmarks(bookmarks) : default(JsonElement?); - var executionLogRecordsElement = executionLogRecords != null ? await _safeSerializer.SerializeToElementAsync(executionLogRecords, cancellationToken) : default(JsonElement?); - var activityExecutionLogRecordsElement = activityExecutionLogRecords != null ? await _safeSerializer.SerializeToElementAsync(activityExecutionLogRecords, cancellationToken) : default(JsonElement?); + var executionLogRecordsElement = executionLogRecords != null ? _safeSerializer.SerializeToElement(executionLogRecords, cancellationToken) : default(JsonElement?); + var activityExecutionLogRecordsElement = activityExecutionLogRecords != null ? _safeSerializer.SerializeToElement(activityExecutionLogRecords, cancellationToken) : default(JsonElement?); var model = new ExportedWorkflowState(workflowStateElement, bookmarksElement, activityExecutionLogRecordsElement, executionLogRecordsElement); return model; } diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs index 1a8170769..295a577df 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Import/Endpoint.cs @@ -137,13 +137,13 @@ internal class Import : ElsaEndpointWithoutRequest if (model.ActivityExecutionRecords != null) { - var activityExecutionRecords = await _safeSerializer.DeserializeAsync>(model.ActivityExecutionRecords.Value, cancellationToken); + var activityExecutionRecords = _safeSerializer.Deserialize>(model.ActivityExecutionRecords.Value, cancellationToken); await _activityExecutionStore.SaveManyAsync(activityExecutionRecords, cancellationToken); } if (model.WorkflowExecutionLogRecords != null) { - var workflowExecutionLogRecords = await _safeSerializer.DeserializeAsync>(model.WorkflowExecutionLogRecords.Value, cancellationToken); + var workflowExecutionLogRecords = _safeSerializer.Deserialize>(model.WorkflowExecutionLogRecords.Value, cancellationToken); await _workflowExecutionLogStore.SaveManyAsync(workflowExecutionLogRecords, cancellationToken); } } diff --git a/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs index e241694d3..79f18e7b2 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/ISafeSerializer.cs @@ -11,24 +11,52 @@ public interface ISafeSerializer /// /// Serializes the specified state. /// + [Obsolete("Use the non-async Serialize instead.")] [RequiresUnreferencedCode("The type T may be trimmed.")] ValueTask SerializeAsync(object? value, CancellationToken cancellationToken = default); /// /// Serializes the specified state to a object. /// + [Obsolete("Use the non-async SerializeToElement instead.")] [RequiresUnreferencedCode("The type T may be trimmed.")] ValueTask SerializeToElementAsync(object? value, CancellationToken cancellationToken = default); /// /// Deserializes the specified state. /// + [Obsolete("Use the non-async Deserialize instead.")] [RequiresUnreferencedCode("The type T may be trimmed.")] ValueTask DeserializeAsync(string json, CancellationToken cancellationToken = default); /// /// Deserializes the specified state. /// + [Obsolete("Use the non-async Deserialize instead.")] [RequiresUnreferencedCode("The type T may be trimmed.")] ValueTask DeserializeAsync(JsonElement element, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified state. + /// + [RequiresUnreferencedCode("The type T may be trimmed.")] + string Serialize(object? value, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified state to a object. + /// + [RequiresUnreferencedCode("The type T may be trimmed.")] + JsonElement SerializeToElement(object? value, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified state. + /// + [RequiresUnreferencedCode("The type T may be trimmed.")] + T Deserialize(string json, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified state. + /// + [RequiresUnreferencedCode("The type T may be trimmed.")] + T Deserialize(JsonElement element, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowStateSerializer.cs index 5a113bef3..89325b77d 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowStateSerializer.cs @@ -16,8 +16,9 @@ public interface IWorkflowStateSerializer /// The cancellation token. /// The serialized workflow state. [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version Serialize instead.")] Task SerializeAsync(WorkflowState workflowState, CancellationToken cancellationToken = default); - + /// /// Serializes the specified workflow state. /// @@ -25,8 +26,18 @@ public interface IWorkflowStateSerializer /// The cancellation token. /// The serialized workflow state. [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + string Serialize(WorkflowState workflowState, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified workflow state. + /// + /// The workflow state to serialize. + /// The cancellation token. + /// The serialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version SerializeToUtfBytes instead.")] Task SerializeToUtfBytesAsync(WorkflowState workflowState, CancellationToken cancellationToken = default); - + /// /// Serializes the specified workflow state. /// @@ -34,8 +45,18 @@ public interface IWorkflowStateSerializer /// The cancellation token. /// The serialized workflow state. [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + byte[] SerializeToUtfBytes(WorkflowState workflowState, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified workflow state. + /// + /// The workflow state to serialize. + /// The cancellation token. + /// The serialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version SerializeToElement instead.")] Task SerializeToElementAsync(WorkflowState workflowState, CancellationToken cancellationToken = default); - + /// /// Serializes the specified workflow state. /// @@ -43,32 +64,81 @@ public interface IWorkflowStateSerializer /// The cancellation token. /// The serialized workflow state. [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + JsonElement SerializeToElement(WorkflowState workflowState, CancellationToken cancellationToken = default); + + /// + /// Serializes the specified workflow state. + /// + /// The workflow state to serialize. + /// The cancellation token. + /// The serialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version Serialize instead.")] Task SerializeAsync(object workflowState, CancellationToken cancellationToken = default); - + + /// + /// Serializes the specified workflow state. + /// + /// The workflow state to serialize. + /// The cancellation token. + /// The serialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + string Serialize(object workflowState, CancellationToken cancellationToken = default); + /// /// Deserializes the specified serialized state. /// /// The serialized state. /// The cancellation token. /// The deserialized workflow state. - [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default); - + /// /// Deserializes the specified serialized state. /// /// The serialized state. /// The cancellation token. /// The deserialized workflow state. - [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + WorkflowState Deserialize(string serializedState, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified serialized state. + /// + /// The serialized state. + /// The cancellation token. + /// The deserialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] Task DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default); - + /// /// Deserializes the specified serialized state. /// /// The serialized state. /// The cancellation token. /// The deserialized workflow state. - [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + WorkflowState Deserialize(JsonElement serializedState, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified serialized state. + /// + /// The serialized state. + /// The cancellation token. + /// The deserialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified serialized state. + /// + /// The serialized state. + /// The cancellation token. + /// The deserialized workflow state. + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + T Deserialize(string serializedState, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 5c18306fc..9c7f5c2c4 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -253,7 +253,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 = await context.GetRequiredService().SerializeToElementAsync(value); + var serializedValue = context.GetRequiredService().SerializeToElement(value); context.ActivityState[inputDescriptor.Name] = serializedValue; } @@ -409,7 +409,7 @@ public static class ActivityExecutionContextExtensions if (outputValue == null!) continue; - var serializedOutputValue = await serializer.SerializeAsync(outputValue, cancellationToken); + var serializedOutputValue = serializer.Serialize(outputValue, cancellationToken); context.JournalData[outputName] = serializedOutputValue; } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index 3d71dd61c..341f2ff5f 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -31,62 +31,100 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version Serialize instead.")] public Task SerializeAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return Task.FromResult(JsonSerializer.Serialize(workflowState, options)); + return Task.FromResult(Serialize(workflowState, cancellationToken)); } /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version SerializeToUtfBytes instead.")] public Task SerializeToUtfBytesAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return Task.FromResult(JsonSerializer.SerializeToUtf8Bytes(workflowState, options)); + return Task.FromResult(SerializeToUtfBytes(workflowState, cancellationToken)); } /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version SerializeToElement instead.")] public Task SerializeToElementAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return Task.FromResult(JsonSerializer.SerializeToElement(workflowState, options)); + return Task.FromResult(SerializeToElement(workflowState, cancellationToken)); } /// - [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] + [Obsolete("Use the non-async version Serialize instead.")] public Task SerializeAsync(object workflowState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - var json = JsonSerializer.Serialize(workflowState, workflowState.GetType(), options); - return Task.FromResult(json); + return Task.FromResult(Serialize(workflowState, cancellationToken)); } /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] public Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - var workflowState = JsonSerializer.Deserialize(serializedState, options)!; - return Task.FromResult(workflowState); + return Task.FromResult(Deserialize(serializedState, cancellationToken)); } /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] public Task DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default) { - var options = GetOptions(); - var workflowState = serializedState.Deserialize(options)!; - return Task.FromResult(workflowState); + return Task.FromResult(Deserialize(serializedState, cancellationToken)); } /// [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] + [Obsolete("Use the non-async version Deserialize instead.")] public Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default) + { + return Task.FromResult(Deserialize(serializedState, cancellationToken)); + } + + public string Serialize(WorkflowState workflowState, CancellationToken cancellationToken = default) { var options = GetOptions(); - var workflowState = JsonSerializer.Deserialize(serializedState, options)!; - return Task.FromResult(workflowState); + return JsonSerializer.Serialize(workflowState, options); + } + + public byte[] SerializeToUtfBytes(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.SerializeToUtf8Bytes(workflowState, options); + } + + public JsonElement SerializeToElement(WorkflowState workflowState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.SerializeToElement(workflowState, options); + } + + public string Serialize(object workflowState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.Serialize(workflowState, workflowState.GetType(), options); + } + + public WorkflowState Deserialize(string serializedState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.Deserialize(serializedState, options)!; + } + + public WorkflowState Deserialize(JsonElement serializedState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return serializedState.Deserialize(options)!; + } + + public T Deserialize(string serializedState, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.Deserialize(serializedState, options)!; } /// diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs index a9437dba0..6a868c337 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs @@ -22,39 +22,59 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask SerializeAsync(object? value, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return ValueTask.FromResult(JsonSerializer.Serialize(value, options)); + return ValueTask.FromResult(Serialize(value, cancellationToken)); } /// [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask SerializeToElementAsync(object? value, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return new(JsonSerializer.SerializeToElement(value, options)); + return new(SerializeToElement(value, cancellationToken)); } /// [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask DeserializeAsync(string json, CancellationToken cancellationToken = default) { - var options = GetOptions(); - return new(JsonSerializer.Deserialize(json, options)!); + return new(Deserialize(json, cancellationToken)); } /// [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask DeserializeAsync(JsonElement element, CancellationToken cancellationToken = default) + { + return new(Deserialize(element, cancellationToken)); + } + + public string Serialize(object? value, CancellationToken cancellationToken = default) { var options = GetOptions(); - return new(element.Deserialize(options)!); + return JsonSerializer.Serialize(value, options); + } + + public JsonElement SerializeToElement(object? value, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.SerializeToElement(value, options); + } + + public T Deserialize(string json, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return JsonSerializer.Deserialize(json, options)!; + } + + public T Deserialize(JsonElement element, CancellationToken cancellationToken = default) + { + var options = GetOptions(); + return element.Deserialize(options)!; } /// protected override void AddConverters(JsonSerializerOptions options) { var expressionDescriptorRegistry = ServiceProvider.GetRequiredService(); - + options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); options.Converters.Add(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault())); options.Converters.Add(new SafeValueConverterFactory()); diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/SerializerEncodingTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/SerializerEncodingTests.cs index 803cb7f9b..2e9901668 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/SerializerEncodingTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/SerializerEncodingTests.cs @@ -32,17 +32,17 @@ public class SerializerUnicodeEncodingTests(ITestOutputHelper testOutputHelper) } [Fact] - public async Task TestSafeSerializer() + public void TestSafeSerializer() { var serializer = _serviceProvider.GetRequiredService(); - await TestSerializerAsync(input => serializer.SerializeAsync(input).AsTask()); + TestSerializer(input => serializer.Serialize(input)); } [Fact] - public async Task TestWorkflowStateSerializer() + public void TestWorkflowStateSerializer() { var serializer = _serviceProvider.GetRequiredService(); - await TestSerializerAsync(input => serializer.SerializeAsync(input)); + TestSerializer(input => serializer.Serialize(input)); } private void TestSerializer(Func serialize) @@ -56,18 +56,6 @@ public class SerializerUnicodeEncodingTests(ITestOutputHelper testOutputHelper) var serializedStringValue = GetSerializedTextValue(serializedJson); Assert.Equal(unicodeString, serializedStringValue); } - - private async Task TestSerializerAsync(Func> serialize) - { - var unicodeString = UnicodeRangeGenerator.GenerateUnicodeString(); - var anonymousObject = new - { - Text = unicodeString - }; - var serializedJson = await serialize(anonymousObject); - var serializedStringValue = GetSerializedTextValue(serializedJson); - Assert.Equal(unicodeString, serializedStringValue); - } private string GetSerializedTextValue(string serializedJson) {