From 0560d11432646da012cb090a62ccdfbd00df1297 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 10:50:42 +0100 Subject: [PATCH 1/2] Improve JSON array conversion in ObjectConverter Refactored ObjectConverter to handle JSON array conversions more robustly, including support for arrays of complex types. Added a `Person` class for unit testing and updated tests to validate the new functionality. Included necessary project reference updates to ensure proper functionality. --- .../Elsa.Expressions/Elsa.Expressions.csproj | 1 + .../Helpers/ObjectConverter.cs | 28 ++++++++++++++----- .../ObjectConversion/Person.cs | 9 ++++++ .../ObjectConversion/Tests.cs | 17 +++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs diff --git a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj index d696bff63..66d1a6e5e 100644 --- a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj +++ b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj @@ -15,6 +15,7 @@ + diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index f59e9861c..899a84ddc 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -102,15 +102,29 @@ public static class ObjectConverter return jsonElement.Deserialize(targetType, serializerOptions); } - if (value is JsonNode jsonNode and not JsonArray) // If the value is a JsonNode, we can convert it to the target type. If it's a JsonArray, we let the enumerable conversion logic handle it. + if (value is JsonNode jsonNode) { - return underlyingTargetType switch + if (jsonNode is not JsonArray jsonArray) { - { } t when t == typeof(string) => jsonNode.ToString(), - { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()), - { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), - _ => jsonNode - }; + return underlyingTargetType switch + { + { } t when t == typeof(string) => jsonNode.ToString(), + { } t when t == typeof(ExpandoObject) && jsonNode.GetValueKind() == JsonValueKind.Object => JsonSerializer.Deserialize(jsonNode.ToJsonString()), + { } t when t != typeof(object) || converterOptions?.DeserializeJsonObjectToObject == true => jsonNode.Deserialize(targetType, serializerOptions), + _ => jsonNode + }; + } + + // Convert to target type if target type is an array or a generic collection. + if (targetType.IsArray || targetType.IsCollectionType()) + { + // The element type of the source array is JsonObject. If the element type of the target array is Object then return the source array as an array of JsonObjects. + // Deserializing normally would return an array of JsonElement instead of JsonObject, but we want to keep JsonObject elements: + var targetElementType = targetType.IsArray ? targetType.GetElementType()! : targetType.GenericTypeArguments[0]; + + if (targetElementType != typeof(object)) + return jsonArray.Deserialize(targetType, serializerOptions); + } } if (underlyingSourceType == typeof(string) && !underlyingTargetType.IsPrimitive && underlyingTargetType != typeof(object)) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs new file mode 100644 index 000000000..b5583db8b --- /dev/null +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Person.cs @@ -0,0 +1,9 @@ +namespace Elsa.Workflows.Core.UnitTests.ObjectConversion +{ + public class Person + { + public double? Age { get; set; } + + public string? Name { get; set; } + } +} diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs index 6525f5d0e..c245cb5cc 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/ObjectConversion/Tests.cs @@ -283,4 +283,21 @@ public class Tests Assert.Equal("Bob", secondElement["name"]?.ToString()); Assert.Equal("25", secondElement["age"]?.ToString()); } + + [Fact] + public void ConvertFrom_JsonArrayToArrayOfComplextType_ReturnsArrayOfComplexType() + { + // Arrange + var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]"; + var options = new ObjectConverterOptions(); + + // Act + var result = jsonArrayString.ConvertTo(options); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Length); + Assert.Equal("Alice", result[0].Name); + Assert.Equal("Bob", result[1].Name); + } } \ No newline at end of file From 4d7b551d79122c21bcc5607fd15ebd5251b57b31 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 29 Jan 2025 11:24:13 +0100 Subject: [PATCH 2/2] Add support for ExpandoObject attachments in email sending This update enables handling ExpandoObject as email attachments by extracting relevant properties like FileName, ContentType, and Content. It ensures proper parsing of the content and supports both byte arrays and streams for attachment data. --- src/modules/Elsa.Email/Activities/SendEmail.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/modules/Elsa.Email/Activities/SendEmail.cs b/src/modules/Elsa.Email/Activities/SendEmail.cs index 4ec9ca02d..db68773ca 100644 --- a/src/modules/Elsa.Email/Activities/SendEmail.cs +++ b/src/modules/Elsa.Email/Activities/SendEmail.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Dynamic; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -183,6 +184,22 @@ public class SendEmail : Activity else if (emailAttachment.Content is Stream stream) await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); + break; + } + case ExpandoObject expandoObject: + { + var dictionary = new Dictionary(expandoObject, StringComparer.OrdinalIgnoreCase); + var fileName = dictionary.GetValue("FileName") ?? $"Attachment-{++index}"; + var contentType = dictionary.GetValue("ContentType") ?? "application/binary"; + var parsedContentType = ContentType.Parse(contentType); + var content = dictionary.GetValue("Content"); + + if (content is byte[] bytes) + bodyBuilder.Attachments.Add(fileName, bytes, parsedContentType); + + else if (content is Stream stream) + await bodyBuilder.Attachments.AddAsync(fileName, stream, parsedContentType, cancellationToken); + break; } default: