Implement dynamic HTTP content writing

This commit is contained in:
Sipke Schoorstra 2023-01-24 21:48:05 +01:00
parent 6dba654e9f
commit 061daa206d
11 changed files with 124 additions and 72 deletions

View file

@ -114,14 +114,14 @@ public class SendHttpRequest : CodeActivity<HttpResponse>
if (contentType != null && content != null)
{
var contentWriters = context.GetServices<IHttpContentWriter>();
var contentWriters = context.GetServices<IHttpContentFactory>();
var contentWriter = SelectContentWriter(contentType, contentWriters);
request.Content = contentWriter.GetContent(content, contentType);
request.Content = contentWriter.CreateHttpContent(content, contentType);
}
return request;
}
private IHttpContentWriter SelectContentWriter(string? contentType, IEnumerable<IHttpContentWriter> requestContentWriters) =>
string.IsNullOrWhiteSpace(contentType) ? new StringHttpContentWriter() : requestContentWriters.First(w => w.SupportsContentType(contentType));
private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable<IHttpContentFactory> requestContentWriters) =>
string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType));
}

View file

@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Headers;
using Elsa.Extensions;
using Elsa.Http.ContentWriters;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
@ -25,8 +26,8 @@ public class WriteHttpResponse : CodeActivity
/// <summary>
/// The content to write back.
/// </summary>
[Input(Description = "The content to write back.")]
public Input<string?> Content { get; set; } = new("");
[Input(Description = "The content to write back. String values will be sent as-is, while objects will be serialized to a JSON string. Byte arrays and streams will be sent as files.")]
public Input<object?> Content { get; set; } = default!;
/// <summary>
/// The content type to use when returning the response.
@ -78,15 +79,30 @@ public class WriteHttpResponse : CodeActivity
private async Task WriteResponseAsync(ActivityExecutionContext context, HttpResponse response)
{
// Set status code.
response.StatusCode = (int)context.Get(StatusCode);
// Add headers.
var headers = ResponseHeaders.TryGet(context) ?? new HttpResponseHeaders();
foreach (var header in headers)
response.Headers.Add(header.Key, header.Value);
// Get content and content type.
var content = context.Get(Content);
if (content != null)
await response.WriteAsync(content, context.CancellationToken);
if (content == null)
return;
var contentType = ContentType.Get(context) ?? DetermineContentType(content);
var contentWriter = context.GetServices<IHttpContentFactory>().FirstOrDefault(x => x.SupportsContentType(contentType)) ?? new TextContentFactory();
var httpContent = contentWriter.CreateHttpContent(content, contentType);
// Set content type.
response.ContentType = httpContent.Headers.ContentType?.ToString() ?? contentType;
// Write content.
await httpContent.CopyToAsync(response.Body);
}
private string DetermineContentType(object? content) => content is byte[] or Stream ? "application/octet-stream" : "text/plain";
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Http.Constants;
public static class MimeTypes
{
public const string ApplicationXml = "application/xml";
public const string ApplicationJson = "application/json";
public const string ApplicationWwwFormUrlEncoded = "application/x-www-form-urlencoded";
}

View file

@ -0,0 +1,23 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Elsa.Http.ContentWriters;
/// <summary>
/// A content writer that writes content in the application/x-www-form-urlencoded format.
/// </summary>
public class FormUrlEncodedHttpContentFactory : IHttpContentFactory
{
private readonly List<string> _supportedContentTypes = new() { "application/x-www-form-urlencoded" };
/// <inheritdoc />
public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType);
/// <inheritdoc />
public HttpContent CreateHttpContent(object content, string? contentType = null) => new FormUrlEncodedContent(GetContentAsDictionary(content));
private static Dictionary<string, string> GetContentAsDictionary(object content) =>
(content is string or JsonObject
? JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(content))
: (Dictionary<string, string>)Convert.ChangeType(content, typeof(Dictionary<string, string>)))!;
}

View file

@ -1,24 +0,0 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Elsa.Http.Constants;
namespace Elsa.Http.ContentWriters;
/// <summary>
/// A content writer that writes content in the application/x-www-form-urlencoded format.
/// </summary>
public class FormUrlEncodedHttpContentWriter : IHttpContentWriter
{
private readonly List<string> _supportedContentTypes = new() {MimeTypes.ApplicationWwwFormUrlEncoded};
/// <inheritdoc />
public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType);
/// <inheritdoc />
public HttpContent GetContent<T>( T content, string? contentType = null) => new FormUrlEncodedContent(GetContentAsDictionary(content));
private static Dictionary<string, string> GetContentAsDictionary<TType>(TType body) =>
(body is string || body is JsonObject ?
JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(body)) :
(Dictionary<string, string>)Convert.ChangeType(body, typeof(Dictionary<string, string>))!)!;
}

View file

@ -0,0 +1,17 @@
namespace Elsa.Http.ContentWriters;
/// <summary>
/// Creates a concrete <see cref="HttpContent"/> instance based on the specified content type.
/// </summary>
public interface IHttpContentFactory
{
/// <summary>
/// Returns a value indicating whether this factory supports the specified content type.
/// </summary>
bool SupportsContentType(string contentType);
/// <summary>
/// Creates a concrete <see cref="HttpContent"/> derivative based on the specified content type.
/// </summary>
HttpContent CreateHttpContent(object content, string contentType);
}

View file

@ -1,7 +0,0 @@
namespace Elsa.Http.ContentWriters;
public interface IHttpContentWriter
{
bool SupportsContentType(string contentType);
HttpContent GetContent<T>(T content, string? contentType = null);
}

View file

@ -0,0 +1,27 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Text;
namespace Elsa.Http.ContentWriters;
/// <summary>
/// Creates a <see cref="StringContent"/> object for application/json.
/// </summary>
public class JsonContentFactory : IHttpContentFactory
{
private readonly List<string> _supportedContentTypes = new() { MediaTypeNames.Application.Json };
/// <inheritdoc />
public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType);
/// <inheritdoc />
public HttpContent CreateHttpContent(object content, string contentType)
{
if (content is string s)
return new StringContent(s, Encoding.UTF8, contentType);
var mediaType = MediaTypeHeaderValue.Parse(contentType);
return JsonContent.Create(content, mediaType);
}
}

View file

@ -1,21 +0,0 @@
using System.Text;
using System.Text.Json;
using Elsa.Http.Constants;
namespace Elsa.Http.ContentWriters;
public class StringHttpContentWriter : IHttpContentWriter
{
private List<string> SupportedContentTypes = new() {MimeTypes.ApplicationJson, MimeTypes.ApplicationXml};
public bool SupportsContentType(string contentType)
{
return SupportedContentTypes.Contains(contentType);
}
public HttpContent GetContent<T>(T content, string? contentType = null)
{
var serializedContent = JsonSerializer.Serialize(content);
return new StringContent(serializedContent, Encoding. UTF8, contentType);
}
}

View file

@ -0,0 +1,28 @@
using System.Net.Mime;
using System.Text;
namespace Elsa.Http.ContentWriters;
/// <summary>
/// Creates a <see cref="StringContent"/> object for application/json, application/xml, text/json and text/xml content types.
/// </summary>
public class TextContentFactory : IHttpContentFactory
{
private readonly List<string> _supportedContentTypes = new()
{
MediaTypeNames.Text.Plain,
MediaTypeNames.Text.Xml,
MediaTypeNames.Text.RichText,
MediaTypeNames.Text.Html,
};
/// <inheritdoc />
public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType);
/// <inheritdoc />
public HttpContent CreateHttpContent(object content, string contentType)
{
var text = content as string ?? "";
return new StringContent(text, Encoding.UTF8, contentType);
}
}

View file

@ -78,14 +78,15 @@ public class HttpFeature : FeatureBase
.AddNotificationHandlersFrom<UpdateRouteTable>()
.AddHttpContextAccessor()
// Add Content Parsers.
// Add content parsers.
.AddSingleton<IHttpContentParser, StringHttpContentParser>()
.AddSingleton<IHttpContentParser, JsonHttpContentParser>()
.AddSingleton<IHttpContentParser, XmlHttpContentParser>()
// Add Request Content Writers.
.AddSingleton<IHttpContentWriter, StringHttpContentWriter>()
.AddSingleton<IHttpContentWriter, FormUrlEncodedHttpContentWriter>()
// Add HTTP content factories.
.AddSingleton<IHttpContentFactory, TextContentFactory>()
.AddSingleton<IHttpContentFactory, JsonContentFactory>()
.AddSingleton<IHttpContentFactory, FormUrlEncodedHttpContentFactory>()
;
}
}