diff --git a/src/modules/Elsa.Http/ContentWriters/MultipartFormDataHttpContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/MultipartFormDataHttpContentFactory.cs
new file mode 100644
index 0000000000..2bbaad9c6a
--- /dev/null
+++ b/src/modules/Elsa.Http/ContentWriters/MultipartFormDataHttpContentFactory.cs
@@ -0,0 +1,36 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Elsa.Http.ContentWriters;
+
+///
+/// A content writer that writes content in the multipart/form-data format.
+///
+public class MultipartFormDataHttpContentFactory : IHttpContentFactory
+{
+ ///
+ public IEnumerable SupportedContentTypes => ["multipart/form-data"];
+
+ ///
+ 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 GetContentAsDictionary(object content)
+ {
+ if (content is IDictionary dictionary)
+ return dictionary.ToDictionary(x => x.Key, x => x.Value.ToString() ?? string.Empty);
+
+ if (content is string or JsonObject)
+ return JsonSerializer.Deserialize>(JsonSerializer.Serialize(content))!;
+
+ var jsonElement = JsonSerializer.SerializeToElement(content);
+ return jsonElement.EnumerateObject().ToDictionary(x => x.Name, x => x.Value.ToString());
+ }
+}
diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs
index 6e406665ae..440b912ac4 100644
--- a/src/modules/Elsa.Http/Features/HttpFeature.cs
+++ b/src/modules/Elsa.Http/Features/HttpFeature.cs
@@ -194,6 +194,7 @@ public override void Apply()
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
// Activity property options providers.
.AddScoped()
diff --git a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
index 7f333e1916..a46e51acf3 100644
--- a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
+++ b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
@@ -164,6 +164,7 @@ public void ConfigureServices(IServiceCollection services)
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
// Activity property options providers.
.AddScoped()
diff --git a/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs b/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs
index f14b02175a..8610e08d25 100644
--- a/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs
+++ b/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs
@@ -1,4 +1,5 @@
using System.Text;
+using System.Text.Json.Nodes;
using Elsa.Http.ContentWriters;
using Xunit;
@@ -112,4 +113,77 @@ public async Task JsonContentFactory_ShouldHandleMultiByteCharacters()
Assert.Equal(Encoding.UTF8.GetByteCount(content), bytes.Length);
Assert.Equal(httpContent.Headers.ContentLength, bytes.Length);
}
+
+ ///
+ /// Tests that reports support for the multipart/form-data content type.
+ ///
+ [Fact]
+ public void MultipartFormDataHttpContentFactory_ShouldSupportMultipartFormData()
+ {
+ // Arrange
+ var factory = new MultipartFormDataHttpContentFactory();
+
+ // Assert
+ Assert.Contains("multipart/form-data", factory.SupportedContentTypes);
+ }
+
+ ///
+ /// Tests that produces multipart/form-data content with a boundary.
+ ///
+ [Fact]
+ public void MultipartFormDataHttpContentFactory_ShouldSetMultipartContentType()
+ {
+ // Arrange
+ var content = new Dictionary { ["field1"] = "value1", ["field2"] = "value2" };
+ var factory = new MultipartFormDataHttpContentFactory();
+
+ // Act
+ var httpContent = factory.CreateHttpContent(content, "multipart/form-data");
+
+ // Assert
+ Assert.IsType(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");
+ }
+
+ ///
+ /// Tests that writes one part per dictionary entry.
+ ///
+ [Fact]
+ public async Task MultipartFormDataHttpContentFactory_ShouldWriteEachFieldAsPart()
+ {
+ // Arrange
+ var content = new Dictionary { ["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);
+ }
+
+ ///
+ /// Tests that accepts a payload.
+ ///
+ [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);
+ }
}
\ No newline at end of file