From 7120f9f2a0af9d960f33efbf3b7d783c28a1ca0e Mon Sep 17 00:00:00 2001 From: Max Brooks <45081361+MaxBrooks114@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:01:16 -0400 Subject: [PATCH] Add FuncExpressionValueConverter for JSON serialization (#6607) Implemented FuncExpressionValueConverter to handle serialization and deserialization of Func> types, ensuring delegates are not serialized and cannot be rehydrated from JSON. Updated multiple serializers including ApiSerializer, BookmarkPayloadSerializer, JsonActivitySerializer, JsonPayloadSerializer, JsonWorkflowStateSerializer, and SafeSerializer to utilize the new converter in their JSON serialization options. Co-authored-by: Max Brooks --- .../FuncExpressionValueConverter.cs | 28 +++++++++++++++++++ .../FuncExpressionValueConverterFactory.cs | 12 ++++++++ .../Serializers/ApiSerializer.cs | 8 +++--- .../Serializers/BookmarkPayloadSerializer.cs | 13 +++++---- .../Serializers/JsonActivitySerializer.cs | 3 +- .../Serializers/JsonPayloadSerializer.cs | 10 ++++--- .../JsonWorkflowStateSerializer.cs | 11 ++++---- .../Serializers/SafeSerializer.cs | 8 +++--- 8 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverter.cs create mode 100644 src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverterFactory.cs diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverter.cs new file mode 100644 index 000000000..ad13c2bf7 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverter.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Expressions.Models; + +namespace Elsa.Workflows.Serialization.Converters; + +/// +/// Prevents System.Text.Json from trying to serialize the compiled delegate. +/// Always emits null and cannot rehydrate a Func. +/// +public class FuncExpressionValueConverter : JsonConverter>> +{ + public override Func> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Skip whatever value is in the JSON (probably null). + if (reader.TokenType != JsonTokenType.Null) + reader.Skip(); + + // We can't deserialize a delegate, so return null. + return null!; + } + + public override void Write(Utf8JsonWriter writer, Func> value, JsonSerializerOptions options) + { + // Emit a JSON null instead of trying to serialize the delegate + writer.WriteNullValue(); + } +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverterFactory.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverterFactory.cs new file mode 100644 index 000000000..0b7cd10b0 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/FuncExpressionValueConverterFactory.cs @@ -0,0 +1,12 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Expressions.Models; + +namespace Elsa.Workflows.Serialization.Converters; + +public class FuncExpressionConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type t) => t == typeof(Func>); + + public override JsonConverter CreateConverter(Type t, JsonSerializerOptions opts) => new FuncExpressionValueConverter(); +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs index a3423338a..7f97058db 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/ApiSerializer.cs @@ -10,9 +10,8 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer /// /// Initializes a new instance of the class. /// - public ApiSerializer(IServiceProvider serviceProvider) : base(serviceProvider) - { - } + public ApiSerializer(IServiceProvider serviceProvider) + : base(serviceProvider) { } /// public string Serialize(object model) @@ -41,6 +40,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer protected override void AddConverters(JsonSerializerOptions options) { options.Converters.Add(CreateInstance()); + options.Converters.Add(CreateInstance()); } JsonSerializerOptions IApiSerializer.GetOptions() => GetOptions(); @@ -50,4 +50,4 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer ApplyOptions(options); return options; } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs index 0d6e73f15..9dab96949 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/BookmarkPayloadSerializer.cs @@ -18,18 +18,21 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer { // Enables serialization of ValueTuples, which use fields instead of properties. IncludeFields = true, - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, }; - + _settings.Converters.Add(new TypeJsonConverter(wellKnownTypeRegistry)); + _settings.Converters.Add(new FuncExpressionValueConverter()); } /// - public T Deserialize(string json) where T : notnull => JsonSerializer.Deserialize(json, _settings)!; + public T Deserialize(string json) + where T : notnull => JsonSerializer.Deserialize(json, _settings)!; /// public object Deserialize(string json, Type type) => JsonSerializer.Deserialize(json, type, _settings)!; /// - public string Serialize(T payload) where T : notnull => JsonSerializer.Serialize(payload, payload.GetType(), _settings); -} \ No newline at end of file + public string Serialize(T payload) + where T : notnull => JsonSerializer.Serialize(payload, payload.GetType(), _settings); +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs index 62f65db9a..7e3f996aa 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonActivitySerializer.cs @@ -37,5 +37,6 @@ public class JsonActivitySerializer(IServiceProvider serviceProvider) : Configur options.Converters.Add(CreateInstance()); options.Converters.Add(CreateInstance()); options.Converters.Add(CreateInstance()); + options.Converters.Add(CreateInstance()); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs index c5ab81a8f..9c97db7a8 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonPayloadSerializer.cs @@ -20,7 +20,7 @@ public class JsonPayloadSerializer : IPayloadSerializer { _serviceProvider = serviceProvider; } - + /// public string Serialize(object payload) { @@ -82,8 +82,10 @@ public class JsonPayloadSerializer : IPayloadSerializer options.Converters.Add(GetService()); options.Converters.Add(GetService()); options.Converters.Add(GetService()); + options.Converters.Add(new FuncExpressionValueConverter()); return options; } - - private T GetService() where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider); -} \ No newline at end of file + + private T GetService() + where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider); +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index ad664f7e8..faf4215b8 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -20,7 +20,8 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat /// /// Initializes a new instance of the class. /// - public JsonWorkflowStateSerializer(IServiceProvider serviceProvider, IWellKnownTypeRegistry wellKnownTypeRegistry, ILoggerFactory loggerFactory) : base(serviceProvider) + public JsonWorkflowStateSerializer(IServiceProvider serviceProvider, IWellKnownTypeRegistry wellKnownTypeRegistry, ILoggerFactory loggerFactory) + : base(serviceProvider) { _wellKnownTypeRegistry = wellKnownTypeRegistry; _loggerFactory = loggerFactory; @@ -128,10 +129,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat public override JsonSerializerOptions GetOptions() { var options = base.GetOptions(); - return new(options) - { - ReferenceHandler = new CrossScopedReferenceHandler() - }; + return new(options) { ReferenceHandler = new CrossScopedReferenceHandler() }; } /// @@ -140,5 +138,6 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry)); options.Converters.Add(new PolymorphicObjectConverterFactory(_wellKnownTypeRegistry)); options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory)); + options.Converters.Add(new FuncExpressionValueConverter()); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs index fbfd58a82..46b7b5400 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/SafeSerializer.cs @@ -13,9 +13,8 @@ namespace Elsa.Workflows.Serialization.Serializers; public class SafeSerializer : ConfigurableSerializer, ISafeSerializer { /// - public SafeSerializer(IServiceProvider serviceProvider) : base(serviceProvider) - { - } + public SafeSerializer(IServiceProvider serviceProvider) + : base(serviceProvider) { } /// [RequiresUnreferencedCode("The type T may be trimmed.")] @@ -78,5 +77,6 @@ public class SafeSerializer : ConfigurableSerializer, ISafeSerializer options.Converters.Add(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault())); options.Converters.Add(new SafeValueConverterFactory()); options.Converters.Add(new ExpressionJsonConverterFactory(expressionDescriptorRegistry)); + options.Converters.Add(new FuncExpressionValueConverter()); } -} \ No newline at end of file +}