Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Elsa.Http.ContentWriters;

/// <summary>
/// A content writer that writes content in the multipart/form-data format.
/// </summary>
public class MultipartFormDataHttpContentFactory : IHttpContentFactory
{
/// <inheritdoc />
public IEnumerable<string> SupportedContentTypes => ["multipart/form-data"];

/// <inheritdoc />
public HttpContent CreateHttpContent(object content, string? contentType = null)
{
var multipartContent = new MultipartFormDataContent();

foreach (var (key, value) in GetContentAsDictionary(content))
multipartContent.Add(new StringContent(value), key);

return multipartContent;
}

private static IDictionary<string, string> GetContentAsDictionary(object content)
{
if (content is IDictionary<string, object> dictionary)
return dictionary.ToDictionary(x => x.Key, x => x.Value.ToString() ?? string.Empty);

if (content is string or JsonObject)
return JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(content))!;

var jsonElement = JsonSerializer.SerializeToElement(content);
return jsonElement.EnumerateObject().ToDictionary(x => x.Name, x => x.Value.ToString());
}
}
1 change: 1 addition & 0 deletions src/modules/Elsa.Http/Features/HttpFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public override void Apply()
.AddScoped<IHttpContentFactory, JsonContentFactory>()
.AddScoped<IHttpContentFactory, XmlContentFactory>()
.AddScoped<IHttpContentFactory, FormUrlEncodedHttpContentFactory>()
.AddScoped<IHttpContentFactory, MultipartFormDataHttpContentFactory>()

// Activity property options providers.
.AddScoped<IPropertyUIHandler, HttpContentTypeOptionsProvider>()
Expand Down
1 change: 1 addition & 0 deletions src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public void ConfigureServices(IServiceCollection services)
.AddScoped<IHttpContentFactory, JsonContentFactory>()
.AddScoped<IHttpContentFactory, XmlContentFactory>()
.AddScoped<IHttpContentFactory, FormUrlEncodedHttpContentFactory>()
.AddScoped<IHttpContentFactory, MultipartFormDataHttpContentFactory>()

// Activity property options providers.
.AddScoped<IPropertyUIHandler, HttpContentTypeOptionsProvider>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json.Nodes;
using Elsa.Http.ContentWriters;
using Xunit;

Expand Down Expand Up @@ -112,4 +113,77 @@ public async Task JsonContentFactory_ShouldHandleMultiByteCharacters()
Assert.Equal(Encoding.UTF8.GetByteCount(content), bytes.Length);
Assert.Equal(httpContent.Headers.ContentLength, bytes.Length);
}

/// <summary>
/// Tests that <see cref="MultipartFormDataHttpContentFactory"/> reports support for the multipart/form-data content type.
/// </summary>
[Fact]
public void MultipartFormDataHttpContentFactory_ShouldSupportMultipartFormData()
{
// Arrange
var factory = new MultipartFormDataHttpContentFactory();

// Assert
Assert.Contains("multipart/form-data", factory.SupportedContentTypes);
}

/// <summary>
/// Tests that <see cref="MultipartFormDataHttpContentFactory"/> produces multipart/form-data content with a boundary.
/// </summary>
[Fact]
public void MultipartFormDataHttpContentFactory_ShouldSetMultipartContentType()
{
// Arrange
var content = new Dictionary<string, object> { ["field1"] = "value1", ["field2"] = "value2" };
var factory = new MultipartFormDataHttpContentFactory();

// Act
var httpContent = factory.CreateHttpContent(content, "multipart/form-data");

// Assert
Assert.IsType<MultipartFormDataContent>(httpContent);
Assert.Equal("multipart/form-data", httpContent.Headers.ContentType?.MediaType);
// A boundary parameter is required for multipart content.
Assert.Contains(httpContent.Headers.ContentType!.Parameters, p => p.Name == "boundary");
}

/// <summary>
/// Tests that <see cref="MultipartFormDataHttpContentFactory"/> writes one part per dictionary entry.
/// </summary>
[Fact]
public async Task MultipartFormDataHttpContentFactory_ShouldWriteEachFieldAsPart()
{
// Arrange
var content = new Dictionary<string, object> { ["name"] = "Alice", ["role"] = "admin" };
var factory = new MultipartFormDataHttpContentFactory();

// Act
var httpContent = factory.CreateHttpContent(content, "multipart/form-data");
var body = await httpContent.ReadAsStringAsync();

// Assert
// Each entry becomes a form-data section carrying its value.
Assert.Contains("form-data", body);
Assert.Contains("Alice", body);
Assert.Contains("admin", body);
}

/// <summary>
/// Tests that <see cref="MultipartFormDataHttpContentFactory"/> accepts a <see cref="JsonObject"/> payload.
/// </summary>
[Fact]
public async Task MultipartFormDataHttpContentFactory_ShouldSupportJsonObjectContent()
{
// Arrange
var content = new JsonObject { ["field1"] = "value1", ["field2"] = "value2" };
var factory = new MultipartFormDataHttpContentFactory();

// Act
var httpContent = factory.CreateHttpContent(content, "multipart/form-data");
var body = await httpContent.ReadAsStringAsync();

// Assert
Assert.Contains("value1", body);
Assert.Contains("value2", body);
}
}