Add ExpandoObjectConverter for JavaScript object conversion

Introduced `ConverterHelper` and `ExpandoObjectConverter` to handle conversions from .NET dynamic objects to JavaScript objects. Updated integration tests to validate the conversion and sorting functionalities.

Fixes #5895
This commit is contained in:
Sipke Schoorstra 2024-08-13 19:58:18 +02:00
parent 0a9d4102c9
commit 71d56e1665
5 changed files with 146 additions and 14 deletions

View file

@ -799,6 +799,7 @@ Global
{F50336DA-42D1-4DD1-A107-67AFEB8A33EE}.Release|Any CPU.Build.0 = Release|Any CPU
{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99F2B1DA-2F69-4D70-A2A3-AC985AD91EC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBCE36D1-6767-4ED1-B3E8-84D2567A962A}.Release|Any CPU.ActiveCfg = Release|Any CPU

View file

@ -0,0 +1,59 @@
using System.Collections;
using Elsa.Extensions;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
namespace Elsa.JavaScript.Helpers;
internal static class ConverterHelper
{
public static ObjectInstance ConvertToJsObject(Engine engine, IDictionary<string, object?> expando)
{
var jsObject = engine.Intrinsics.Object.Construct([]);
foreach (var kvp in expando)
{
var value = kvp.Value;
var jsValue = ConvertToJsValue(engine, value);
var propertyDescriptor = new PropertyDescriptor(jsValue, true, true, true);
jsObject.DefineOwnProperty(kvp.Key, propertyDescriptor);
}
return jsObject;
}
private static JsValue ConvertToJsValue(Engine engine, object? value)
{
if (value == null)
return JsValue.Null;
if (value is IDictionary<string, object?> dict)
return ConvertToJsObject(engine, dict);
var valueType = value.GetType();
if (valueType.IsCollectionType())
{
var list = (ICollection)value;
var jsArray = engine.Intrinsics.Array.Construct(list.Count);
var index = 0;
foreach (var item in list)
jsArray.Set(index++, ConvertToJsValue(engine, item), true);
return jsArray;
}
if (value is string str)
return JsValue.FromObject(engine, str);
if (value is int or double or float or decimal)
return JsValue.FromObject(engine, Convert.ToDouble(value));
if (value is bool b)
return JsValue.FromObject(engine, b);
return JsValue.FromObject(engine, value);
}
}

View file

@ -0,0 +1,22 @@
using System.Dynamic;
using Elsa.JavaScript.Helpers;
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
namespace Elsa.JavaScript.ObjectConverters;
internal class ExpandoObjectConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
if (value is ExpandoObject expandoObject)
{
result = ConverterHelper.ConvertToJsObject(engine, expandoObject);
return true;
}
result = JsValue.Null;
return false;
}
}

View file

@ -70,7 +70,7 @@ public class JintJavaScriptEvaluator(IConfiguration configuration, INotification
return instance;
});
opts.Interop.ObjectConverters.Add(new ByteArrayConverter());
opts.Interop.ObjectConverters.AddRange([new ByteArrayConverter(), new ExpandoObjectConverter()]);
});
configureEngine?.Invoke(engine);

View file

@ -11,40 +11,50 @@ namespace Elsa.Workflows.IntegrationTests.Scenarios.JavaScriptListsAndArrays;
public class Tests
{
private readonly IServiceProvider _services;
private readonly IJavaScriptEvaluator _evaluator;
private readonly ExpressionExecutionContext _expressionContext;
public Tests(ITestOutputHelper testOutputHelper)
{
var testOutputHelper1 = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper));
_services = new TestApplicationBuilder(testOutputHelper1).Build();
_evaluator = _services.GetRequiredService<IJavaScriptEvaluator>();
_expressionContext = new ExpressionExecutionContext(_services, new MemoryRegister());
var services = new TestApplicationBuilder(testOutputHelper1).Build();
_evaluator = services.GetRequiredService<IJavaScriptEvaluator>();
_expressionContext = new ExpressionExecutionContext(services, new MemoryRegister());
}
[Fact(DisplayName = "Workflow inputs containing .NET lists on dynamic objects are converted to arrays for use in JavaScript.")]
public async Task Test1()
{
dynamic dynamicObject = new ExpandoObject();
dynamicObject.List = new List<string> { "a", "b", "c" };
dynamicObject.List = new List<string>
{
"a",
"b",
"c"
};
var script = "getObj().List.filter(x => x === 'b').length === 1";
_expressionContext.SetVariable("obj", (object)dynamicObject);
var result = await _evaluator.EvaluateAsync(script, typeof(bool), _expressionContext);
Assert.True((bool)result!);
}
[Fact(DisplayName = "Can access list properties as arrays")]
public async Task Test2()
{
var person = new ExpandoObject();
var magicNumbers = new[]{42, 43, 44};
var magicNumbers = new[]
{
42, 43, 44
};
var order1 = new ExpandoObject();
order1.TryAdd("magicNumbers", magicNumbers);
person.TryAdd("orders", new []{ order1 });
person.TryAdd("orders", new[]
{
order1
});
person.TryAdd("name", "John");
person.TryAdd("age", 12);
@ -57,8 +67,11 @@ public class Tests
person.TryAdd("languages", languages);
var obj = new ExpandoObject();
obj.TryAdd("persons", new List<object> { person });
obj.TryAdd("persons", new List<object>
{
person
});
var name = await _evaluator.EvaluateAsync("o.persons.filter(x => x.age == 12)[0].name", typeof(string), _expressionContext, configureEngine: engine => engine.SetValue("o", obj));
var language = await _evaluator.EvaluateAsync("o.persons[0].languages.filter(x => x == 'English')[0]", typeof(string), _expressionContext, configureEngine: engine => engine.SetValue("o", obj));
var magicNumber = await _evaluator.EvaluateAsync("o.persons[0].orders[0].magicNumbers[1]", typeof(int), _expressionContext, configureEngine: engine => engine.SetValue("o", obj));
@ -66,4 +79,41 @@ public class Tests
Assert.Equal("English", language);
Assert.Equal(43, magicNumber);
}
[Theory(DisplayName = "Can sort array and list properties as mutable arrays")]
[MemberData(nameof(ArraySortEnumerableData))]
// See also: https://github.com/sebastienros/jint/issues/1942
public async Task Test5(IEnumerable<double> collection)
{
dynamic dynamicObject = new ExpandoObject();
dynamicObject.Items = collection;
var script = """
const model = getModel();
model.Items.sort((a, b) => a - b);
return model;
""";
_expressionContext.SetVariable("Model", (object)dynamicObject);
dynamicObject = (ExpandoObject)(await _evaluator.EvaluateAsync(script, typeof(ExpandoObject), _expressionContext))!;
var items = ((object[])dynamicObject.Items).Cast<double>().ToArray();
Assert.Equal([2, 4, 8], items);
}
public static IEnumerable<object[]> ArraySortEnumerableData = new object[][]
{
[
new double[]
{
8, 4, 2
}
],
[
new List<double>
{
8,
4,
2
}
]
};
}