Skip to content
Merged
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
50 changes: 50 additions & 0 deletions src/Http/Wolverine.Http.Tests/from_form_file_binding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Net.Http;
using Shouldly;

namespace Wolverine.Http.Tests;

public class from_form_file_binding : IntegrationContext
{
public from_form_file_binding(AppFixture fixture) : base(fixture)
{
}

[Fact]
public async Task bind_single_file_on_complex_model()
{
var content = new MultipartFormDataContent();
content.Add(new StringContent("test-name"), "Name");
content.Add(new ByteArrayContent(new byte[] { 1, 2, 3 }), "File", "test.txt");

var response = await Host.Server.CreateClient().PostAsync("/api/fromform-file", content);
var text = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
text.ShouldBe("test-name|test.txt|3");
}

[Fact]
public async Task bind_file_collection_on_complex_model()
{
var content = new MultipartFormDataContent();
content.Add(new StringContent("test-name"), "Name");
content.Add(new ByteArrayContent(new byte[] { 1, 2, 3 }), "Files", "file1.txt");
content.Add(new ByteArrayContent(new byte[] { 4, 5 }), "Files", "file2.txt");

var response = await Host.Server.CreateClient().PostAsync("/api/fromform-files", content);
response.EnsureSuccessStatusCode();
var text = await response.Content.ReadAsStringAsync();
text.ShouldBe("test-name|2");
}

[Fact]
public async Task bind_file_on_complex_model_when_no_file_sent()
{
var content = new MultipartFormDataContent();
content.Add(new StringContent("test-name"), "Name");

var response = await Host.Server.CreateClient().PostAsync("/api/fromform-file", content);
response.EnsureSuccessStatusCode();
var text = await response.Content.ReadAsStringAsync();
text.ShouldBe("test-name||");
}
}
36 changes: 34 additions & 2 deletions src/Http/Wolverine.Http/CodeGen/FormBindingFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JasperFx.CodeGeneration.Model;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;
using Wolverine.Http.CodeGen;
Expand Down Expand Up @@ -81,6 +82,20 @@ public FormBindingFrame(Type queryType, HttpChain chain){
_constructor = constructors.Single();
foreach (var parameter in _constructor.GetParameters())
{
if (parameter.ParameterType == typeof(IFormFile))
{
var fileFrame = new FormFilePropertyFrame(parameter.Name!);
_parameters.Add(fileFrame.Variable);
continue;
}

if (parameter.ParameterType == typeof(IFormFileCollection))
{
var filesFrame = new FormFileCollectionPropertyFrame();
_parameters.Add(filesFrame.Variable);
continue;
}

var formValueVariable = chain.TryFindOrCreateFormValue(parameter);
_parameters.Add(formValueVariable);
}
Expand All @@ -90,14 +105,31 @@ public FormBindingFrame(Type queryType, HttpChain chain){
foreach (var propertyInfo in queryType.GetProperties().Where(x => x.CanWrite))
{
var formName = propertyInfo.Name;
if (propertyInfo.TryGetAttribute<FromFormAttribute>(out var att))
if (propertyInfo.TryGetAttribute<FromFormAttribute>(out var att) && att.Name.IsNotEmpty())
{
formName = att.Name;
}

if (propertyInfo.PropertyType == typeof(IFormFile))
{
var fileFrame = new FormFilePropertyFrame(formName);
fileFrame.AssignToProperty($"{Variable.Usage}.{propertyInfo.Name}");
_props.Add(fileFrame);
continue;
}

if (propertyInfo.PropertyType == typeof(IFormFileCollection))
{
var filesFrame = new FormFileCollectionPropertyFrame();
filesFrame.AssignToProperty($"{Variable.Usage}.{propertyInfo.Name}");
_props.Add(filesFrame);
continue;
}

var formValueVariable =
chain.TryFindOrCreateFormValue(propertyInfo.PropertyType, propertyInfo.Name, formName);

if (formValueVariable.Creator is IReadHttpFrame frame)
if (formValueVariable?.Creator is IReadHttpFrame frame)
{
frame.AssignToProperty($"{Variable.Usage}.{propertyInfo.Name}");
_props.Add(frame);
Expand Down
74 changes: 74 additions & 0 deletions src/Http/Wolverine.Http/CodeGen/FromFileStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,78 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
$"var {Variable.Usage} = {nameof(HttpHandler.ReadManyFormFileValues)}({_httpContext!.Usage});");
Next?.GenerateCode(method, writer);
}
}

internal class FormFilePropertyFrame : SyncFrame, IReadHttpFrame
{
private string? _property;
private readonly string _formName;

public FormFilePropertyFrame(string formName)
{
_formName = formName;
Variable = new HttpElementVariable(typeof(IFormFile), formName.SanitizeFormNameForVariable(), this);
}

public HttpElementVariable Variable { get; }

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
if (Mode == AssignMode.WriteToProperty && _property != null)
{
writer.Write(
$"{_property} = {nameof(HttpHandler.ReadFormFileByName)}(httpContext, \"{_formName}\");");
}
else
{
writer.Write(
$"var {Variable.Usage} = {nameof(HttpHandler.ReadFormFileByName)}(httpContext, \"{_formName}\");");
}

Next?.GenerateCode(method, writer);
}

public void AssignToProperty(string usage)
{
_property = usage;
Mode = AssignMode.WriteToProperty;
}

public AssignMode Mode { get; private set; } = AssignMode.WriteToVariable;
}

internal class FormFileCollectionPropertyFrame : SyncFrame, IReadHttpFrame
{
private string? _property;

public FormFileCollectionPropertyFrame()
{
Variable = new HttpElementVariable(typeof(IFormFileCollection), "files", this);
}

public HttpElementVariable Variable { get; }

public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
if (Mode == AssignMode.WriteToProperty && _property != null)
{
writer.Write(
$"{_property} = {nameof(HttpHandler.ReadManyFormFileValues)}(httpContext);");
}
else
{
writer.Write(
$"var {Variable.Usage} = {nameof(HttpHandler.ReadManyFormFileValues)}(httpContext);");
}

Next?.GenerateCode(method, writer);
}

public void AssignToProperty(string usage)
{
_property = usage;
Mode = AssignMode.WriteToProperty;
}

public AssignMode Mode { get; private set; } = AssignMode.WriteToVariable;
}
2 changes: 1 addition & 1 deletion src/Http/Wolverine.Http/HttpChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ private void applyMetadata()
if (HasRequestType)
{
if(IsFormData){
Metadata.Accepts(RequestType, true, "application/x-www-form-urlencoded");
Metadata.Accepts(RequestType, true, "application/x-www-form-urlencoded", "multipart/form-data");
}else{
Metadata.Accepts(RequestType, false, "application/json");
}
Expand Down
5 changes: 5 additions & 0 deletions src/Http/Wolverine.Http/HttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ public static string[] ReadManyHeaderValues(HttpContext context, string headerKe
return context.Request.Form.Files.SingleOrDefault();
}

public static IFormFile? ReadFormFileByName(HttpContext context, string name)
{
return context.Request.Form.Files.GetFile(name);
}

public static IFormFileCollection? ReadManyFormFileValues(HttpContext context)
{
return context.Request.Form.Files;
Expand Down
24 changes: 24 additions & 0 deletions src/Http/WolverineWebApi/Forms/FormEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ public static class FromFormEndpoints{
[WolverinePost("/api/fromformbigquery")]
public static BigQuery Post([FromForm] BigQuery query) => query;
#endregion

[WolverinePost("/api/fromform-file")]
public static string PostWithFile([FromForm] FormWithFile form)
{
return $"{form.Name}|{form.File?.FileName}|{form.File?.Length}";
}

[WolverinePost("/api/fromform-files")]
public static string PostWithFiles([FromForm] FormWithFiles form)
{
return $"{form.Name}|{form.Files?.Count}";
}
}

public class FormWithFile
{
public string Name { get; set; }
public IFormFile? File { get; set; }
}

public class FormWithFiles
{
public string Name { get; set; }
public IFormFileCollection? Files { get; set; }
}

#region sample_using_as_parameters_binding
Expand Down
Loading