Support serialization of non(de)serializable values (#4356)

This commit is contained in:
Sipke Schoorstra 2023-08-23 20:56:12 +02:00 committed by GitHub
parent 73482a8e4c
commit a02bacbbde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 121 additions and 23 deletions

View file

@ -23,7 +23,7 @@ public static partial class Extensions
return feature;
}
public static EFCoreWorkflowManagementPersistenceFeature UseMySql(this EFCoreWorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
public static WorkflowManagementPersistenceFeature UseMySql(this WorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaMySql(connectionString, options);
return feature;

View file

@ -23,7 +23,7 @@ public static partial class Extensions
return feature;
}
public static EFCoreWorkflowManagementPersistenceFeature UsePostgreSql(this EFCoreWorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
public static WorkflowManagementPersistenceFeature UsePostgreSql(this WorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaPostgreSql(connectionString, options);
return feature;

View file

@ -20,7 +20,7 @@ public static partial class Extensions
return feature;
}
public static EFCoreWorkflowManagementPersistenceFeature UseSqlServer(this EFCoreWorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
public static WorkflowManagementPersistenceFeature UseSqlServer(this WorkflowManagementPersistenceFeature feature, string connectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlServer(connectionString, options);
return feature;

View file

@ -21,7 +21,7 @@ public static partial class Extensions
return feature;
}
public static EFCoreWorkflowManagementPersistenceFeature UseSqlite(this EFCoreWorkflowManagementPersistenceFeature feature, string connectionString = Constants.DefaultConnectionString, ElsaDbContextOptions? options = default)
public static WorkflowManagementPersistenceFeature UseSqlite(this WorkflowManagementPersistenceFeature feature, string connectionString = Constants.DefaultConnectionString, ElsaDbContextOptions? options = default)
{
feature.DbContextOptionsBuilder = (_, db) => db.UseElsaSqlite(connectionString, options);
return feature;

View file

@ -28,7 +28,7 @@ public static class WorkflowManagementFeatureExtensions
/// <summary>
/// Sets up the EF Core persistence provider.
/// </summary>
public static WorkflowManagementFeature UseEntityFrameworkCore(this WorkflowManagementFeature feature, Action<EFCoreWorkflowManagementPersistenceFeature>? configure = default)
public static WorkflowManagementFeature UseEntityFrameworkCore(this WorkflowManagementFeature feature, Action<WorkflowManagementPersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;

View file

@ -15,10 +15,10 @@ namespace Elsa.EntityFrameworkCore.Modules.Management;
[DependsOn(typeof(WorkflowInstancesFeature))]
[DependsOn(typeof(WorkflowDefinitionsFeature))]
[PublicAPI]
public class EFCoreWorkflowManagementPersistenceFeature : PersistenceFeatureBase<ManagementElsaDbContext>
public class WorkflowManagementPersistenceFeature : PersistenceFeatureBase<ManagementElsaDbContext>
{
/// <inheritdoc />
public EFCoreWorkflowManagementPersistenceFeature(IModule module) : base(module)
public WorkflowManagementPersistenceFeature(IModule module) : base(module)
{
}

View file

@ -53,7 +53,7 @@ public class EFCoreActivityExecutionStore : IActivityExecutionStore
private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, ActivityExecutionRecord entity, CancellationToken cancellationToken)
{
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState != null ? (await _activityStateSerializer.SerializeAsync(entity.ActivityState, cancellationToken)).ToString() : default;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.ActivityState != null ? (await _activityStateSerializer.SerializeAsync(entity.Outputs, cancellationToken)).ToString() : default;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs != null ? (await _activityStateSerializer.SerializeAsync(entity.Outputs, cancellationToken)).ToString() : default;
dbContext.Entry(entity).Property("SerializedException").CurrentValue = entity.Exception != null ? _payloadSerializer.Serialize(entity.Exception) : default;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? _payloadSerializer.Serialize(entity.Payload) : default;
}

View file

@ -1,3 +1,4 @@
using System.Text.Json;
using Elsa.EntityFrameworkCore.Common;
using Elsa.Common.Models;
using Elsa.Extensions;
@ -16,15 +17,17 @@ namespace Elsa.EntityFrameworkCore.Modules.Runtime;
public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
{
private readonly EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> _store;
private readonly IPayloadSerializer _serializer;
private readonly IPayloadSerializer _payloadSerializer;
private readonly IActivityStateSerializer _activityStateSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="EFCoreWorkflowExecutionLogStore"/> class.
/// </summary>
public EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> store, IPayloadSerializer serializer)
public EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> store, IPayloadSerializer payloadPayloadSerializer, IActivityStateSerializer activityStateSerializer)
{
_store = store;
_serializer = serializer;
_payloadSerializer = payloadPayloadSerializer;
_activityStateSerializer = activityStateSerializer;
}
/// <inheritdoc />
@ -70,11 +73,10 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
return await _store.DeleteWhereAsync(queryable => Filter(queryable, filter), cancellationToken);
}
private ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken)
private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken)
{
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState != null ? _serializer.Serialize(entity.ActivityState) : default;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? _serializer.Serialize(entity.Payload) : default;
return default;
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState != null ? (await _activityStateSerializer.SerializeAsync(entity.ActivityState, cancellationToken)).ToString() : default;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? (await _activityStateSerializer.SerializeAsync(entity.Payload, cancellationToken)).ToString() : default;
}
private async ValueTask OnLoadAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity, CancellationToken cancellationToken)
@ -89,13 +91,13 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
private ValueTask<object?> LoadPayload(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity)
{
var json = dbContext.Entry(entity).Property<string>("SerializedPayload").CurrentValue;
return new(!string.IsNullOrEmpty(json) ? _serializer.Deserialize(json) : null);
return new(!string.IsNullOrEmpty(json) ? JsonSerializer.Deserialize<object>(json) : null);
}
private ValueTask<IDictionary<string, object>?> LoadActivityState(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity)
{
var json = dbContext.Entry(entity).Property<string>("SerializedActivityState").CurrentValue;
return new(!string.IsNullOrEmpty(json) ? _serializer.Deserialize<IDictionary<string, object>>(json) : null);
return new(!string.IsNullOrEmpty(json) ? JsonSerializer.Deserialize<IDictionary<string, object>>(json) : null);
}
private static IQueryable<WorkflowExecutionLogRecord> Filter(IQueryable<WorkflowExecutionLogRecord> queryable, WorkflowExecutionLogRecordFilter filter) => filter.Apply(queryable);

View file

@ -6,6 +6,7 @@ using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
namespace Elsa.Workflows.Core.Serialization.Converters;
@ -42,7 +43,16 @@ public class PolymorphicObjectConverter : JsonConverter<object>
var isEnumerable = typeof(IEnumerable).IsAssignableFrom(targetType);
if (!isEnumerable)
return JsonSerializer.Deserialize(ref reader, targetType, newOptions)!;
{
try
{
return JsonSerializer.Deserialize(ref reader, targetType, newOptions)!;
}
catch (NotSupportedException e)
{
return default!;
}
}
// If the target type is a Newtonsoft.JObject, parse the JSON island.
var isNewtonsoftObject = targetType == typeof(JObject);

View file

@ -0,0 +1,51 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// A JSON converter that safely serializes a dictionary of objects, even if some of the objects are not serializable.
/// In that case, the converter will serialize a fallback object that contains the type name of the original object.
/// </summary>
public class SafeDictionaryConverter : JsonConverter<IDictionary<string, object>>
{
/// <inheritdoc />
public override IDictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// For simplicity, I'm assuming you're using the default deserialization for dictionaries.
// If you need custom deserialization logic, you can implement it here.
return JsonSerializer.Deserialize<IDictionary<string, object>>(ref reader, options)!;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, IDictionary<string, object> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var kvp in value)
{
if (kvp.Value == null!) continue; // Skip null values
writer.WritePropertyName(kvp.Key);
try
{
// Serialize the value to a temporary string.
var serializedValue = JsonSerializer.Serialize(kvp.Value, options);
// Use the serialized string value to write to the main writer.
using var doc = JsonDocument.Parse(serializedValue);
doc.WriteTo(writer);
}
catch
{
// If serialization fails, write the fallback object.
writer.WriteStartObject();
writer.WriteString("TypeName", kvp.Value.GetType().FullName);
writer.WriteEndObject();
}
}
writer.WriteEndObject();
}
}

View file

@ -0,0 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Core.Serialization.Converters;
/// <summary>
/// A JSON converter factory that creates <see cref="SafeDictionaryConverter"/> instances.
/// </summary>
public class SafeDictionaryConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
// Check if the type is assignable to IDictionary<string, object>
return typeof(IDictionary<string, object>).IsAssignableFrom(typeToConvert);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (CanConvert(typeToConvert))
return new SafeDictionaryConverter();
throw new InvalidOperationException($"Cannot convert {typeToConvert}");
}
}

View file

@ -1,6 +1,8 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Elsa.Expressions.Services;
using Elsa.Extensions;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
@ -25,8 +27,13 @@ public class BasicSerializationProvider : ISerializationProvider
if (CanSerialize(value, out var jsonElement))
return new(jsonElement);
var type = JsonSerializer.SerializeToElement(value!.GetType().Name);
return new(type);
var unserializableTypeHolder = new JsonObject()
{
["UnserializableType"] = value!.GetType().GetSimpleAssemblyQualifiedName()
};
var serializedTypeHolder = JsonSerializer.SerializeToElement(unserializableTypeHolder);
return new(serializedTypeHolder);
}
private static bool CanSerialize(object? obj, out JsonElement jsonElement)
@ -44,19 +51,21 @@ public class BasicSerializationProvider : ISerializationProvider
jsonElement = JsonSerializer.SerializeToElement(obj, options);
return true;
}
catch (JsonException)
catch (Exception)
{
jsonElement = default;
return false;
}
}
private static JsonSerializerOptions CreateOptions() => new()
{
ReferenceHandler = ReferenceHandler.Preserve,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault())
new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault()),
new SafeDictionaryConverterFactory()
}
};
}