Web API file upload with metadata

March 14th 2025 ASP.NET Core .NET

In my experience, file upload is not a very common operation in REST APIs. But when you are dealing with files, JSON requests aren't the best approach. Unless your files are text only or very small, you don't want to make your requests larger than necessary by encoding the files as JSON.

If you look at file upload in the OpenAPI guide, you'll see that multipart form data encoding is used instead. ASP.NET Core Web API has full support for it, but it isn't well documented. There is extensive documentation about file uploads, and there is documentation about different types of model binding, including binding from form fields using [FromForm] attribute, but neither covers file upload with structured metadata.

You can model such a request with a standard data transfer object, to which you add an IFormFile property for the uploaded file (or an IFormFile collection property if you want to support upload of multiple files):

public class UploadRequest
{
    [Required]
    public string? Title { get; set; }

    [Required]
    public string? Description { get; set; }

    public List<IFormFile> Files { get; set; } = new();
}

In the action method, you have to add the [FromForm] attribute to the corresponding parameter, so that all properties will be encoded as form data parts. If you don't, only the file(s) will be encoded as form data parts, and all the other properties will become query parameters:

[HttpPost]
[Consumes("multipart/form-data")]
public IActionResult UploadFile([FromForm] UploadRequest request)
{
    logger.LogInformation(
        "Request received, title: {Title}, description: {Description}, files: {Files}",
        request.Title,
        request.Description,
        request.Files.Count
    );

    foreach (var file in request.Files)
    {
        logger.LogInformation(
            "File uploaded, filename: {Filename}, size: {Size}",
            file.FileName,
            file.Length
        );
    }
    return Accepted();
}

The [Consumes("multipart/form-data")] is there to convince the .NET 9 OpenAPI document generation to use this encoding instead of application/x-www-form-urlencoded. Swashbuckle will use multipart/form-data even without it. The action method accepts both encodings, but you want to document multipart/form-data to have smaller requests for large binary files.

Swagger UI for file upload endpoint using multipart form data encoding

The form data model binder uses the data annotation attributes on model properties as expected. For example, if you don't include the required Title property in the request, it will return a standard 400 problem details response:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Title": ["The Title field is required."]
  },
  "traceId": "00-2174272844d25415474ddccf38817357-7ef12c8d03f8e197-00"
}

All HTTP clients should have good support for multipart form data requests, for example:

  • With curl, you can use the --form option to add properties or files:
    curl --request POST \
      --url https://localhost:7196/Upload \
      --header 'content-type: multipart/form-data' \
      --form Title=Title \
      --form Description=Description \
      --form Files=@"./Files/img.png" \
      --form Files=@"./Files/img.jpg"
    
  • With .NET HttpClient, you can use MultipartFormDataContent in combination with StringContent and StreamContent to compose the request:

    var content = new MultipartFormDataContent();
    if (!string.IsNullOrEmpty(title))
    {
        content.Add(new StringContent(title), "Title");
    }
    if (!string.IsNullOrEmpty(description))
    {
        content.Add(new StringContent(description), "Description");
    }
    
    var contentTypeProvider = new FileExtensionContentTypeProvider();
    foreach (var file in files)
    {
        var streamContent = new StreamContent(File.OpenRead(file));
        contentTypeProvider.TryGetContentType(file, out var contentType);
        streamContent.Headers.ContentType = new MediaTypeHeaderValue(
            contentType ?? "application/octet-stream"
        );
        content.Add(streamContent, "Files", file);
    }
    
    var response = await httpClient.PostAsync("/Upload", content);
    

You can find a working sample project in my GitHub repository. It includes a set of tests, which also show how such a multipart form data endpoint can be called from .NET. If you want to try generating a client from the OpenAPI specification, you can get it at the standard .NET 9 path: /openapi/v1.json.

If you want to support file upload in your ASP.NET Core web API REST service, you shouldn't require the files to be encoded in JSON fields (using Base64 or otherwise). This will only prevent you from using built-in server-side and client-side plumbing for file uploads, and make the requests larger than necessary. Use multipart form data encoding instead. You'll need to write less code and your requests will be smaller.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License