8

I want to create a simple file upload endpoint in ASP.NET Core 6 and thought it would be as easy as described here https://dotnetthoughts.net/handling-file-uploads-in-openapi-with-aspnet-core/.

When I have an endpoint defined like:

app.MapPost("/upload", (IFormFile file) =>
{
    //Do something with the file
    return Results.Ok();
}).Accepts<IFormFile>("multipart/form-data").Produces(200);

I get a 415 back when I call the endpoint. The message I get back is something like:

Expected a supported JSON media type but got "multipart/form-data; ...

Not sure why it expected a supported json when I say that the endpoint should accept multipart/form-data.

Any ideas or thoughts on what to do here?

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Tomas Jansson
  • 22,767
  • 13
  • 83
  • 137

4 Answers4

8

Currently out of the box support for binding in Minimal APIs is quite limited. Supported binding sources:

  • Route values
  • Query string
  • Header
  • Body (as JSON)
  • Services provided by dependency injection
  • Custom

NOTE: Binding from forms is not natively supported in .NET 6

You can either leverage custom binding or use special types handling:

app.MapPost("/upload", (HttpRequest request) =>
{
    //Do something with the file
    var files = request.Form.Files;
    return Results.Ok();
})
.Accepts("multipart/form-data")
.Produces(200);

UPD

Since Minimal APIs should be able to bind IFormFile/IFormFileCollection directly:

app.MapPost("/upload", async (IFormFile file) =>
{
    // ...
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        // ...
    }
});
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Thanks. Will test tomorrow. I just wonder how they did it at the blog I referred to? Hm – Tomas Jansson Mar 16 '22 at 15:23
  • @TomasJansson I'm pretty much sure they are not using Minimal APIs but "standard" [Web APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#routing-basics-1) – Guru Stron Mar 16 '22 at 15:30
  • 1
    Have you verified that this actually work? When I do it I get a `Unexpected end of Stream, the content may have already been read by another component. ` exception. One difference is that I have `Accepts("multipart/form-data")`, I guess that is what make it so asp.net tries to read the body. But I don't seem to have a non-generic `Accepts` to use. – Tomas Jansson Mar 17 '22 at 08:16
  • 1
    Nevermind, I think it was how I created the request using the vs code rest client that was the problem, worked fine when testing the endpoint from insomnia. – Tomas Jansson Mar 17 '22 at 09:17
8

Just noting here, you can upload a file with any ContentType like the following sample code.

In this example I chose a text/plain ContentType, but you can choose your own by editing the .Accepts<IFormFile>("text/plain"); line to your desired ContentType.

app.MapPost("/upload",
    async (HttpRequest request) =>
    {
        using (var reader = new StreamReader(request.Body, System.Text.Encoding.UTF8))
        {

            // Read the raw file as a `string`.
            string fileContent = await reader.ReadToEndAsync();

            // Do something with `fileContent`...
    
            return "File Was Processed Sucessfully!";
        }
    }).Accepts<IFormFile>("text/plain");

It is also well supported by Swagger UI:

enter image description here

  • If the file is binary I think its better to access the file via `request.Form.Files[0]` instead of reading from the body. Havent tested it though. – Tono Nam Dec 14 '22 at 02:51
1

This has been addressed with .NET 7. Support for IFormFile and IFormFileCollection has been added. You should be able to use it in your MediatR pipelines.

Ref: .NET 7 Minimal API Support for File Upload Bindings

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();
akriy
  • 21
  • 2
0
app.MapPost("/upload",
    async Task<IResult> (HttpRequest request) =>
    {
        if (!request.HasFormContentType)
            return Results.BadRequest();

        var form = await request.ReadFormAsync();

        if (form.Files.Any() == false)
            return Results.BadRequest("There are no files");

        var file = form.Files.FirstOrDefault();

        if (file is null || file.Length == 0)
            return Results.BadRequest("File cannot be empty");

        using var stream = file.OpenReadStream();

        var reader = new StreamReader(stream);
        var text = await reader.ReadToEndAsync();

        return Results.Ok(text);
    }).Accepts<IFormFile>("multipart/form-data"); 
Tono Nam
  • 34,064
  • 78
  • 298
  • 470