Update serialization and conversion functions (#5682)

* Update serialization and conversion functions

Updated JSON conversion to be able to use type aliases when saving serialized data to the DB.

* Simplify and update type alias registration.

Removed redundant XML comments and replaced IDictionary type aliases with Dictionary equivalents for consistency. These changes improve code readability and maintainability.

* Use collection initializer syntax

* Change "_type" values to "ObjectDictionary" in test files

Updated the "_type" field in various JSON test files from multiple different values to a unified "ObjectDictionary". This ensures consistency across the test files and aligns with the expected type format.

---------

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
This commit is contained in:
raymonddenhaan 2024-07-03 20:47:46 +02:00 committed by GitHub
parent 1be3b43c76
commit 6f36cfff0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 71 additions and 59 deletions

View file

@ -60,6 +60,11 @@ public class ActivityExecutionRecordRecord
/// </summary>
public string? SerializedException { get; set; }
/// <summary>
/// Any properties provided by the activity.
/// </summary>
public string? SerializedProperties { get; set; }
/// <summary>
/// Gets or sets the time at which the activity execution began.
/// </summary>

View file

@ -130,10 +130,11 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore
HasBookmarks = source.HasBookmarks,
Status = source.Status.ToString(),
ActivityTypeVersion = source.ActivityTypeVersion,
SerializedActivityState = source.ActivityState != null ? await _safeSerializer.SerializeAsync(source.ActivityState, cancellationToken) : default,
SerializedPayload = source.Payload != null ? await _safeSerializer.SerializeAsync(source.Payload, cancellationToken) : default,
SerializedOutputs = source.Outputs != null ? await _safeSerializer.SerializeAsync(source.Outputs, cancellationToken) : default,
SerializedException = source.Exception != null ? _payloadSerializer.Serialize(source.Exception) : default
SerializedActivityState = source.ActivityState != null ? await _safeSerializer.SerializeAsync(source.ActivityState, cancellationToken) : null,
SerializedPayload = source.Payload != null ? await _safeSerializer.SerializeAsync(source.Payload, cancellationToken) : null,
SerializedOutputs = source.Outputs?.Any() == true ? await _safeSerializer.SerializeAsync(source.Outputs, cancellationToken) : null,
SerializedException = source.Exception != null ? _payloadSerializer.Serialize(source.Exception) : null,
SerializedProperties = source.Properties.Any() ? await _safeSerializer.SerializeAsync(source.Properties, cancellationToken) : null
};
}
@ -155,7 +156,8 @@ public class DapperActivityExecutionRecordStore : IActivityExecutionStore
ActivityState = source.SerializedActivityState != null ? _payloadSerializer.Deserialize<IDictionary<string, object>>(source.SerializedActivityState) : default,
Payload = source.SerializedPayload != null ? await _safeSerializer.DeserializeAsync<IDictionary<string, object>>(source.SerializedPayload, cancellationToken) : default,
Outputs = source.SerializedOutputs != null ? await _safeSerializer.DeserializeAsync<IDictionary<string, object?>>(source.SerializedOutputs, cancellationToken) : default,
Exception = source.SerializedException != null ? _payloadSerializer.Deserialize<ExceptionState>(source.SerializedException) : default
Exception = source.SerializedException != null ? _payloadSerializer.Deserialize<ExceptionState>(source.SerializedException) : default,
Properties = source.SerializedProperties != null ? await _safeSerializer.DeserializeAsync<IDictionary<string, object>>(source.SerializedProperties, cancellationToken) : default
};
}

View file

@ -83,15 +83,15 @@ public class EFCoreActivityExecutionStore(
{
entity = entity.SanitizeLogMessage();
var compressionAlgorithm = options.Value.CompressionAlgorithm ?? nameof(None);
var serializedActivityState = entity.ActivityState != null ? await safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : default;
var compressedSerializedActivityState = serializedActivityState != null ? await compressionCodecResolver.Resolve(compressionAlgorithm).CompressAsync(serializedActivityState, cancellationToken) : default;
var serializedActivityState = entity.ActivityState != null ? await safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : null;
var compressedSerializedActivityState = serializedActivityState != null ? await compressionCodecResolver.Resolve(compressionAlgorithm).CompressAsync(serializedActivityState, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = compressedSerializedActivityState;
dbContext.Entry(entity).Property("SerializedActivityStateCompressionAlgorithm").CurrentValue = compressionAlgorithm;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs != null && entity.Outputs.Any() ? await safeSerializer.SerializeAsync(entity.Outputs, cancellationToken) : default;
dbContext.Entry(entity).Property("SerializedProperties").CurrentValue =entity.Properties.Any() ? payloadSerializer.Serialize(entity.Properties) : 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;
dbContext.Entry(entity).Property("SerializedOutputs").CurrentValue = entity.Outputs?.Any() == true ? await safeSerializer.SerializeAsync(entity.Outputs, cancellationToken) : null;
dbContext.Entry(entity).Property("SerializedProperties").CurrentValue = entity.Properties.Any() ? payloadSerializer.Serialize(entity.Properties) : null;
dbContext.Entry(entity).Property("SerializedException").CurrentValue = entity.Exception != null ? payloadSerializer.Serialize(entity.Exception) : null;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload?.Any() == true ? payloadSerializer.Serialize(entity.Payload) : null;
}
[RequiresUnreferencedCode("Calls Elsa.EntityFrameworkCore.Modules.Runtime.EFCoreActivityExecutionStore.DeserializeActivityState(RuntimeElsaDbContext, ActivityExecutionRecord, CancellationToken)")]

View file

@ -87,7 +87,7 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken)
{
entity = entity.SanitizeLogMessage();
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState != null ? await _safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : default;
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? await _safeSerializer.SerializeAsync(entity.ActivityState, cancellationToken) : default;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? await _safeSerializer.SerializeAsync(entity.Payload, cancellationToken) : default;
}

View file

@ -4,16 +4,12 @@ using Elsa.Extensions;
namespace Elsa.Expressions.Options;
/// <summary>
/// Options for the expression feature.
/// </summary>
public class ExpressionOptions
{
private readonly IDictionary<string, Type> _aliasTypeDictionary = new Dictionary<string, Type>();
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionOptions"/> class.
/// </summary>
public ExpressionOptions()
{
AliasTypeDictionary = new ReadOnlyDictionary<string, Type>(_aliasTypeDictionary);
@ -34,23 +30,16 @@ public class ExpressionOptions
this.AddTypeAlias<TimeSpan>("TimeSpan");
this.AddTypeAlias<ExpandoObject>("ExpandoObject");
this.AddTypeAlias<ExpandoObject>("JSON");
this.AddTypeAlias<IDictionary<string, string>>("StringDictionary");
this.AddTypeAlias<IDictionary<string, string>>("StringMap");
this.AddTypeAlias<IDictionary<string, object>>("ObjectMap");
this.AddTypeAlias<IDictionary<string, object>>("ObjectDictionary");
this.AddTypeAlias<Dictionary<string, string>>("StringDictionary");
this.AddTypeAlias<Dictionary<string, string>>("StringMap");
this.AddTypeAlias<Dictionary<string, object>>("ObjectMap");
this.AddTypeAlias<Dictionary<string, object>>("ObjectDictionary");
}
/// <summary>
/// Gets the type alias dictionary.
/// </summary>
public IDictionary<string, Type> AliasTypeDictionary { get; set; }
/// <summary>
/// Registers a well known type alias.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="alias">The alias.</param>
/// <returns>The options.</returns>
/// Registers a well-known type alias.
public ExpressionOptions RegisterTypeAlias(Type type, string alias)
{
_aliasTypeDictionary[alias] = type;

View file

@ -1,6 +1,7 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Activities.Flowchart.Serialization;
namespace Elsa.Workflows.Features;
@ -19,5 +20,11 @@ public class FlowchartFeature : FeatureBase
public override void Apply()
{
Services.AddSerializationOptionsConfigurator<FlowchartSerializationOptionConfigurator>();
}
public override void Configure()
{
Module.AddTypeAlias<FlowScope>("FlowScope");
}
}

View file

@ -1,5 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Contracts;
namespace Elsa.Workflows.Serialization.Converters;
@ -11,9 +12,9 @@ public class PolymorphicDictionaryConverter : JsonConverter<IDictionary<string,
private readonly JsonConverter<object> _objectConverter;
/// <inheritdoc />
public PolymorphicDictionaryConverter(JsonSerializerOptions options)
public PolymorphicDictionaryConverter(JsonSerializerOptions options, IWellKnownTypeRegistry wellKnownTypeRegistry)
{
var factory = (JsonConverterFactory)(options.Converters.FirstOrDefault(x => x is PolymorphicObjectConverterFactory) ?? new PolymorphicObjectConverterFactory());
var factory = (JsonConverterFactory)(options.Converters.FirstOrDefault(x => x is PolymorphicObjectConverterFactory) ?? new PolymorphicObjectConverterFactory(wellKnownTypeRegistry));
_objectConverter = (JsonConverter<object>)factory.CreateConverter(typeof(object), options)!;
}

View file

@ -1,10 +1,10 @@
using System.Collections;
using System.Dynamic;
using System.Reflection;
using System.Runtime;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Elsa.Expressions.Contracts;
using Elsa.Extensions;
using Elsa.Workflows.Serialization.ReferenceHandlers;
using Newtonsoft.Json.Linq;
@ -14,7 +14,7 @@ namespace Elsa.Workflows.Serialization.Converters;
/// <summary>
/// Reads objects as primitive types rather than <see cref="JsonElement"/> values while also maintaining the .NET type name for reconstructing the actual type.
/// </summary>
public class PolymorphicObjectConverter : JsonConverter<object>
public class PolymorphicObjectConverter(IWellKnownTypeRegistry wellKnownTypeRegistry) : JsonConverter<object>
{
private const string TypePropertyName = "_type";
private const string ItemsPropertyName = "_items";
@ -23,11 +23,6 @@ public class PolymorphicObjectConverter : JsonConverter<object>
private const string RefPropertyName = "$ref";
private const string ValuesPropertyName = "$values";
/// <inheritdoc />
public PolymorphicObjectConverter()
{
}
/// <inheritdoc />
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
@ -145,10 +140,9 @@ public class PolymorphicObjectConverter : JsonConverter<object>
}
else if (isHashSet)
{
addSetMethod.Invoke(collection, new[]
{
addSetMethod.Invoke(collection, [
deserializedElement
});
]);
}
else if (collection is IList list)
{
@ -250,13 +244,26 @@ public class PolymorphicObjectConverter : JsonConverter<object>
if (type != typeof(ExpandoObject))
{
if (shouldWriteTypeField)
writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName());
{
var typeOptions = newOptions.Clone();
typeOptions.Converters.RemoveWhere(c => c.GetType() != typeof(TypeJsonConverter));
if (typeOptions.Converters.Any())
{
var typeValue = JsonSerializer.Serialize(type, typeOptions).Trim('"');
writer.WriteString(TypePropertyName, typeValue);
}
else
{
writer.WriteString(TypePropertyName, type.GetSimpleAssemblyQualifiedName());
}
}
}
writer.WriteEndObject();
}
private static Type? ReadType(Utf8JsonReader reader)
private Type? ReadType(Utf8JsonReader reader)
{
reader.Read(); // Move to the first token inside the object.
string? typeName = null;
@ -298,7 +305,7 @@ public class PolymorphicObjectConverter : JsonConverter<object>
}
// If we found the _type property, attempt to resolve the type.
var targetType = typeName != null ? Type.GetType(typeName) : default;
var targetType = typeName != null ? wellKnownTypeRegistry.TryGetType(typeName, out var type) ? type : Type.GetType(typeName) : default;
return targetType;
}

View file

@ -1,13 +1,14 @@
using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Contracts;
namespace Elsa.Workflows.Serialization.Converters;
/// <summary>
/// A JSON converter factory that creates <see cref="PolymorphicObjectConverter"/> instances.
/// </summary>
public class PolymorphicObjectConverterFactory : JsonConverterFactory
public class PolymorphicObjectConverterFactory(IWellKnownTypeRegistry wellKnownTypeRegistry) : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
@ -24,8 +25,8 @@ public class PolymorphicObjectConverterFactory : JsonConverterFactory
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (typeof(IDictionary<string, object>).IsAssignableFrom(typeToConvert))
return new PolymorphicDictionaryConverter(options);
return new PolymorphicDictionaryConverter(options, wellKnownTypeRegistry);
return new PolymorphicObjectConverter();
return new PolymorphicObjectConverter(wellKnownTypeRegistry);
}
}

View file

@ -113,7 +113,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry));
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
options.Converters.Add(new PolymorphicObjectConverterFactory());
options.Converters.Add(new PolymorphicObjectConverterFactory(_wellKnownTypeRegistry));
options.Converters.Add(new TypeJsonConverter(_wellKnownTypeRegistry));
options.Converters.Add(new VariableConverterFactory(_wellKnownTypeRegistry, _loggerFactory));
}

View file

@ -4,5 +4,5 @@
"_island": "[\r\n {\r\n \u0022path\u0022: \u0022folder1\u0022,\r\n \u0022command\u0022: \u0022add\u0022\r\n }\r\n]",
"_type": "Newtonsoft.Json.Linq.JArray, Newtonsoft.Json"
},
"_type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -6,5 +6,5 @@
"command": "add"
}
],
"_type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -4,5 +4,5 @@
"_island": "{\r\n \u0022file1\u0022: {\r\n \u0022script\u0022: [\r\n {\r\n \u0022path\u0022: \u0022folder1\u0022,\r\n \u0022command\u0022: \u0022add\u0022\r\n }\r\n ]\r\n }\r\n}",
"_type": "Newtonsoft.Json.Linq.JObject, Newtonsoft.Json"
},
"_type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -10,5 +10,5 @@
]
}
},
"_type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -4,5 +4,5 @@
"_island": "[\r\n {\r\n \u0022path\u0022: \u0022folder1\u0022,\r\n \u0022command\u0022: \u0022add\u0022\r\n }\r\n]",
"_type": "System.Text.Json.Nodes.JsonArray, System.Text.Json"
},
"_type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -4,5 +4,5 @@
"_island": "{\r\n \u0022file1\u0022: {\r\n \u0022script\u0022: [\r\n {\r\n \u0022path\u0022: \u0022folder1\u0022,\r\n \u0022command\u0022: \u0022add\u0022\r\n }\r\n ]\r\n }\r\n}",
"_type": "System.Text.Json.Nodes.JsonObject, System.Text.Json"
},
"_type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -10,5 +10,5 @@
]
}
},
"_type": "System.Collections.Generic.Dictionary\u00602[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib"
"_type": "ObjectDictionary"
}

View file

@ -2,6 +2,7 @@ using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Elsa.Expressions.Services;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.Serialization.ReferenceHandlers;
using Xunit;
@ -74,7 +75,6 @@ public class Tests
private JsonSerializerOptions GetSerializerOptions()
{
var referenceHandler = new CrossScopedReferenceHandler();
var options = new JsonSerializerOptions
{
ReferenceHandler = referenceHandler,
@ -85,7 +85,7 @@ public class Tests
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
options.Converters.Add(new PolymorphicObjectConverterFactory());
options.Converters.Add(new PolymorphicObjectConverterFactory(new WellKnownTypeRegistry()));
return options;
}
}