How to receive both File and data POSTed to an ASP.net core 2.1 Web-API endpoint

Viewed 1448

I'm trying to upload a file and some data (list of objects) to an ASP.net core 2.1 web-api endpoint.

This code below generates and sends the expected payload correctly:

const formData = new FormData()
formData.append('file', this.file)
formData.append('data', JSON.stringify(this.mappings))

axios.post('upload', formData)
// {headers: {'content-type': 'multipart/form-data'}} optional ???

The payload looks like this: (as seen in chrome dev-tools, network tab)

------WebKitFormBoundarydAP5tYG5GcFa4ivU
Content-Disposition: form-data; name="file"; filename="Master-List.xls"
Content-Type: application/vnd.ms-excel


------WebKitFormBoundarydAP5tYG5GcFa4ivU
Content-Disposition: form-data; name="data"

[{"required":true,"type":"field","value":"code","col":1,"text":"Item Code"},{"required":true,"type":"field","value":"name","col":2,"text":"Description"},{"required":true,"type":"field","value":"barcode","col":3,"text":"Barcode"},{"type":"field","value":"category","col":null,"text":"Category"},{"type":"field","value":"unit","col":null,"text":"Unit"},{"type":"location","value":1,"col":6,"text":"Store 1"}]
------WebKitFormBoundarydAP5tYG5GcFa4ivU--

The Web-api endpoint looks like this:

[HttpPost]
[Route("upload")]
//[Consumes("multipart/form-data")]
public Task<ActionResult> _upload([FromForm] Upload obj)
{
    // do something
    return Ok();
}

public class Upload
{
    public Field[] data { get; set; }
    public IFormFile file { get; set; }
}

public class Field
{
    public int col { get; set; }
    public string type { get; set; }
    public string value { get; set; }
}

I am able to receive the file, but the data is always empty.

Please what am I missing?

2 Answers

Although there appears to be no out-of-the-box solution for sending JSON data to an ASP.NET Core MVC endpoint, it is possible to to format the data as a set of key-value pairs that will be parsed into your Fields[] as expected. It looks a little bit like this (name = value):

data[<index>][<key>] = value

Here's a code sample that builds up the FormData using explicit property keys:

for (let i = 0; i < this.mappings.length; i++) {
    formData.append(`data[${i}][col]`, this.mappings[i].col);
    formData.append(`data[${i}][type]`, this.mappings[i].type);
    formData.append(`data[${i}][value]`, this.mappings[i].value);
}

This builds something that looks a bit like this:

data[0][col] = value
data[0][type] = value
data[0][value] = value

Here's an extended code sample that iterates over the properties, rather than using explicit keys:

for (let i = 0; i < this.mappings.length; i++) {
    for (let k in this.mappings[i]) {
        formData.append(`data[${i}][${k}]`, this.mappings[i][k]);
    }
}

This last sample assumes that you want all iterable properties of each mapping - there are a number of different ways to do this in JavaScript that I will not enumerate here. If your mapping is a simple object, the code I've provided should suffice.

Overall, this is more verbose both in the code and in the number of parts being send to the ASP.NET Core MVC endpoint. The benefit is that the endpoint can just receive a strongly-typed model without having to perform any extra parsing of JSON, etc.

Not ideal, but this modification works

  public class Upload
  {
    public string data { get; set; }
    public IFormFile file { get; set; }
  }

[HttpPost]
[Route("upload")]
[Consumes("multipart/form-data")]
public async Task<ActionResult> _upload([FromForm] Upload o)
{
  List<Field> x = JsonConvert.DeserializeObject<List<Field>>(o.data);

  return Ok();
}
Related