Implement support for HTTP file download for SendHttpRequest and WriteHttpResponse

This commit is contained in:
Sipke Schoorstra 2021-06-15 19:47:46 +02:00
parent c6a32d49e4
commit ebaeabb934
23 changed files with 170 additions and 124 deletions

View file

@ -29,11 +29,11 @@ namespace Elsa.Activities.Http
public class SendHttpRequest : Activity
{
private readonly HttpClient _httpClient;
private readonly IEnumerable<IHttpResponseBodyParser> _parsers;
private readonly IEnumerable<IHttpResponseContentReader> _parsers;
public SendHttpRequest(
IHttpClientFactory httpClientFactory,
IEnumerable<IHttpResponseBodyParser> parsers)
IEnumerable<IHttpResponseContentReader> parsers)
{
_httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest));
_parsers = parsers;
@ -68,7 +68,7 @@ namespace Elsa.Activities.Http
[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
Hint = "The content type to send with the request.",
Options = new[] { "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? ContentType { get; set; }
@ -102,7 +102,7 @@ namespace Elsa.Activities.Http
)]
public ICollection<int>? SupportedStatusCodes { get; set; } = new HashSet<int>(new[] { 200 });
[ActivityOutput] public HttpResponseModel Output { get; set; }
[ActivityOutput] public HttpResponseModel? Output { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
@ -122,8 +122,8 @@ namespace Elsa.Activities.Http
if (hasContent && ReadContent)
{
var formatter = SelectContentParser(contentType!);
responseModel.Content = await formatter.ParseAsync(response, cancellationToken);
var formatter = SelectContentParser(contentType);
responseModel.Content = await formatter.ReadAsync(response, cancellationToken);
}
var statusCode = (int) response.StatusCode;
@ -139,13 +139,12 @@ namespace Elsa.Activities.Http
return Outcomes(outcomes);
}
private IHttpResponseBodyParser SelectContentParser(string contentType)
private IHttpResponseContentReader SelectContentParser(string? contentType)
{
var simpleContentType = contentType?.Split(';').First();
var simpleContentType = contentType?.Split(';').First() ?? "";
var formatters = _parsers.OrderByDescending(x => x.Priority).ToList();
return formatters.FirstOrDefault(
x => x.SupportedContentTypes.Contains(simpleContentType, StringComparer.OrdinalIgnoreCase)
) ?? formatters.Last();
return formatters.FirstOrDefault(x => x.GetSupportsContentType(simpleContentType)) ?? formatters.Last();
}
private HttpRequestMessage CreateRequest()
@ -153,7 +152,7 @@ namespace Elsa.Activities.Http
var method = Method ?? HttpMethods.Get;
var methodSupportsBody = GetMethodSupportsBody(method);
var url = Url;
var request = new HttpRequestMessage(new HttpMethod(Method), url);
var request = new HttpRequestMessage(new HttpMethod(method), url);
var authorizationHeaderValue = Authorization;
var requestHeaders = new HeaderDictionary(RequestHeaders.ToDictionary(x => x.Key, x => new StringValues(x.Value.Split(','))));

View file

@ -1,10 +1,13 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Models;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Design;
using Elsa.Expressions;
using Elsa.Serialization;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.AspNetCore.Http;
@ -22,11 +25,13 @@ namespace Elsa.Activities.Http
public class WriteHttpResponse : Activity
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IContentSerializer _contentSerializer;
public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocalizer<WriteHttpResponse> localizer)
public WriteHttpResponse(IHttpContextAccessor httpContextAccessor, IStringLocalizer<WriteHttpResponse> localizer, IContentSerializer contentSerializer)
{
T = localizer;
_httpContextAccessor = httpContextAccessor;
_contentSerializer = contentSerializer;
}
private IStringLocalizer T { get; }
@ -45,10 +50,10 @@ namespace Elsa.Activities.Http
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
/// <summary>
/// The content to send along with the response
/// The content to send along with the response.
/// </summary>
[ActivityInput(Hint = "The HTTP content to write.", UIHint = ActivityInputUIHints.MultiLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public string? Content { get; set; }
public object? Content { get; set; }
/// <summary>
/// The Content-Type header to send along with the response.
@ -68,7 +73,7 @@ namespace Elsa.Activities.Http
[ActivityInput(
Hint = "The character set to use when writing the response.",
UIHint = ActivityInputUIHints.Dropdown,
Options = new[] { "utf-8", "ASCII", "ANSI", "ISO-8859-1" },
Options = new[] { "", "utf-8", "ASCII", "ANSI", "ISO-8859-1" },
DefaultValue = "utf-8",
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid },
Category = PropertyCategories.Advanced)]
@ -78,10 +83,10 @@ namespace Elsa.Activities.Http
/// The headers to send along with the response.
/// </summary>
[ActivityInput(
Hint = "Additional headers to write.",
Hint = "Additional headers to write.",
UIHint = ActivityInputUIHints.MultiLine,
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[]{ SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json },
SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid, SyntaxNames.Json },
Category = PropertyCategories.Advanced
)]
public HttpResponseHeaders? ResponseHeaders { get; set; }
@ -95,7 +100,7 @@ namespace Elsa.Activities.Http
return Fault(T["Response has already started"]!);
response.StatusCode = (int) StatusCode;
response.ContentType = $"{ContentType};charset={CharSet}";
response.ContentType = string.IsNullOrWhiteSpace(CharSet) ? ContentType : $"{ContentType};charset={CharSet}";
var headers = ResponseHeaders;
@ -105,12 +110,37 @@ namespace Elsa.Activities.Http
response.Headers[header.Key] = header.Value;
}
var bodyText = Content;
if (!string.IsNullOrWhiteSpace(bodyText))
await response.WriteAsync(bodyText, context.CancellationToken);
await WriteContentAsync(context.CancellationToken);
return Done();
}
private async Task WriteContentAsync(CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext ?? new DefaultHttpContext();
var response = httpContext.Response;
var content = Content;
if (content == null)
return;
if (content is string stringContent)
{
if (!string.IsNullOrWhiteSpace(stringContent))
await response.WriteAsync(stringContent, cancellationToken);
return;
}
if (content is byte[] buffer)
{
await response.Body.WriteAsync(buffer, cancellationToken);
return;
}
var json = _contentSerializer.Serialize(content);
await response.WriteAsync(json, cancellationToken);
}
}
}

View file

@ -6,6 +6,8 @@ using Elsa.Activities.Http.JavaScript;
using Elsa.Activities.Http.Liquid;
using Elsa.Activities.Http.Options;
using Elsa.Activities.Http.Parsers;
using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Parsers.Response;
using Elsa.Activities.Http.Services;
using Elsa.Scripting.Liquid.Extensions;
using Microsoft.AspNetCore.Http;
@ -36,8 +38,9 @@ namespace Microsoft.Extensions.DependencyInjection
.AddSingleton<IHttpRequestBodyParser, DefaultHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, JsonHttpRequestBodyParser>()
.AddSingleton<IHttpRequestBodyParser, FormHttpRequestBodyParser>()
.AddSingleton<IHttpResponseBodyParser, DefaultHttpResponseBodyParser>()
.AddSingleton<IHttpResponseBodyParser, JsonHttpResponseBodyParser>()
.AddSingleton<IHttpResponseContentReader, DefaultHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, JsonHttpResponseContentReader>()
.AddSingleton<IHttpResponseContentReader, FileResponseContentReader>()
.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
.AddSingleton<IAbsoluteUrlProvider, DefaultAbsoluteUrlProvider>()
.AddBookmarkProvider<HttpEndpointBookmarkProvider>()

View file

@ -7,6 +7,7 @@ using Elsa.Activities.Http.Bookmarks;
using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Http.Models;
using Elsa.Activities.Http.Parsers;
using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Services;
using Elsa.Persistence;
using Elsa.Persistence.Specifications.WorkflowInstances;

View file

@ -1,15 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;
namespace Elsa.Activities.Http.Parsers
{
public class DefaultHttpResponseBodyParser : IHttpResponseBodyParser
{
public int Priority => -1;
public IEnumerable<string?> SupportedContentTypes => new[] { "", default };
public async Task<object> ParseAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}

View file

@ -5,7 +5,7 @@ using Elsa.Activities.Http.Extensions;
using Elsa.Activities.Http.Services;
using Microsoft.AspNetCore.Http;
namespace Elsa.Activities.Http.Parsers
namespace Elsa.Activities.Http.Parsers.Request
{
public class DefaultHttpRequestBodyParser : IHttpRequestBodyParser
{

View file

@ -0,0 +1,14 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;
namespace Elsa.Activities.Http.Parsers.Response
{
public class DefaultHttpResponseContentReader : IHttpResponseContentReader
{
public int Priority => -1;
public bool GetSupportsContentType(string contentType) => true;
public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Http.Services;
namespace Elsa.Activities.Http.Parsers.Response
{
public class FileResponseContentReader : IHttpResponseContentReader
{
public virtual int Priority => 5;
public virtual bool GetSupportsContentType(string contentType)
{
var types = new[] { "audio", "video", "application", "pdf" };
return types.Any(x => contentType.Contains(x, StringComparison.OrdinalIgnoreCase));
}
public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken) => await response.Content.ReadAsByteArrayAsync();
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System;
using System.Dynamic;
using System.Net.Http;
using System.Threading;
@ -6,14 +6,14 @@ using System.Threading.Tasks;
using Elsa.Activities.Http.Services;
using Newtonsoft.Json;
namespace Elsa.Activities.Http.Parsers
namespace Elsa.Activities.Http.Parsers.Response
{
public class JsonHttpResponseBodyParser : IHttpResponseBodyParser
public class JsonHttpResponseContentReader : IHttpResponseContentReader
{
public int Priority => 0;
public IEnumerable<string?> SupportedContentTypes => new[] { "application/json", "text/json" };
public int Priority => 10;
public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase);
public async Task<object> ParseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
public async Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken)
{
#if NET
var json = (await response.Content.ReadAsStringAsync(cancellationToken)).Trim();

View file

@ -1,14 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Activities.Http.Services
{
public interface IHttpResponseBodyParser
{
int Priority { get; }
IEnumerable<string?> SupportedContentTypes { get; }
Task<object> ParseAsync(HttpResponseMessage response, CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Activities.Http.Services
{
public interface IHttpResponseContentReader
{
int Priority { get; }
bool GetSupportsContentType(string contentType);
Task<object> ReadAsync(HttpResponseMessage response, CancellationToken cancellationToken);
}
}

View file

@ -0,0 +1,24 @@
using Jint;
using Jint.Native;
using Jint.Runtime.Interop;
namespace Elsa.Scripting.JavaScript.Converters.Jint
{
/// <summary>
/// Prevents Jint from returning byte[] as object[].
/// </summary>
internal class ByteArrayConverter : IObjectConverter
{
public bool TryConvert(Engine engine, object value, out JsValue result)
{
result = JsValue.Null;
if (value is not byte[] buffer)
return false;
result = new ObjectWrapper(engine, buffer);
return true;
}
}
}

View file

@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace Elsa.Scripting.JavaScript.Converters
namespace Elsa.Scripting.JavaScript.Converters.Json
{
/// <summary>
/// Ensures that whole numeric values are serialized without any decimal (e.g. 2 instead of 2.0) to ensure deserialization to models having int properties works.

View file

@ -8,42 +8,37 @@ namespace Elsa.Scripting.JavaScript.Services
{
public class EnumerableResultConverter : IConvertsJintEvaluationResult, IConvertsEnumerableToObject
{
readonly IConvertsJintEvaluationResult? wrapped;
private readonly IConvertsJintEvaluationResult? _wrapped;
public EnumerableResultConverter(IConvertsJintEvaluationResult? wrapped) => _wrapped = wrapped;
public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{
if(evaluationResult is IEnumerable enumerable && !(evaluationResult is ExpandoObject))
if (evaluationResult is IEnumerable enumerable && !(evaluationResult is ExpandoObject))
return ConvertEnumerable(enumerable, desiredType);
return wrapped?.ConvertToDesiredType(evaluationResult, desiredType);
return _wrapped?.ConvertToDesiredType(evaluationResult, desiredType);
}
object? ConvertEnumerable(IEnumerable enumerable, Type? desiredType = null)
private static object? ConvertEnumerable(IEnumerable enumerable, Type? desiredType = null)
{
if(enumerable is string) return enumerable;
if(enumerable is JObject) return enumerable;
if (enumerable is string) return enumerable;
if (enumerable is JObject) return enumerable;
if (enumerable is byte[]) return enumerable;
var destinationType = GetDestinationType(desiredType);
var json = JsonConvert.SerializeObject(enumerable);
return JsonConvert.DeserializeObject(json, destinationType);
}
static Type GetDestinationType(Type? desiredType)
{
return desiredType switch
private static Type GetDestinationType(Type? desiredType) =>
desiredType switch
{
Type t when t == typeof(object) => typeof(object[]),
null => typeof(object[]),
_ => desiredType,
{ } t when t == typeof(object) => typeof(object[]),
null => typeof(object[]),
_ => desiredType,
};
}
object? IConvertsEnumerableToObject.ConvertEnumerable(IEnumerable enumerable)
=> ConvertEnumerable(enumerable);
public EnumerableResultConverter(IConvertsJintEvaluationResult? wrapped)
{
this.wrapped = wrapped;
}
object? IConvertsEnumerableToObject.ConvertEnumerable(IEnumerable enumerable) => ConvertEnumerable(enumerable);
}
}

View file

@ -9,8 +9,8 @@ namespace Elsa.Scripting.JavaScript.Services
{
public class ExpandoObjectToDictionaryWhenNoDesiredTypeResultConverter : IConvertsJintEvaluationResult
{
readonly IConvertsJintEvaluationResult wrapped;
readonly IConvertsEnumerableToObject enumerableConverter;
private readonly IConvertsJintEvaluationResult wrapped;
private readonly IConvertsEnumerableToObject enumerableConverter;
public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{
@ -20,7 +20,7 @@ namespace Elsa.Scripting.JavaScript.Services
return wrapped.ConvertToDesiredType(evaluationResult, desiredType);
}
object? RecursivelyPrepareExpandoObjectForReturn(ExpandoObject obj)
private object? RecursivelyPrepareExpandoObjectForReturn(ExpandoObject obj)
{
IDictionary<string,object?> ExpandoToDictionary(ExpandoObject expando)
{

View file

@ -1,20 +0,0 @@
// using System;
// using Newtonsoft.Json.Linq;
//
// namespace Elsa.Scripting.JavaScript.Services
// {
// public class JObjectResultConverter : IConvertsJintEvaluationResult
// {
// readonly IConvertsJintEvaluationResult _wrapped;
//
// public JObjectResultConverter(IConvertsJintEvaluationResult wrapped) => _wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped));
//
// public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
// {
// if(evaluationResult is JObject jObject)
// return JObjectExtensions.DeserializeState(jObject, desiredType);
//
// return _wrapped.ConvertToDesiredType(evaluationResult, desiredType);
// }
// }
// }

View file

@ -11,37 +11,30 @@ namespace Elsa.Scripting.JavaScript.Services
public IConvertsJintEvaluationResult GetConverter()
{
IConvertsJintEvaluationResult service;
// Builds a chain-of-responsibility service
// Note: The order in which these classes execute is bottom-to-top
service = GetConvertChangeTypeService();
var service = GetConvertChangeTypeService();
service = GetPlainObjectService(service);
service = GetEnumerableConvertingService(service);
service = GetExpandoConvertingService(service);
//service = GetJObjectService(service);
service = GetTypeConverterConvertingService(service);
service = GetNullConvertingService(service);
return service;
}
static IConvertsJintEvaluationResult GetConvertChangeTypeService() => new ConvertChangeTypeResultConverter();
private static IConvertsJintEvaluationResult GetConvertChangeTypeService() => new ConvertChangeTypeResultConverter();
private static IConvertsJintEvaluationResult GetPlainObjectService(IConvertsJintEvaluationResult wrapped) => new PlainObjectResultConverter(wrapped);
private IConvertsJintEvaluationResult GetEnumerableConvertingService(IConvertsJintEvaluationResult wrapped) => new EnumerableResultConverter(wrapped);
static IConvertsJintEvaluationResult GetPlainObjectService(IConvertsJintEvaluationResult wrapped) => new PlainObjectResultConverter(wrapped);
//static IConvertsJintEvaluationResult GetJObjectService(IConvertsJintEvaluationResult wrapped) => new JObjectResultConverter(wrapped);
IConvertsJintEvaluationResult GetEnumerableConvertingService(IConvertsJintEvaluationResult wrapped) => new EnumerableResultConverter(wrapped);
IConvertsJintEvaluationResult GetExpandoConvertingService(IConvertsJintEvaluationResult wrapped)
private IConvertsJintEvaluationResult GetExpandoConvertingService(IConvertsJintEvaluationResult wrapped)
{
var enumerableConverter = _serviceProvider.GetRequiredService<IConvertsEnumerableToObject>();
return new ExpandoObjectToDictionaryWhenNoDesiredTypeResultConverter(enumerableConverter, wrapped);
}
static IConvertsJintEvaluationResult GetTypeConverterConvertingService(IConvertsJintEvaluationResult wrapped) => new TypeConverterResultConverter(wrapped);
static IConvertsJintEvaluationResult GetNullConvertingService(IConvertsJintEvaluationResult wrapped) => new NullResultConverter(wrapped);
private static IConvertsJintEvaluationResult GetTypeConverterConvertingService(IConvertsJintEvaluationResult wrapped) => new TypeConverterResultConverter(wrapped);
private static IConvertsJintEvaluationResult GetNullConvertingService(IConvertsJintEvaluationResult wrapped) => new NullResultConverter(wrapped);
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Scripting.JavaScript.Converters.Jint;
using Elsa.Scripting.JavaScript.Messages;
using Elsa.Scripting.JavaScript.Options;
using Elsa.Services.Models;
@ -41,10 +42,11 @@ namespace Elsa.Scripting.JavaScript.Services
return _resultConverter.ConvertToDesiredType(result, returnType);
}
async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ActivityExecutionContext context, CancellationToken cancellationToken)
private async Task<Engine> GetConfiguredEngine(Action<Engine>? configureEngine, ActivityExecutionContext context, CancellationToken cancellationToken)
{
var engine = new Engine(opts =>
{
opts.AddObjectConverter<ByteArrayConverter>();
if (_scriptOptions.AllowClr)
opts.AllowClr();
});

View file

@ -4,7 +4,7 @@ namespace Elsa.Scripting.JavaScript.Services
{
public class NullResultConverter : IConvertsJintEvaluationResult
{
readonly IConvertsJintEvaluationResult wrapped;
private readonly IConvertsJintEvaluationResult wrapped;
public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{

View file

@ -4,7 +4,7 @@ namespace Elsa.Scripting.JavaScript.Services
{
public class PlainObjectResultConverter : IConvertsJintEvaluationResult
{
readonly IConvertsJintEvaluationResult wrapped;
private readonly IConvertsJintEvaluationResult wrapped;
public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{

View file

@ -5,7 +5,7 @@ namespace Elsa.Scripting.JavaScript.Services
{
public class TypeConverterResultConverter : IConvertsJintEvaluationResult
{
readonly IConvertsJintEvaluationResult wrapped;
private readonly IConvertsJintEvaluationResult wrapped;
public object? ConvertToDesiredType(object? evaluationResult, Type desiredType)
{