Search code examples
c#asp.net-coreasp.net-core-2.0asp.net-core-3.0asp.net-apicontroller

How to write API action method that would accept a file?


I am trying to write an API endpoint that would allow a user to send a file to my server using Asp.NET Core 3.1.

I have the following action method that needs to respond to the client's request and process the file validation and store the file.

[HttpPost, Route("api/store")]
public async Task<IActionResult> Store([FromBody] MultipartFormDataContent content)
{
    // Validate the file, store it.

    return Ok();
}

I am trying to send the file from the client-side like this

using HttpClient client = new HttpClient();

var multiForm = new MultipartFormDataContent();
multiForm.Add(new StringContent("1/12/1.jpg"), "custom_file_name");
FileStream fs = System.IO.File.OpenRead("1.jpg");
multiForm.Add(new StreamContent(fs), "file", "1.jpg");

// send request to API
var url = "https://localhost:123/api/store";
var response = await client.PostAsync(url, multiForm);

But the server returns HTTP Code 404 Not Found.

How can I correctly tweak the Store method above so it accepts the file being sent by the client?


Solution

  • You shouldn't include api/ in the action method route. The controller's route is already prefixed with api. Also, api/[controller] means the controller route will be api/ControllerName, then your action will be api/ControllerName/store.

    If your controller class is named ThingsController then its route is api/things, and your Store action should be api/things/store.

    You don't need to use both HttpPost and Route attributes on an action. The Route attribute accepts all HTTP verbs, so use one or the other (typically just HttpPost or whatever HTTP verb you will be handling with the action method. So:

    [ApiController]
    [Route("api/[controller]")]
    public class ThingsController : ControllerBase
    {
        // POST api/things/store
        [HttpPost("store")]
        public async Task<IActionResult> Store([FromBody] MultipartFormDataContent content)
        {
            // Do stuff
        }
    }
    

    Also, the controller should be using IFormFile to deal with file uploads in your controller. The documentation is here:

    Upload files in ASP.NET Core .

    Your controller would accept a IFormFile files parameter rather than MultipartFormDataContent content.

    Example from MSDN, adapted to your action

    [HttpPost("store")]
    public async Task<IActionResult> Store(List<IFormFile> files)
    {
        // Get total size
        long size = files.Sum(f => f.Length);
    
        foreach (var formFile in files)
        {
            if (formFile.Length > 0)
            {
                // Gets a temporary file name
                var filePath = Path.GetTempFileName();
    
                // Creates the file at filePath and copies the contents
                using (var stream = System.IO.File.Create(filePath))
                {
                    await formFile.CopyToAsync(stream);
                }
            }
        }
    
        // Process uploaded files
        // Don't rely on or trust the FileName property without validation.
    
        return Ok(new { count = files.Count, size, filePath });
    }
    

    If you're dealing with a single file, you can just pass IFormFile file instead of a list, and get rid of the looping in the action method.

    I have not tested this code myself, wrote it on a mobile device. Hopefully it solves the problem though.