diff --git a/src/modules/Elsa.Alterations.Core/Serialization/AlterationSerializer.cs b/src/modules/Elsa.Alterations.Core/Serialization/AlterationSerializer.cs index ff87addd0..001dc5aab 100644 --- a/src/modules/Elsa.Alterations.Core/Serialization/AlterationSerializer.cs +++ b/src/modules/Elsa.Alterations.Core/Serialization/AlterationSerializer.cs @@ -19,7 +19,7 @@ public class AlterationSerializer : ConfigurableSerializer, IAlterationSerialize [RequiresUnreferencedCode("The type of the alteration must be known at compile time.")] public string Serialize(IAlteration alteration) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Serialize(alteration, options); } @@ -27,7 +27,7 @@ public class AlterationSerializer : ConfigurableSerializer, IAlterationSerialize [RequiresUnreferencedCode("The type of the alteration must be known at compile time.")] public string SerializeMany(IEnumerable alterations) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Serialize(alterations.ToArray(), options); } @@ -35,7 +35,7 @@ public class AlterationSerializer : ConfigurableSerializer, IAlterationSerialize [RequiresUnreferencedCode("The type of the alteration must be known at compile time.")] public IAlteration Deserialize(string json) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Deserialize(json, options)!; } @@ -43,7 +43,7 @@ public class AlterationSerializer : ConfigurableSerializer, IAlterationSerialize [RequiresUnreferencedCode("The type of the alteration must be known at compile time.")] public IEnumerable DeserializeMany(string json) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Deserialize(json, options)!; } } \ No newline at end of file diff --git a/src/modules/Elsa.Common/Contracts/IJsonSerializer.cs b/src/modules/Elsa.Common/Contracts/IJsonSerializer.cs index 883aaeade..604b34769 100644 --- a/src/modules/Elsa.Common/Contracts/IJsonSerializer.cs +++ b/src/modules/Elsa.Common/Contracts/IJsonSerializer.cs @@ -11,7 +11,7 @@ public interface IJsonSerializer /// /// Returns the serializer options. /// - JsonSerializerOptions CreateOptions(); + JsonSerializerOptions GetOptions(); /// /// Applies the specified options. diff --git a/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs b/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs index 54648d08b..62a5cf995 100644 --- a/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs +++ b/src/modules/Elsa.Common/Extensions/JsonSerializerOptionsExtensions.cs @@ -19,4 +19,12 @@ public static class JsonSerializerOptionsExtensions return options; } + + /// + /// Clones the options. + /// + public static JsonSerializerOptions Clone(this JsonSerializerOptions options) + { + return new JsonSerializerOptions(options); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Common/Serialization/ConfigurableSerializer.cs b/src/modules/Elsa.Common/Serialization/ConfigurableSerializer.cs index ab7185eb3..a017e645b 100644 --- a/src/modules/Elsa.Common/Serialization/ConfigurableSerializer.cs +++ b/src/modules/Elsa.Common/Serialization/ConfigurableSerializer.cs @@ -14,6 +14,8 @@ namespace Elsa.Common.Serialization; /// public abstract class ConfigurableSerializer { + private JsonSerializerOptions? _options; + /// /// Initializes a new instance of the class. /// @@ -21,7 +23,7 @@ public abstract class ConfigurableSerializer { ServiceProvider = serviceProvider; } - + /// /// Gets the service provider. /// @@ -30,13 +32,17 @@ public abstract class ConfigurableSerializer /// /// Creates a new instance of with the configured options. /// - public virtual JsonSerializerOptions CreateOptions() + public virtual JsonSerializerOptions GetOptions() { + if (_options != null) + return _options; + var options = CreateOptionsInternal(); ApplyOptions(options); + _options = options; return options; } - + /// /// Creates a new instance of with the configured options. /// @@ -46,6 +52,17 @@ public abstract class ConfigurableSerializer AddConverters(options); RunConfigurators(options); } + + /// + /// Creates a new instance of . + /// + protected JsonSerializerOptions GetOptionsInternal() + { + var options = CreateOptionsInternal(); + ApplyOptions(options); + _options = options; + return options; + } /// /// Creates a new instance of . @@ -59,7 +76,7 @@ public abstract class ConfigurableSerializer DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) }; - + options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(JsonMetadataServices.TimeSpanConverter); options.Converters.Add(new IntegerJsonConverter()); @@ -89,14 +106,14 @@ public abstract class ConfigurableSerializer { var configurators = ServiceProvider.GetServices(); var modifiers = new List>(); - + foreach (var configurator in configurators) { configurator.Configure(options); var modifiersToAdd = configurator.GetModifiers(); modifiers.AddRange(modifiersToAdd); } - + options.TypeInfoResolver = new ModifiableJsonTypeInfoResolver(modifiers); } diff --git a/src/modules/Elsa.Common/Serialization/JsonSerializer.cs b/src/modules/Elsa.Common/Serialization/JsonSerializer.cs index 5f0509a2f..2e2dcc6a5 100644 --- a/src/modules/Elsa.Common/Serialization/JsonSerializer.cs +++ b/src/modules/Elsa.Common/Serialization/JsonSerializer.cs @@ -18,7 +18,7 @@ public class StandardJsonSerializer : ConfigurableSerializer, IJsonSerializer [RequiresUnreferencedCode("The type is not known at compile time.")] public string Serialize(object value) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Serialize(value, options); } @@ -26,7 +26,7 @@ public class StandardJsonSerializer : ConfigurableSerializer, IJsonSerializer [RequiresUnreferencedCode("The type is not known at compile time.")] public string Serialize(object value, Type type) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Serialize(value, type, options); } @@ -34,7 +34,7 @@ public class StandardJsonSerializer : ConfigurableSerializer, IJsonSerializer [RequiresUnreferencedCode("The type is not known at compile time.")] public object Deserialize(string json) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Deserialize(json, options)!; } @@ -42,7 +42,7 @@ public class StandardJsonSerializer : ConfigurableSerializer, IJsonSerializer [RequiresUnreferencedCode("The type is not known at compile time.")] public object Deserialize(string json, Type type) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Deserialize(json, type, options)!; } } \ No newline at end of file diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index ba569ef1d..c526fc4b0 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -29,17 +29,17 @@ public static class ObjectConverter /// /// Attempts to convert the source value into the destination type. /// - public static Result TryConvertTo(this object? value, ObjectConverterOptions? serializerOptions = null) => value.TryConvertTo(typeof(T), serializerOptions); + public static Result TryConvertTo(this object? value, ObjectConverterOptions? converterOptions = null) => value.TryConvertTo(typeof(T), converterOptions); /// /// Attempts to convert the source value into the destination type. /// [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")] - public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? serializerOptions = null) + public static Result TryConvertTo(this object? value, Type targetType, ObjectConverterOptions? converterOptions = null) { try { - var convertedValue = value.ConvertTo(targetType, serializerOptions); + var convertedValue = value.ConvertTo(targetType, converterOptions); return new Result(true, convertedValue, null); } catch (Exception e) @@ -52,7 +52,24 @@ public static class ObjectConverter /// Attempts to convert the source value into the destination type. /// [RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")] - public static T? ConvertTo(this object? value, ObjectConverterOptions? serializerOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), serializerOptions) : default; + 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() }, + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; + + private static JsonSerializerOptions InternalSerializerOptions => _internalSerializerOptions ??= new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; /// /// Attempts to convert the source value into the destination type. @@ -68,18 +85,7 @@ public static class ObjectConverter if (sourceType == targetType) return value; - var serializerOptions = converterOptions?.SerializerOptions != null ? new JsonSerializerOptions(converterOptions.SerializerOptions) : new JsonSerializerOptions(); - serializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - serializerOptions.ReferenceHandler = ReferenceHandler.Preserve; - serializerOptions.PropertyNameCaseInsensitive = true; - serializerOptions.Converters.Add(new JsonStringEnumConverter()); - serializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); - - var internalSerializerOptions = new JsonSerializerOptions - { - Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), - }; - + var serializerOptions = converterOptions?.SerializerOptions ?? DefaultSerializerOptions; var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; var underlyingSourceType = Nullable.GetUnderlyingType(sourceType) ?? sourceType; @@ -133,6 +139,8 @@ public static class ObjectConverter if (IsDateType(underlyingSourceType) && IsDateType(underlyingTargetType)) return ConvertAnyDateType(value, underlyingTargetType); + var internalSerializerOptions = InternalSerializerOptions; + if (typeof(IDictionary).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass) { if (typeof(ExpandoObject) == underlyingTargetType) diff --git a/src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs b/src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs index 6f4877850..502f50503 100644 --- a/src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs +++ b/src/modules/Elsa.Workflows.Core/Expressions/ObjectExpressionHandler.cs @@ -1,5 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; using Elsa.Common.Converters; using Elsa.Expressions.Contracts; using Elsa.Expressions.Helpers; @@ -23,9 +26,16 @@ public class ObjectExpressionHandler : IExpressionHandler if (string.IsNullOrWhiteSpace(value)) return ValueTask.FromResult(default(object?)); - var serializerOptions = new JsonSerializerOptions(); + var serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.Preserve, + PropertyNameCaseInsensitive = true, + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; serializerOptions.Converters.Add(new IntegerJsonConverter()); serializerOptions.Converters.Add(new DecimalJsonConverter()); + serializerOptions.Converters.Add(new JsonStringEnumConverter()); var converterOptions = new ObjectConverterOptions(serializerOptions); var model = value.ConvertTo(returnType, converterOptions); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index d5d11c00c..8edbdb55c 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -1,4 +1,8 @@ using System.Collections; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; using Elsa.Common.Contracts; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; @@ -350,6 +354,19 @@ public static class ExpressionExecutionContextExtensions { return context.GetInput(inputDefinition.Name); } + + private static JsonSerializerOptions? _serializerOptions; + + private static JsonSerializerOptions GetSerializerOptions(ExpressionExecutionContext context) + { + if(_serializerOptions != null) + return _serializerOptions; + + var serializerOptions = context.GetRequiredService().GetOptions().Clone(); + serializerOptions.ReferenceHandler = ReferenceHandler.Preserve; + _serializerOptions = serializerOptions; + return serializerOptions; + } /// /// Returns the value of the specified input. @@ -361,7 +378,7 @@ public static class ExpressionExecutionContextExtensions public static T? GetInput(this ExpressionExecutionContext context, string name) { var value = context.GetInput(name); - var serializerOptions = context.GetRequiredService().CreateOptions(); + var serializerOptions = GetSerializerOptions(context); var converterOptions = new ObjectConverterOptions(serializerOptions); return value.ConvertTo(converterOptions); } @@ -432,7 +449,6 @@ public static class ExpressionExecutionContextExtensions { var activity = activityWithOutput.Activity; var activityDescriptor = activityWithOutput.ActivityDescriptor; - var activityIdentifier = useActivityName ? activity.Name : activity.Id; var activityIdPascalName = activityIdentifier.Pascalize(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/VariableExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/VariableExtensions.cs index 83352ed0b..880d22970 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/VariableExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/VariableExtensions.cs @@ -1,5 +1,8 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Unicode; using Elsa.Expressions.Helpers; using Elsa.Workflows.Contracts; using Elsa.Workflows.Memory; @@ -55,9 +58,16 @@ public static class VariableExtensions public static object? ParseValue(this Variable variable, object? value) { var genericType = variable.GetType().GenericTypeArguments.FirstOrDefault(); - var jsonSerializerOptions = new JsonSerializerOptions(); - jsonSerializerOptions.Converters.Add(new ExpandoObjectConverterFactory()); - var converterOptions = new ObjectConverterOptions(jsonSerializerOptions); + var serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.Preserve, + PropertyNameCaseInsensitive = true, + Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) + }; + serializerOptions.Converters.Add(new JsonStringEnumConverter()); + serializerOptions.Converters.Add(new ExpandoObjectConverterFactory()); + var converterOptions = new ObjectConverterOptions(serializerOptions); return genericType == null ? value : value?.ConvertTo(genericType, converterOptions); } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs index 1a59fdf26..4859f023f 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs @@ -22,6 +22,7 @@ public class ActivityJsonConverter : JsonConverter private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; + private JsonSerializerOptions? _options; /// public ActivityJsonConverter( @@ -55,9 +56,7 @@ public class ActivityJsonConverter : JsonConverter activityTypeName = GetActivityDetails(activityRoot, out activityTypeVersion, out activityDescriptor); } - var newOptions = new JsonSerializerOptions(options); - newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); - newOptions.Converters.Add(new OutputJsonConverterFactory(_serviceProvider)); + var newOptions = GetClonedOptions(options); // If the activity type is not found, create a NotFoundActivity instead. if (activityDescriptor == null) @@ -84,10 +83,7 @@ public class ActivityJsonConverter : JsonConverter /// public override void Write(Utf8JsonWriter writer, IActivity value, JsonSerializerOptions options) { - var newOptions = new JsonSerializerOptions(options); - - newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); - newOptions.Converters.Add(new OutputJsonConverterFactory(_serviceProvider)); + var newOptions = GetClonedOptions(options); // Write to a JsonObject so that we can add additional information. var activityModel = JsonSerializer.SerializeToNode(value, value.GetType(), newOptions)!; @@ -212,4 +208,15 @@ public class ActivityJsonConverter : JsonConverter return activityTypeName; } + + private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options) + { + if(_options != null) + return _options; + + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider)); + newOptions.Converters.Add(new OutputJsonConverterFactory(_serviceProvider)); + return _options = newOptions; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExcludeFromHashConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExcludeFromHashConverter.cs index cc175f359..4e3ee4925 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExcludeFromHashConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ExcludeFromHashConverter.cs @@ -11,6 +11,8 @@ namespace Elsa.Workflows.Serialization.Converters; /// public class ExcludeFromHashConverter : JsonConverter { + private JsonSerializerOptions? _options; + /// public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -21,8 +23,7 @@ public class ExcludeFromHashConverter : JsonConverter public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { writer.WriteStartObject(); - var newOptions = new JsonSerializerOptions(options); - newOptions.Converters.RemoveWhere(x => x is ExcludeFromHashConverterFactory); + var newOptions = GetClonedOptions(options); foreach (var property in value.GetType().GetProperties()) { @@ -39,6 +40,16 @@ public class ExcludeFromHashConverter : JsonConverter writer.WriteEndObject(); } + + private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options) + { + if(_options != null) + return _options; + + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.RemoveWhere(x => x is ExcludeFromHashConverterFactory); + return _options = newOptions; + } } /// diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/JsonIgnoreCompositeRootConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/JsonIgnoreCompositeRootConverter.cs index dfd7072b8..4d515f0da 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/JsonIgnoreCompositeRootConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/JsonIgnoreCompositeRootConverter.cs @@ -25,7 +25,6 @@ public class JsonIgnoreCompositeRootConverter : JsonConverter writer.WriteStartObject(); var properties = value?.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) ?? Array.Empty(); - var newOptions = new JsonSerializerOptions(options); foreach (var property in properties) { @@ -45,7 +44,7 @@ public class JsonIgnoreCompositeRootConverter : JsonConverter continue; } - JsonSerializer.Serialize(writer, input, newOptions); + JsonSerializer.Serialize(writer, input, options); } writer.WriteEndObject(); diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs index c5fd590c7..ef7e192fd 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/PolymorphicObjectConverter.cs @@ -31,7 +31,7 @@ public class PolymorphicObjectConverter : JsonConverter /// public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var newOptions = new JsonSerializerOptions(options); + var newOptions = options.Clone(); if (reader.TokenType != JsonTokenType.StartObject && reader.TokenType != JsonTokenType.StartArray) return ReadPrimitive(ref reader, newOptions); @@ -117,7 +117,7 @@ public class PolymorphicObjectConverter : JsonConverter throw new InvalidOperationException($"Cannot determine the element type of array '{targetType}'."); var model = JsonElement.ParseValue(ref reader); - var referenceResolver = (options.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver(); + var referenceResolver = (newOptions.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver(); if (model.TryGetProperty(RefPropertyName, out var refProperty)) { @@ -168,7 +168,7 @@ public class PolymorphicObjectConverter : JsonConverter return; } - var newOptions = new JsonSerializerOptions(options); + var newOptions = options.Clone(); var type = value.GetType(); if (type.IsPrimitive || value is string or decimal or DateTimeOffset or DateTime or DateOnly or TimeOnly or JsonElement or Guid or TimeSpan or Uri or Version or Enum) @@ -196,7 +196,7 @@ public class PolymorphicObjectConverter : JsonConverter // Determine if the value is going to be serialized for the first time. // Later on, we need to know this information to determine if we need to write the type name or not, so that we can reconstruct the actual type when deserializing. var shouldWriteTypeField = true; - var referenceResolver = (CustomPreserveReferenceResolver?)(options.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver(); + var referenceResolver = (CustomPreserveReferenceResolver?)(newOptions.ReferenceHandler as CrossScopedReferenceHandler)?.GetResolver(); if (referenceResolver != null) { diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SafeValueConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/SafeValueConverter.cs index ae720a1b5..fd304ccdc 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/SafeValueConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/SafeValueConverter.cs @@ -10,10 +10,12 @@ namespace Elsa.Workflows.Serialization.Converters; /// public class SafeValueConverter : JsonConverter { + private JsonSerializerOptions? _options; + /// public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var newOptions = CreateNewOptions(options); + var newOptions = GetClonedOptions(options); return JsonSerializer.Deserialize(ref reader, typeToConvert, newOptions)!; } @@ -22,7 +24,7 @@ public class SafeValueConverter : JsonConverter { try { - var newOptions = CreateNewOptions(options); + var newOptions = GetClonedOptions(options); // Serialize the value to a temporary string. var serializedValue = JsonSerializer.Serialize(value, newOptions); @@ -40,10 +42,14 @@ public class SafeValueConverter : JsonConverter } } - private JsonSerializerOptions CreateNewOptions(JsonSerializerOptions options) + private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options) { + if(_options != null) + return _options; + var newOptions = new JsonSerializerOptions(options); newOptions.Converters.RemoveWhere(x => x is SafeValueConverterFactory); + _options = newOptions; return newOptions; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/VariableConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/VariableConverter.cs index 12c622a43..7cd6adef4 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/VariableConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/VariableConverter.cs @@ -14,6 +14,7 @@ namespace Elsa.Workflows.Serialization.Converters; public class VariableConverter : JsonConverter { private readonly VariableMapper _mapper; + private JsonSerializerOptions? _options; /// // ReSharper disable once ContextualLoggerProblem @@ -25,8 +26,7 @@ public class VariableConverter : JsonConverter /// public override Variable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var newOptions = new JsonSerializerOptions(options); - newOptions.Converters.Add(new JsonPrimitiveToStringConverter()); + var newOptions = GetClonedOptions(options); var model = JsonSerializer.Deserialize(ref reader, newOptions)!; var variable = _mapper.Map(model); @@ -39,4 +39,15 @@ public class VariableConverter : JsonConverter var model = _mapper.Map(value); JsonSerializer.Serialize(writer, model, options); } + + private JsonSerializerOptions GetClonedOptions(JsonSerializerOptions options) + { + if(_options != null) + return _options; + + var newOptions = new JsonSerializerOptions(options); + newOptions.Converters.Add(new JsonPrimitiveToStringConverter()); + _options = newOptions; + return newOptions; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs index 54d662798..fcf83da16 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs @@ -18,7 +18,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer /// public string Serialize(object model) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Serialize(model, options); } @@ -28,7 +28,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer /// public T Deserialize(string serializedModel) { - var options = CreateOptions(); + var options = GetOptions(); return JsonSerializer.Deserialize(serializedModel, options)!; } @@ -44,7 +44,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer options.Converters.Add(CreateInstance()); } - JsonSerializerOptions IApiSerializer.CreateOptions() => base.CreateOptions(); + JsonSerializerOptions IApiSerializer.CreateOptions() => GetOptions(); JsonSerializerOptions IApiSerializer.ApplyOptions(JsonSerializerOptions options) { diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs index 7ce83bb14..c206fefd6 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs @@ -1,44 +1,38 @@ using System.Text.Json; using Elsa.Common.Serialization; +using Elsa.Extensions; using Elsa.Workflows.Contracts; using Elsa.Workflows.Serialization.Converters; namespace Elsa.Workflows.Serialization.Serializers; /// -public class JsonActivitySerializer : ConfigurableSerializer, IActivitySerializer +public class JsonActivitySerializer(IServiceProvider serviceProvider) : ConfigurableSerializer(serviceProvider), IActivitySerializer { - /// - /// Initializes a new instance of the class. - /// - public JsonActivitySerializer(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - + private JsonSerializerOptions? _options; + /// public string Serialize(IActivity activity) { - var options = CreateOptions(); - options.Converters.Add(CreateInstance()); + var options = GetOptionsInternal(); return JsonSerializer.Serialize(activity, activity.GetType(), options); } /// public string Serialize(object value) { - var options = CreateOptions(); - options.Converters.Add(CreateInstance()); + var options = GetOptionsInternal(); return JsonSerializer.Serialize(value, options); } /// - public IActivity Deserialize(string serializedActivity) => JsonSerializer.Deserialize(serializedActivity, CreateOptions())!; + public IActivity Deserialize(string serializedActivity) => JsonSerializer.Deserialize(serializedActivity, GetOptions())!; /// - public object Deserialize(string serializedValue, Type type) => JsonSerializer.Deserialize(serializedValue, type, CreateOptions())!; + public object Deserialize(string serializedValue, Type type) => JsonSerializer.Deserialize(serializedValue, type, GetOptions())!; /// - public T Deserialize(string serializedValue) => JsonSerializer.Deserialize(serializedValue, CreateOptions())!; + public T Deserialize(string serializedValue) => JsonSerializer.Deserialize(serializedValue, GetOptions())!; /// protected override void AddConverters(JsonSerializerOptions options) @@ -46,4 +40,14 @@ public class JsonActivitySerializer : ConfigurableSerializer, IActivitySerialize options.Converters.Add(CreateInstance()); options.Converters.Add(CreateInstance()); } + + private JsonSerializerOptions GetOptionsInternal() + { + if(_options != null) + return _options; + + var options = GetOptions().Clone(); + options.Converters.Add(CreateInstance()); + return _options = options; + } } \ 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 c96b6dcb7..48c462c00 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -33,7 +33,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] public Task SerializeAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return Task.FromResult(JsonSerializer.Serialize(workflowState, options)); } @@ -41,7 +41,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] public Task SerializeToUtfBytesAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return Task.FromResult(JsonSerializer.SerializeToUtf8Bytes(workflowState, options)); } @@ -49,7 +49,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The serialization process may require access to the type.")] public Task SerializeToElementAsync(WorkflowState workflowState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return Task.FromResult(JsonSerializer.SerializeToElement(workflowState, options)); } @@ -57,7 +57,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] public Task SerializeAsync(object workflowState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); var json = JsonSerializer.Serialize(workflowState, workflowState.GetType(), options); return Task.FromResult(json); } @@ -66,7 +66,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] public Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); var workflowState = JsonSerializer.Deserialize(serializedState, options)!; return Task.FromResult(workflowState); } @@ -75,7 +75,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] public Task DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); var workflowState = serializedState.Deserialize(options)!; return Task.FromResult(workflowState); } @@ -84,11 +84,18 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat [RequiresUnreferencedCode("The type 'T' may be trimmed from the output. The deserialization process may require access to the type.")] public Task DeserializeAsync(string serializedState, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); var workflowState = JsonSerializer.Deserialize(serializedState, options)!; return Task.FromResult(workflowState); } + /// + public override JsonSerializerOptions GetOptions() + { + // Bypass cached options to ensure that the reference handler is always fresh. + return GetOptionsInternal(); + } + /// protected override void Configure(JsonSerializerOptions options) { diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs index 87cb7307d..12a79f64b 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs @@ -20,7 +20,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask SerializeAsync(object? value, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return ValueTask.FromResult(JsonSerializer.Serialize(value, options)); } @@ -28,7 +28,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask SerializeToElementAsync(object? value, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return new(JsonSerializer.SerializeToElement(value, options)); } @@ -36,7 +36,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask DeserializeAsync(string json, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return new(JsonSerializer.Deserialize(json, options)!); } @@ -44,7 +44,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer [RequiresUnreferencedCode("The type T may be trimmed.")] public ValueTask DeserializeAsync(JsonElement element, CancellationToken cancellationToken = default) { - var options = CreateOptions(); + var options = GetOptions(); return new(element.Deserialize(options)!); } diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowSerializer.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowSerializer.cs index 6299e4f15..40dd126e3 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowSerializer.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowSerializer.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using Elsa.Extensions; using Elsa.Workflows.Activities; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; @@ -12,13 +14,14 @@ namespace Elsa.Workflows.Management.Services; /// public class WorkflowSerializer(IApiSerializer apiSerializer, WorkflowDefinitionMapper workflowDefinitionMapper) : IWorkflowSerializer { + private JsonSerializerOptions? _writeOptions; + /// public string Serialize(Workflow workflow) { var model = workflowDefinitionMapper.Map(workflow); - var serializerOptions = apiSerializer.CreateOptions(); - serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory()); - return apiSerializer.Serialize(model); + var serializerOptions = GetWriteOptionsInternal(); + return JsonSerializer.Serialize(model, serializerOptions); } /// @@ -27,4 +30,14 @@ public class WorkflowSerializer(IApiSerializer apiSerializer, WorkflowDefinition var model = apiSerializer.Deserialize(serializedWorkflow); return workflowDefinitionMapper.Map(model); } + + private JsonSerializerOptions GetWriteOptionsInternal() + { + if(_writeOptions != null) + return _writeOptions; + + var options = apiSerializer.CreateOptions().Clone(); + options.Converters.Add(new JsonIgnoreCompositeRootConverterFactory()); + return _writeOptions = options; + } } \ No newline at end of file