Refactor serializer options usage and enhance caching in workflow serialization (#5275)

* Refactor serializer options usage and enhance caching in workflow serialization

This update refactors multiple classes dealing with JSON serialization. The usage of serializer options now includes a caching mechanism to avoid unnecessary recomputation. 'CreateOptions' method is replaced by a 'GetOptions' method that considers cached options. Classes are adjusted to clone options when necessary, ensuring a fresh and accurate context each time.

* Refactor serializer options in workflow serialization

This commit refactors how options are handled for JSON serializers in Elsa's workflow modules. JSON serializer options are now handled more efficiently, by directly using the provided options in `JsonIgnoreCompositeRootConverter` and setting up internal serializer options in `ObjectConverter`. Unnecessary methods related to the cloning of serializer options have been removed.
This commit is contained in:
Sipke Schoorstra 2024-04-25 11:42:48 +02:00 committed by GitHub
parent f663ad3813
commit 1e6aee4bba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 216 additions and 89 deletions

View file

@ -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<IAlteration> 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<IAlteration>(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<IAlteration> DeserializeMany(string json)
{
var options = CreateOptions();
var options = GetOptions();
return JsonSerializer.Deserialize<IAlteration[]>(json, options)!;
}
}

View file

@ -11,7 +11,7 @@ public interface IJsonSerializer
/// <summary>
/// Returns the serializer options.
/// </summary>
JsonSerializerOptions CreateOptions();
JsonSerializerOptions GetOptions();
/// <summary>
/// Applies the specified options.

View file

@ -19,4 +19,12 @@ public static class JsonSerializerOptionsExtensions
return options;
}
/// <summary>
/// Clones the options.
/// </summary>
public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
{
return new JsonSerializerOptions(options);
}
}

View file

@ -14,6 +14,8 @@ namespace Elsa.Common.Serialization;
/// </summary>
public abstract class ConfigurableSerializer
{
private JsonSerializerOptions? _options;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurableSerializer"/> class.
/// </summary>
@ -30,10 +32,14 @@ public abstract class ConfigurableSerializer
/// <summary>
/// Creates a new instance of <see cref="JsonSerializerOptions"/> with the configured options.
/// </summary>
public virtual JsonSerializerOptions CreateOptions()
public virtual JsonSerializerOptions GetOptions()
{
if (_options != null)
return _options;
var options = CreateOptionsInternal();
ApplyOptions(options);
_options = options;
return options;
}
@ -47,6 +53,17 @@ public abstract class ConfigurableSerializer
RunConfigurators(options);
}
/// <summary>
/// Creates a new instance of <see cref="JsonSerializerOptions"/>.
/// </summary>
protected JsonSerializerOptions GetOptionsInternal()
{
var options = CreateOptionsInternal();
ApplyOptions(options);
_options = options;
return options;
}
/// <summary>
/// Creates a new instance of <see cref="JsonSerializerOptions"/>.
/// </summary>

View file

@ -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<object>(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)!;
}
}

View file

@ -29,17 +29,17 @@ public static class ObjectConverter
/// <summary>
/// Attempts to convert the source value into the destination type.
/// </summary>
public static Result TryConvertTo<T>(this object? value, ObjectConverterOptions? serializerOptions = null) => value.TryConvertTo(typeof(T), serializerOptions);
public static Result TryConvertTo<T>(this object? value, ObjectConverterOptions? converterOptions = null) => value.TryConvertTo(typeof(T), converterOptions);
/// <summary>
/// Attempts to convert the source value into the destination type.
/// </summary>
[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.
/// </summary>
[RequiresUnreferencedCode("The JsonSerializer type is not trim-compatible.")]
public static T? ConvertTo<T>(this object? value, ObjectConverterOptions? serializerOptions = null) => value != null ? (T?)value.ConvertTo(typeof(T), serializerOptions) : default;
public static T? ConvertTo<T>(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)
};
/// <summary>
/// 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<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
{
if (typeof(ExpandoObject) == underlyingTargetType)

View file

@ -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);

View file

@ -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;
@ -351,6 +355,19 @@ public static class ExpressionExecutionContextExtensions
return context.GetInput<T>(inputDefinition.Name);
}
private static JsonSerializerOptions? _serializerOptions;
private static JsonSerializerOptions GetSerializerOptions(ExpressionExecutionContext context)
{
if(_serializerOptions != null)
return _serializerOptions;
var serializerOptions = context.GetRequiredService<IJsonSerializer>().GetOptions().Clone();
serializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
_serializerOptions = serializerOptions;
return serializerOptions;
}
/// <summary>
/// Returns the value of the specified input.
/// </summary>
@ -361,7 +378,7 @@ public static class ExpressionExecutionContextExtensions
public static T? GetInput<T>(this ExpressionExecutionContext context, string name)
{
var value = context.GetInput(name);
var serializerOptions = context.GetRequiredService<IJsonSerializer>().CreateOptions();
var serializerOptions = GetSerializerOptions(context);
var converterOptions = new ObjectConverterOptions(serializerOptions);
return value.ConvertTo<T>(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();

View file

@ -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);
}

View file

@ -22,6 +22,7 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ActivityJsonConverter> _logger;
private JsonSerializerOptions? _options;
/// <inheritdoc />
public ActivityJsonConverter(
@ -55,9 +56,7 @@ public class ActivityJsonConverter : JsonConverter<IActivity>
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<IActivity>
/// <inheritdoc />
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<IActivity>
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;
}
}

View file

@ -11,6 +11,8 @@ namespace Elsa.Workflows.Serialization.Converters;
/// </summary>
public class ExcludeFromHashConverter : JsonConverter<object>
{
private JsonSerializerOptions? _options;
/// <inheritdoc />
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@ -21,8 +23,7 @@ public class ExcludeFromHashConverter : JsonConverter<object>
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<object>
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;
}
}
/// <summary>

View file

@ -25,7 +25,6 @@ public class JsonIgnoreCompositeRootConverter : JsonConverter<IActivity>
writer.WriteStartObject();
var properties = value?.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) ?? Array.Empty<PropertyInfo>();
var newOptions = new JsonSerializerOptions(options);
foreach (var property in properties)
{
@ -45,7 +44,7 @@ public class JsonIgnoreCompositeRootConverter : JsonConverter<IActivity>
continue;
}
JsonSerializer.Serialize(writer, input, newOptions);
JsonSerializer.Serialize(writer, input, options);
}
writer.WriteEndObject();

View file

@ -31,7 +31,7 @@ public class PolymorphicObjectConverter : JsonConverter<object>
/// <inheritdoc />
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<object>
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<object>
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<object>
// 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)
{

View file

@ -10,10 +10,12 @@ namespace Elsa.Workflows.Serialization.Converters;
/// </summary>
public class SafeValueConverter : JsonConverter<object>
{
private JsonSerializerOptions? _options;
/// <inheritdoc />
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<object>
{
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<object>
}
}
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;
}
}

View file

@ -14,6 +14,7 @@ namespace Elsa.Workflows.Serialization.Converters;
public class VariableConverter : JsonConverter<Variable>
{
private readonly VariableMapper _mapper;
private JsonSerializerOptions? _options;
/// <inheritdoc />
// ReSharper disable once ContextualLoggerProblem
@ -25,8 +26,7 @@ public class VariableConverter : JsonConverter<Variable>
/// <inheritdoc />
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<VariableModel>(ref reader, newOptions)!;
var variable = _mapper.Map(model);
@ -39,4 +39,15 @@ public class VariableConverter : JsonConverter<Variable>
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;
}
}

View file

@ -18,7 +18,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer
/// <inheritdoc />
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
/// <inheritdoc />
public T Deserialize<T>(string serializedModel)
{
var options = CreateOptions();
var options = GetOptions();
return JsonSerializer.Deserialize<T>(serializedModel, options)!;
}
@ -44,7 +44,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer
options.Converters.Add(CreateInstance<TypeJsonConverter>());
}
JsonSerializerOptions IApiSerializer.CreateOptions() => base.CreateOptions();
JsonSerializerOptions IApiSerializer.CreateOptions() => GetOptions();
JsonSerializerOptions IApiSerializer.ApplyOptions(JsonSerializerOptions options)
{

View file

@ -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;
/// <inheritdoc cref="IActivitySerializer" />
public class JsonActivitySerializer : ConfigurableSerializer, IActivitySerializer
public class JsonActivitySerializer(IServiceProvider serviceProvider) : ConfigurableSerializer(serviceProvider), IActivitySerializer
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonActivitySerializer"/> class.
/// </summary>
public JsonActivitySerializer(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
private JsonSerializerOptions? _options;
/// <inheritdoc />
public string Serialize(IActivity activity)
{
var options = CreateOptions();
options.Converters.Add(CreateInstance<JsonIgnoreCompositeRootConverterFactory>());
var options = GetOptionsInternal();
return JsonSerializer.Serialize(activity, activity.GetType(), options);
}
/// <inheritdoc />
public string Serialize(object value)
{
var options = CreateOptions();
options.Converters.Add(CreateInstance<JsonIgnoreCompositeRootConverterFactory>());
var options = GetOptionsInternal();
return JsonSerializer.Serialize(value, options);
}
/// <inheritdoc />
public IActivity Deserialize(string serializedActivity) => JsonSerializer.Deserialize<IActivity>(serializedActivity, CreateOptions())!;
public IActivity Deserialize(string serializedActivity) => JsonSerializer.Deserialize<IActivity>(serializedActivity, GetOptions())!;
/// <inheritdoc />
public object Deserialize(string serializedValue, Type type) => JsonSerializer.Deserialize(serializedValue, type, CreateOptions())!;
public object Deserialize(string serializedValue, Type type) => JsonSerializer.Deserialize(serializedValue, type, GetOptions())!;
/// <inheritdoc />
public T Deserialize<T>(string serializedValue) => JsonSerializer.Deserialize<T>(serializedValue, CreateOptions())!;
public T Deserialize<T>(string serializedValue) => JsonSerializer.Deserialize<T>(serializedValue, GetOptions())!;
/// <inheritdoc />
protected override void AddConverters(JsonSerializerOptions options)
@ -46,4 +40,14 @@ public class JsonActivitySerializer : ConfigurableSerializer, IActivitySerialize
options.Converters.Add(CreateInstance<TypeJsonConverter>());
options.Converters.Add(CreateInstance<InputJsonConverterFactory>());
}
private JsonSerializerOptions GetOptionsInternal()
{
if(_options != null)
return _options;
var options = GetOptions().Clone();
options.Converters.Add(CreateInstance<JsonIgnoreCompositeRootConverterFactory>());
return _options = options;
}
}

View file

@ -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<string> 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<byte[]> 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<JsonElement> 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<string> 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<WorkflowState> DeserializeAsync(string serializedState, CancellationToken cancellationToken = default)
{
var options = CreateOptions();
var options = GetOptions();
var workflowState = JsonSerializer.Deserialize<WorkflowState>(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<WorkflowState> DeserializeAsync(JsonElement serializedState, CancellationToken cancellationToken = default)
{
var options = CreateOptions();
var options = GetOptions();
var workflowState = serializedState.Deserialize<WorkflowState>(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<T> DeserializeAsync<T>(string serializedState, CancellationToken cancellationToken = default)
{
var options = CreateOptions();
var options = GetOptions();
var workflowState = JsonSerializer.Deserialize<T>(serializedState, options)!;
return Task.FromResult(workflowState);
}
/// <inheritdoc />
public override JsonSerializerOptions GetOptions()
{
// Bypass cached options to ensure that the reference handler is always fresh.
return GetOptionsInternal();
}
/// <inheritdoc />
protected override void Configure(JsonSerializerOptions options)
{

View file

@ -20,7 +20,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<string> 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<JsonElement> 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<T> DeserializeAsync<T>(string json, CancellationToken cancellationToken = default)
{
var options = CreateOptions();
var options = GetOptions();
return new(JsonSerializer.Deserialize<T>(json, options)!);
}
@ -44,7 +44,7 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer
[RequiresUnreferencedCode("The type T may be trimmed.")]
public ValueTask<T> DeserializeAsync<T>(JsonElement element, CancellationToken cancellationToken = default)
{
var options = CreateOptions();
var options = GetOptions();
return new(element.Deserialize<T>(options)!);
}

View file

@ -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;
/// </summary>
public class WorkflowSerializer(IApiSerializer apiSerializer, WorkflowDefinitionMapper workflowDefinitionMapper) : IWorkflowSerializer
{
private JsonSerializerOptions? _writeOptions;
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -27,4 +30,14 @@ public class WorkflowSerializer(IApiSerializer apiSerializer, WorkflowDefinition
var model = apiSerializer.Deserialize<WorkflowDefinitionModel>(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;
}
}