From 061daa206dcdc7e6dd6e41ab0d8f1c60dabf7c86 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 24 Jan 2023 21:48:05 +0100 Subject: [PATCH] Implement dynamic HTTP content writing --- .../Elsa.Http/Activities/SendHttpRequest.cs | 8 +++--- .../Elsa.Http/Activities/WriteHttpResponse.cs | 24 +++++++++++++--- src/modules/Elsa.Http/Constants/MimeTypes.cs | 8 ------ .../FormUrlEncodedHttpContentFactory.cs | 23 +++++++++++++++ .../FormUrlEncodedHttpContentWriter.cs | 24 ---------------- .../ContentWriters/IHttpContentFactory.cs | 17 +++++++++++ .../ContentWriters/IHttpContentWriter.cs | 7 ----- .../ContentWriters/JsonContentFactory.cs | 27 ++++++++++++++++++ .../ContentWriters/StringHttpContentWriter.cs | 21 -------------- .../ContentWriters/TextContentFactory.cs | 28 +++++++++++++++++++ src/modules/Elsa.Http/Features/HttpFeature.cs | 9 +++--- 11 files changed, 124 insertions(+), 72 deletions(-) delete mode 100644 src/modules/Elsa.Http/Constants/MimeTypes.cs create mode 100644 src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentFactory.cs delete mode 100644 src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentWriter.cs create mode 100644 src/modules/Elsa.Http/ContentWriters/IHttpContentFactory.cs delete mode 100644 src/modules/Elsa.Http/ContentWriters/IHttpContentWriter.cs create mode 100644 src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs delete mode 100644 src/modules/Elsa.Http/ContentWriters/StringHttpContentWriter.cs create mode 100644 src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 78868bb35..7a8a376e5 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -114,14 +114,14 @@ public class SendHttpRequest : CodeActivity if (contentType != null && content != null) { - var contentWriters = context.GetServices(); + var contentWriters = context.GetServices(); 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 requestContentWriters) => - string.IsNullOrWhiteSpace(contentType) ? new StringHttpContentWriter() : requestContentWriters.First(w => w.SupportsContentType(contentType)); + private IHttpContentFactory SelectContentWriter(string? contentType, IEnumerable requestContentWriters) => + string.IsNullOrWhiteSpace(contentType) ? new JsonContentFactory() : requestContentWriters.First(w => w.SupportsContentType(contentType)); } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs b/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs index ed2e53dfe..00416271b 100644 --- a/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs +++ b/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs @@ -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 /// /// The content to write back. /// - [Input(Description = "The content to write back.")] - public Input 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 Content { get; set; } = default!; /// /// 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().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"; } \ No newline at end of file diff --git a/src/modules/Elsa.Http/Constants/MimeTypes.cs b/src/modules/Elsa.Http/Constants/MimeTypes.cs deleted file mode 100644 index a8f1274a5..000000000 --- a/src/modules/Elsa.Http/Constants/MimeTypes.cs +++ /dev/null @@ -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"; -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentFactory.cs new file mode 100644 index 000000000..488f8232d --- /dev/null +++ b/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentFactory.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Elsa.Http.ContentWriters; + +/// +/// A content writer that writes content in the application/x-www-form-urlencoded format. +/// +public class FormUrlEncodedHttpContentFactory : IHttpContentFactory +{ + private readonly List _supportedContentTypes = new() { "application/x-www-form-urlencoded" }; + + /// + public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType); + + /// + public HttpContent CreateHttpContent(object content, string? contentType = null) => new FormUrlEncodedContent(GetContentAsDictionary(content)); + + private static Dictionary GetContentAsDictionary(object content) => + (content is string or JsonObject + ? JsonSerializer.Deserialize>(JsonSerializer.Serialize(content)) + : (Dictionary)Convert.ChangeType(content, typeof(Dictionary)))!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentWriter.cs b/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentWriter.cs deleted file mode 100644 index 4543b68a3..000000000 --- a/src/modules/Elsa.Http/ContentWriters/FormUrlEncodedHttpContentWriter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Elsa.Http.Constants; - -namespace Elsa.Http.ContentWriters; - -/// -/// A content writer that writes content in the application/x-www-form-urlencoded format. -/// -public class FormUrlEncodedHttpContentWriter : IHttpContentWriter -{ - private readonly List _supportedContentTypes = new() {MimeTypes.ApplicationWwwFormUrlEncoded}; - - /// - public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType); - - /// - public HttpContent GetContent( T content, string? contentType = null) => new FormUrlEncodedContent(GetContentAsDictionary(content)); - - private static Dictionary GetContentAsDictionary(TType body) => - (body is string || body is JsonObject ? - JsonSerializer.Deserialize>(JsonSerializer.Serialize(body)) : - (Dictionary)Convert.ChangeType(body, typeof(Dictionary))!)!; -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/IHttpContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/IHttpContentFactory.cs new file mode 100644 index 000000000..8546e2249 --- /dev/null +++ b/src/modules/Elsa.Http/ContentWriters/IHttpContentFactory.cs @@ -0,0 +1,17 @@ +namespace Elsa.Http.ContentWriters; + +/// +/// Creates a concrete instance based on the specified content type. +/// +public interface IHttpContentFactory +{ + /// + /// Returns a value indicating whether this factory supports the specified content type. + /// + bool SupportsContentType(string contentType); + + /// + /// Creates a concrete derivative based on the specified content type. + /// + HttpContent CreateHttpContent(object content, string contentType); +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/IHttpContentWriter.cs b/src/modules/Elsa.Http/ContentWriters/IHttpContentWriter.cs deleted file mode 100644 index b4a3b3d64..000000000 --- a/src/modules/Elsa.Http/ContentWriters/IHttpContentWriter.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Elsa.Http.ContentWriters; - -public interface IHttpContentWriter -{ - bool SupportsContentType(string contentType); - HttpContent GetContent(T content, string? contentType = null); -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs new file mode 100644 index 000000000..401443f18 --- /dev/null +++ b/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs @@ -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; + +/// +/// Creates a object for application/json. +/// +public class JsonContentFactory : IHttpContentFactory +{ + private readonly List _supportedContentTypes = new() { MediaTypeNames.Application.Json }; + + /// + public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType); + + /// + 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); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/StringHttpContentWriter.cs b/src/modules/Elsa.Http/ContentWriters/StringHttpContentWriter.cs deleted file mode 100644 index e2bbbbf5d..000000000 --- a/src/modules/Elsa.Http/ContentWriters/StringHttpContentWriter.cs +++ /dev/null @@ -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 SupportedContentTypes = new() {MimeTypes.ApplicationJson, MimeTypes.ApplicationXml}; - - public bool SupportsContentType(string contentType) - { - return SupportedContentTypes.Contains(contentType); - } - - public HttpContent GetContent(T content, string? contentType = null) - { - var serializedContent = JsonSerializer.Serialize(content); - return new StringContent(serializedContent, Encoding. UTF8, contentType); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs new file mode 100644 index 000000000..197488480 --- /dev/null +++ b/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs @@ -0,0 +1,28 @@ +using System.Net.Mime; +using System.Text; + +namespace Elsa.Http.ContentWriters; + +/// +/// Creates a object for application/json, application/xml, text/json and text/xml content types. +/// +public class TextContentFactory : IHttpContentFactory +{ + private readonly List _supportedContentTypes = new() + { + MediaTypeNames.Text.Plain, + MediaTypeNames.Text.Xml, + MediaTypeNames.Text.RichText, + MediaTypeNames.Text.Html, + }; + + /// + public bool SupportsContentType(string contentType) => _supportedContentTypes.Contains(contentType); + + /// + public HttpContent CreateHttpContent(object content, string contentType) + { + var text = content as string ?? ""; + return new StringContent(text, Encoding.UTF8, contentType); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 9dca6a73a..6073e974c 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -78,14 +78,15 @@ public class HttpFeature : FeatureBase .AddNotificationHandlersFrom() .AddHttpContextAccessor() - // Add Content Parsers. + // Add content parsers. .AddSingleton() .AddSingleton() .AddSingleton() - // Add Request Content Writers. - .AddSingleton() - .AddSingleton() + // Add HTTP content factories. + .AddSingleton() + .AddSingleton() + .AddSingleton() ; } } \ No newline at end of file