Support serialization of non(de)serializable values (#4356)
This commit is contained in:
parent
73482a8e4c
commit
a02bacbbde
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue