diff --git a/Directory.Packages.props b/Directory.Packages.props index f070fd1cd..d3e0f5472 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -177,11 +177,12 @@ - - + + + \ No newline at end of file diff --git a/build/_build.csproj b/build/_build.csproj index 987e45789..4617e6f76 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -19,16 +19,16 @@ - + - + diff --git a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs index 7d01f8d65..a4408e13c 100644 --- a/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs +++ b/src/modules/Elsa.MongoDb/Features/MongoDbFeature.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using Elsa.Features.Abstractions; using Elsa.Features.Services; using Elsa.KeyValues.Entities; @@ -8,6 +9,7 @@ using Elsa.MongoDb.Options; using Elsa.MongoDb.Serializers; using Elsa.Workflows.Memory; using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -67,6 +69,7 @@ public class MongoDbFeature : FeatureBase TryRegisterSerializerOrSkipWhenExist(typeof(Variable), new VariableSerializer()); TryRegisterSerializerOrSkipWhenExist(typeof(Version), new VersionSerializer()); TryRegisterSerializerOrSkipWhenExist(typeof(JsonElement), new JsonElementSerializer()); + TryRegisterSerializerOrSkipWhenExist(typeof(JsonNode), new JsonNodeBsonConverter()); } private static void RegisterClassMaps() diff --git a/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs b/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs new file mode 100644 index 000000000..51d793298 --- /dev/null +++ b/src/modules/Elsa.MongoDb/Serializers/JsonNodeBsonSerializer.cs @@ -0,0 +1,135 @@ +using System.Text.Json.Nodes; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; + +namespace Elsa.MongoDb.Serializers; + +/// +/// Serializes a . +/// +public class JsonNodeBsonConverter : IBsonSerializer +{ + /// + public Type ValueType => typeof(JsonNode); + + /// + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, JsonNode value) + { + if (value == null!) + { + context.Writer.WriteNull(); + return; + } + + context.Writer.WriteStartDocument(); + context.Writer.WriteName("type"); + + switch (value) + { + case JsonObject jsonObject: + context.Writer.WriteString("JsonObject"); + context.Writer.WriteName("value"); + context.Writer.WriteString(jsonObject.ToJsonString()); + break; + + case JsonArray jsonArray: + context.Writer.WriteString("JsonArray"); + context.Writer.WriteName("value"); + context.Writer.WriteString(jsonArray.ToJsonString()); + break; + + case JsonValue jsonValue: + context.Writer.WriteString("JsonValue"); + context.Writer.WriteName("value"); + if (jsonValue.TryGetValue(out string? stringValue)) + context.Writer.WriteString(stringValue); + else if (jsonValue.TryGetValue(out int intValue)) + context.Writer.WriteInt32(intValue); + else if (jsonValue.TryGetValue(out int longValue)) + context.Writer.WriteInt64(longValue); + else if (jsonValue.TryGetValue(out double doubleValue)) + context.Writer.WriteDouble(doubleValue); + else if (jsonValue.TryGetValue(out bool boolValue)) + context.Writer.WriteBoolean(boolValue); + else if (jsonValue.TryGetValue(out DateTimeOffset dateTimeOffsetValue)) + context.Writer.WriteDateTime(dateTimeOffsetValue.ToUnixTimeMilliseconds()); + else if (jsonValue.TryGetValue(out DateTime dateTimeValue)) + context.Writer.WriteDateTime(new DateTimeOffset(dateTimeValue).ToUnixTimeMilliseconds()); + else + throw new BsonSerializationException("Unsupported JsonValue type"); + break; + + default: + throw new BsonSerializationException($"Unexpected JsonNode type: {value.GetType()}"); + } + + context.Writer.WriteEndDocument(); + } + + public JsonNode Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + context.Reader.ReadStartDocument(); + var type = context.Reader.ReadString(); + context.Reader.ReadName(Utf8NameDecoder.Instance); + + JsonNode result; + switch (type) + { + case "JsonObject": + var jsonObjectString = context.Reader.ReadString(); + result = JsonNode.Parse(jsonObjectString); + break; + + case "JsonArray": + var jsonArrayString = context.Reader.ReadString(); + result = JsonNode.Parse(jsonArrayString); + break; + + case "JsonValue": + var bsonType = context.Reader.GetCurrentBsonType(); + switch (bsonType) + { + case BsonType.String: + result = JsonValue.Create(context.Reader.ReadString()); + break; + case BsonType.Int32: + result = JsonValue.Create(context.Reader.ReadInt32()); + break; + case BsonType.Int64: + result = JsonValue.Create(context.Reader.ReadInt64()); + break; + case BsonType.Double: + result = JsonValue.Create(context.Reader.ReadDouble()); + break; + case BsonType.Boolean: + result = JsonValue.Create(context.Reader.ReadBoolean()); + break; + case BsonType.DateTime: + result = JsonValue.Create(context.Reader.ReadDateTime()); + break; + default: + throw new BsonSerializationException($"Unsupported BSON type: {bsonType}"); + } + break; + + default: + throw new BsonSerializationException($"Unsupported JsonNode type: {type}"); + } + + context.Reader.ReadEndDocument(); + return result!; + } + + /// + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + Serialize(context, args, (JsonNode)value); + } + + object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + return Deserialize(context, args); + } +} + diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs index d3d04873b..eedd35128 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Export/Endpoint.cs @@ -72,7 +72,7 @@ internal class Export : ElsaEndpoint foreach (var definition in definitions) { var model = await CreateWorkflowModelAsync(definition, cancellationToken); - var binaryJson = SerializeWorkflowDefinition(model); + var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken); var fileName = GetFileName(model); var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal); await using var entryStream = entry.Open(); @@ -101,7 +101,7 @@ internal class Export : ElsaEndpoint } var model = await CreateWorkflowModelAsync(definition, cancellationToken); - var binaryJson = SerializeWorkflowDefinition(model); + var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken); var fileName = GetFileName(model); await SendBytesAsync(binaryJson, fileName, cancellation: cancellationToken); @@ -115,10 +115,25 @@ internal class Export : ElsaEndpoint return fileName; } - private byte[] SerializeWorkflowDefinition(WorkflowDefinitionModel model) + private async Task SerializeWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken) { - JsonSerializerOptions serializerOptions = _serializer.GetOptions(); - var binaryJson = JsonSerializer.SerializeToUtf8Bytes(model, serializerOptions); + var serializerOptions = _serializer.GetOptions(); + var document = JsonSerializer.SerializeToDocument(model, serializerOptions); + var rootElement = document.RootElement; + + using var output = new MemoryStream(); + await using var writer = new Utf8JsonWriter(output); + + writer.WriteStartObject(); + writer.WriteString("$schema", "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json"); + + foreach (var property in rootElement.EnumerateObject()) + property.WriteTo(writer); + + writer.WriteEndObject(); + + await writer.FlushAsync(cancellationToken); + var binaryJson = output.ToArray(); return binaryJson; } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs b/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs index 6a5404312..da96c7e30 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Configurators/AdditionalConvertersConfigurator.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Elsa.Workflows.Serialization.Converters; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Serialization.Configurators; @@ -8,23 +9,14 @@ namespace Elsa.Workflows.Serialization.Configurators; /// /// Add additional objects. /// -public class AdditionalConvertersConfigurator : SerializationOptionsConfiguratorBase +[UsedImplicitly] +public class AdditionalConvertersConfigurator(IServiceProvider serviceProvider) : SerializationOptionsConfiguratorBase { - private readonly IServiceProvider _serviceProvider; - - /// - /// Initializes a new instance of the class. - /// - public AdditionalConvertersConfigurator(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - /// public override void Configure(JsonSerializerOptions options) { options.Converters.Add(Create()); } - private T Create() => ActivatorUtilities.CreateInstance(_serviceProvider); + private T Create() => ActivatorUtilities.CreateInstance(serviceProvider); } \ No newline at end of file