Add byte array converter and enhance HTTP file download

A new ByteArrayConverter class has been added to Elsa.JavaScript that converts byte arrays to Uint8Arrays. In Elsa.Http, the DownloadHttpFile activity now also stores the downloaded file's content in bytes. In addition, various new type aliases have been added to the Elsa.Http and Elsa.JavaScript modules, making it easier to handle non-string responses and the transfer of data.
This commit is contained in:
Sipke Schoorstra 2024-06-14 22:46:47 +02:00
parent 43432cb70d
commit 615f30f0eb
7 changed files with 85 additions and 34 deletions

View file

@ -46,7 +46,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
UIHint = InputUIHints.DropDown
)]
public Input<string> Method { get; set; } = new("GET");
/// <summary>
/// A list of expected status codes to handle.
/// </summary>
@ -102,7 +102,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
/// </summary>
[Output(IsSerializable = false)]
public Output<HttpResponseMessage> Response { get; set; } = default!;
/// <summary>
/// The HTTP response status code
/// </summary>
@ -110,17 +110,23 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
public Output<int> StatusCode { get; set; } = default!;
/// <summary>
/// The parsed content, if any.
/// The downloaded content stream, if any.
/// </summary>
[Output(Description = "The parsed content, if any.", IsSerializable = false)]
public Output<Stream?> ResponseContent { get; set; } = default!;
[Output(Description = "The downloaded content stream, if any.", IsSerializable = false)]
public Output<Stream?> ResponseContentStream { get; set; } = default!;
/// <summary>
/// The downloaded content bytes, if any.
/// </summary>
[Output(Description = "The downloaded content bytes, if any.", IsSerializable = false)]
public Output<byte[]?> ResponseContentBytes { get; set; } = default!;
/// <summary>
/// The response headers that were received.
/// </summary>
[Output(Description = "The response headers that were received.")]
public Output<HttpHeaders?> ResponseHeaders { get; set; } = default!;
/// <summary>
/// The response content headers that were received.
/// </summary>
@ -150,11 +156,12 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
var responseContentHeaders = new HttpHeaders(response.Content.Headers);
context.Set(Response, response);
context.Set(ResponseContent, file?.Stream);
context.Set(ResponseContentStream, file?.Stream);
context.Set(Result, file);
context.Set(StatusCode, statusCode);
context.Set(ResponseHeaders, responseHeaders);
context.Set(ResponseContentHeaders, responseContentHeaders);
if (ResponseContentBytes.HasTarget(context)) context.Set(ResponseContentBytes, file?.GetBytes());
await HandleResponseAsync(context, response);
}
@ -179,7 +186,7 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
await HandleTaskCanceledExceptionAsync(context, e);
}
}
/// <summary>
/// Handles the response.
/// </summary>
@ -272,11 +279,14 @@ public class DownloadHttpFile : Activity<HttpFile>, IActivityPropertyDefaultValu
var parsedContentType = new System.Net.Mime.ContentType(contentType);
return factories.FirstOrDefault(httpContentFactory => httpContentFactory.SupportedContentTypes.Any(c => c == parsedContentType.MediaType)) ?? new JsonContentFactory();
}
object IActivityPropertyDefaultValueProvider.GetDefaultValue(PropertyInfo property)
{
if (property.Name == nameof(ExpectedStatusCodes))
return new List<int> { 200 };
return new List<int>
{
200
};
return default!;
}

View file

@ -11,7 +11,7 @@ namespace Elsa.Http.DownloadableContentHandlers;
public class MultiDownloadableContentHandler : DownloadableContentHandlerBase
{
/// <inheritdoc />
public override bool GetSupportsContent(object content) => content is IEnumerable enumerable and not string;
public override bool GetSupportsContent(object content) => content is IEnumerable and not string and not byte[];
/// <inheritdoc />
protected override IEnumerable<Func<ValueTask<Downloadable>>> GetDownloadablesAsync(DownloadableContext context)

View file

@ -219,6 +219,10 @@ public class HttpFeature : FeatureBase
{
options.AddTypeAlias<IFormFile>("FormFile");
options.AddTypeAlias<IFormFile[]>("FormFile[]");
options.AddTypeAlias<HttpFile>("HttpFile");
options.AddTypeAlias<HttpFile[]>("HttpFile[]");
options.AddTypeAlias<Downloadable>("Downloadable");
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
});
}
}

View file

@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
namespace Elsa.JavaScript.ObjectConverters;
/// <summary>
/// Converts a byte array to a <see cref="JsValue"/> instance representing a Uint8Array.
/// </summary>
internal class ByteArrayConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, [NotNullWhen(true)] out JsValue? result)
{
if (value is byte[] bytes)
{
// TODO: Temporary: Uint8Array creates a copy of the byte array. Instead, we want to create a view or a buffer referencing the byte array.
// See also: https://github.com/sebastienros/jint/pull/1590
result = engine.Intrinsics.Uint8Array.Construct(bytes);
return true;
}
result = JsValue.Null;
return false;
}
}

View file

@ -12,10 +12,13 @@ using Elsa.Extensions;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.Helpers;
using Elsa.JavaScript.Notifications;
using Elsa.JavaScript.ObjectConverters;
using Elsa.JavaScript.Options;
using Elsa.Mediator.Contracts;
using Humanizer;
using Jint;
using Jint.Native;
using Jint.Native.TypedArray;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
@ -27,23 +30,11 @@ namespace Elsa.JavaScript.Services;
/// <summary>
/// Provides a JavaScript evaluator using Jint.
/// </summary>
public class JintJavaScriptEvaluator : IJavaScriptEvaluator
public class JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
: IJavaScriptEvaluator
{
private readonly INotificationSender _mediator;
private readonly IMemoryCache _memoryCache;
private readonly JintOptions _jintOptions;
private readonly IConfiguration _configuration;
/// <summary>
/// Constructor.
/// </summary>
public JintJavaScriptEvaluator(IConfiguration configuration, INotificationSender mediator, IOptions<JintOptions> scriptOptions, IMemoryCache memoryCache)
{
_mediator = mediator;
_memoryCache = memoryCache;
_jintOptions = scriptOptions.Value;
_configuration = configuration;
}
private readonly JintOptions _jintOptions = scriptOptions.Value;
private readonly JsonSerializerOptions _jsonSerializerOptions = CreateJsonSerializerOptions();
/// <inheritdoc />
public async Task<object?> EvaluateAsync(string expression,
@ -71,13 +62,15 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
// Wrap objects in ObjectWrapper instances and set their prototype to Array.prototype if they are array-like.
opts.SetWrapObjectHandler((engine, target, type) =>
{
var instance = new ObjectWrapper(engine, target);
var instance = ObjectWrapper.Create(engine, target);
if (ObjectArrayHelper.DetermineIfObjectIsArrayLikeClrCollection(target.GetType()))
instance.Prototype = engine.Intrinsics.Array.PrototypeObject;
return instance;
});
opts.Interop.ObjectConverters.Add(new ByteArrayConverter());
});
configureEngine?.Invoke(engine);
@ -123,7 +116,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
// Create configuration value accessor
if (_jintOptions.AllowConfigurationAccess)
engine.SetValue("getConfig", (Func<string, object?>)(name => _configuration.GetSection(name).Value));
engine.SetValue("getConfig", (Func<string, object?>)(name => configuration.GetSection(name).Value));
// Add common .NET types.
engine.RegisterType<DateTime>();
@ -135,7 +128,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
_jintOptions.ConfigureEngineCallback(engine, context);
// Allow listeners invoked by the mediator to configure the engine.
await _mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
await mediator.SendAsync(new EvaluatingJavaScript(engine, context), cancellationToken);
return engine;
}
@ -159,6 +152,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
}
}
[RequiresUnreferencedCode("Calls Jint.Engine.SetValue<T>(String, T)")]
private static void CreateVariableAccessors(Engine engine, ExpressionExecutionContext context)
{
var variableNames = context.GetVariableNamesInScope().ToList();
@ -184,7 +178,7 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
{
var cacheKey = "jint:script:" + Hash(expression);
var parsedScript = _memoryCache.GetOrCreate(cacheKey, entry =>
var parsedScript = memoryCache.GetOrCreate(cacheKey, entry =>
{
if (_jintOptions.ScriptCacheTimeout.HasValue)
entry.SetAbsoluteExpiration(_jintOptions.ScriptCacheTimeout.Value);
@ -204,15 +198,19 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
}
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
private static string Serialize(object value)
private string Serialize(object value)
{
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
}
private static JsonSerializerOptions CreateJsonSerializerOptions()
{
var options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
options.Converters.Add(new JsonStringEnumConverter());
return JsonSerializer.Serialize(value, options);
return options;
}
private string Hash(string input)

View file

@ -24,10 +24,14 @@ public class TypeAliasRegistry : ITypeAliasRegistry
this.RegisterType<decimal>("Decimal");
this.RegisterType<float>("Single");
this.RegisterType<double>("Double");
this.RegisterType<byte[]>("Buffer");
this.RegisterType<Stream>("Stream");
this.RegisterType<Guid>("Guid");
this.RegisterType<DateTime>("Date");
this.RegisterType<DateTimeOffset>("Date");
this.RegisterType<DateOnly>("Date");
this.RegisterType<TimeOnly>("Date");
this.RegisterType<IDictionary<string, object>>("ObjectDictionary");
}
/// <inheritdoc />

View file

@ -53,4 +53,13 @@ public static class OutputExtensions
var parsedContentVariableType = (memoryBlock.Metadata as VariableBlockMetadata)?.Variable.GetType();
return parsedContentVariableType?.GenericTypeArguments.FirstOrDefault();
}
/// <summary>
/// Returns a value indicating whether the output has a target.
/// </summary>
public static bool HasTarget(this Output? output, ActivityExecutionContext context)
{
var memoryBlockReference = output?.MemoryBlockReference();
return memoryBlockReference is not null && context.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out _);
}
}