Add FuncExpressionValueConverter for JSON serialization (#6607)

Implemented FuncExpressionValueConverter to handle serialization
and deserialization of Func<ExpressionExecutionContext,
ValueTask<object>> 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 <Max@compyl.com>
This commit is contained in:
Max Brooks 2025-04-28 13:01:16 -04:00 committed by GitHub
parent e81b9f58b5
commit 7120f9f2a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 69 additions and 24 deletions

View file

@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
namespace Elsa.Workflows.Serialization.Converters;
/// <summary>
/// Prevents System.Text.Json from trying to serialize the compiled delegate.
/// Always emits null and cannot rehydrate a Func.
/// </summary>
public class FuncExpressionValueConverter : JsonConverter<Func<ExpressionExecutionContext, ValueTask<object>>>
{
public override Func<ExpressionExecutionContext, ValueTask<object>> 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<ExpressionExecutionContext, ValueTask<object>> value, JsonSerializerOptions options)
{
// Emit a JSON null instead of trying to serialize the delegate
writer.WriteNullValue();
}
}

View file

@ -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<ExpressionExecutionContext, ValueTask<object>>);
public override JsonConverter CreateConverter(Type t, JsonSerializerOptions opts) => new FuncExpressionValueConverter();
}

View file

@ -10,9 +10,8 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer
/// <summary>
/// Initializes a new instance of the <see cref="ApiSerializer"/> class.
/// </summary>
public ApiSerializer(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public ApiSerializer(IServiceProvider serviceProvider)
: base(serviceProvider) { }
/// <inheritdoc />
public string Serialize(object model)
@ -41,6 +40,7 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer
protected override void AddConverters(JsonSerializerOptions options)
{
options.Converters.Add(CreateInstance<TypeJsonConverter>());
options.Converters.Add(CreateInstance<FuncExpressionValueConverter>());
}
JsonSerializerOptions IApiSerializer.GetOptions() => GetOptions();
@ -50,4 +50,4 @@ public class ApiSerializer : ConfigurableSerializer, IApiSerializer
ApplyOptions(options);
return options;
}
}
}

View file

@ -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());
}
/// <inheritdoc />
public T Deserialize<T>(string json) where T : notnull => JsonSerializer.Deserialize<T>(json, _settings)!;
public T Deserialize<T>(string json)
where T : notnull => JsonSerializer.Deserialize<T>(json, _settings)!;
/// <inheritdoc />
public object Deserialize(string json, Type type) => JsonSerializer.Deserialize(json, type, _settings)!;
/// <inheritdoc />
public string Serialize<T>(T payload) where T : notnull => JsonSerializer.Serialize(payload, payload.GetType(), _settings);
}
public string Serialize<T>(T payload)
where T : notnull => JsonSerializer.Serialize(payload, payload.GetType(), _settings);
}

View file

@ -37,5 +37,6 @@ public class JsonActivitySerializer(IServiceProvider serviceProvider) : Configur
options.Converters.Add(CreateInstance<InputJsonConverterFactory>());
options.Converters.Add(CreateInstance<OutputJsonConverterFactory>());
options.Converters.Add(CreateInstance<ExpressionJsonConverterFactory>());
options.Converters.Add(CreateInstance<FuncExpressionValueConverter>());
}
}
}

View file

@ -20,7 +20,7 @@ public class JsonPayloadSerializer : IPayloadSerializer
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
public string Serialize(object payload)
{
@ -82,8 +82,10 @@ public class JsonPayloadSerializer : IPayloadSerializer
options.Converters.Add(GetService<PolymorphicObjectConverterFactory>());
options.Converters.Add(GetService<TypeJsonConverter>());
options.Converters.Add(GetService<VariableConverterFactory>());
options.Converters.Add(new FuncExpressionValueConverter());
return options;
}
private T GetService<T>() where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance<T>(_serviceProvider);
}
private T GetService<T>()
where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance<T>(_serviceProvider);
}

View file

@ -20,7 +20,8 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat
/// <summary>
/// Initializes a new instance of the <see cref="JsonWorkflowStateSerializer"/> class.
/// </summary>
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() };
}
/// <inheritdoc />
@ -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());
}
}
}

View file

@ -13,9 +13,8 @@ namespace Elsa.Workflows.Serialization.Serializers;
public class SafeSerializer : ConfigurableSerializer, ISafeSerializer
{
/// <inheritdoc />
public SafeSerializer(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public SafeSerializer(IServiceProvider serviceProvider)
: base(serviceProvider) { }
/// <inheritdoc />
[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());
}
}
}