280

I'm using asp.net mvc 4 webapi beta to build a rest service. I need to be able to accept POSTed images/files from client applications. Is this possible using the webapi? Below is how action I am currently using. Does anyone know of an example how this should work?

[HttpPost]
public string ProfileImagePost(HttpPostedFile profileImage)
{
    string[] extensions = { ".jpg", ".jpeg", ".gif", ".bmp", ".png" };
    if (!extensions.Any(x => x.Equals(Path.GetExtension(profileImage.FileName.ToLower()), StringComparison.OrdinalIgnoreCase)))
    {
        throw new HttpResponseException("Invalid file type.", HttpStatusCode.BadRequest);
    }

    // Other code goes here

    return "/path/to/image.png";
}
Community
  • 1
  • 1
Phil
  • 4,134
  • 4
  • 23
  • 40
  • 3
    That only works with MVC not the WebAPI framework. – Phil Apr 25 '12 at 16:57
  • You should be able to just grab the item from `Request.Files` – Tejs Apr 25 '12 at 16:59
  • 7
    The ApiController does not contain the HttpRequestBase which has the Files property. It's Request object is based on the HttpRequestMessage class. – Phil Apr 25 '12 at 17:03

13 Answers13

406

I'm surprised that a lot of you seem to want to save files on the server. Solution to keep everything in memory is as follows:

[HttpPost("api/upload")]
public async Task<IHttpActionResult> Upload()
{
    if (!Request.Content.IsMimeMultipartContent())
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); 

    var provider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(provider);
    foreach (var file in provider.Contents)
    {
        var filename = file.Headers.ContentDisposition.FileName.Trim('\"');
        var buffer = await file.ReadAsByteArrayAsync();
        //Do whatever you want with filename and its binary data.
    }

    return Ok();
}
Vítor Oliveira
  • 1,851
  • 18
  • 23
Gleno
  • 16,621
  • 12
  • 64
  • 85
  • 41
    Keeping the files in memory can be useful if you don't want to spend diskspace. However, if you allow large files to be uploaded then keeping them in memory means your webserver will use up a lot of memory, which cannot be spend on keeping stuff around for other requests. This will cause problems on servers that work under high load. – Willem Meints Nov 28 '13 at 16:02
  • 25
    @W.Meints I understand the reasons for wanting to store data, but I don't understand why anyone would want to store uploaded data on server disk space. You should always keep file storage isolated from the web-server - even for smaller projects. – Gleno Nov 29 '13 at 16:50
  • True, it's a best practice to keep that stuff somewhere else. – Willem Meints Dec 02 '13 at 07:09
  • That is why the original example was uploading to 'App_Data' folder, that folder is inaccessible from web requests. – John C Jan 23 '14 at 04:34
  • 1
    Make sure your posted file size is less then 64k, the default behaviour is to ignore requests otherwise, I was stuck on this for a log time. – Gary Davies Apr 20 '14 at 12:26
  • I am using this code but I'm getting this error: "Error reading MIME multipart body part. {"Cannot access a disposed object."}" Any ideas why? – user441365 Jun 05 '14 at 16:32
  • @user441365 that sounds a lot like a TPL magic error; are you sure you didn't rewrite this without async? Any changes at all? – Gleno Jun 05 '14 at 23:49
  • What is the best way to check (in the foreach loop) that you're working with a file? I get two results and the first one is not a file but text/form data. – NickG Oct 23 '14 at 11:18
  • Good way how to exhaust your server RAM in one second...sorry, minus one for you. Buffering... – OSP Jan 06 '15 at 09:59
  • 3
    Unfortunately, the MultipartMemoryStreamProvider doesn't help if you want to read form data aswell. Wanted to create something like a MultipartFormDataMemoryStreamProvider but so many classes and helper classes are internal in the aspnetwebstack :( – martinoss Apr 08 '15 at 16:19
  • @martinoss the API is a complete mess. I hope they'll rework it in vNext. – Gleno Apr 09 '15 at 13:49
  • @martinoss Have you found a solution to use the stream provider and read the form data? – Adrian Feb 03 '16 at 14:38
  • @adiii I solved the problem using the accepted answer from this [stackoverflow post](http://stackoverflow.com/questions/19723064/webapi-formdata-upload-to-db-with-extra-parameters) as starting point – martinoss Feb 04 '16 at 08:54
  • 9
    `File.WriteAllBytes(filename, buffer);` to write it to a file – pomber Oct 26 '16 at 19:15
  • until `await Request.Content.ReadAsMultipartAsync(provider);` line executed, can't we access formdata values ?? – NoughT Nov 27 '16 at 16:36
  • For my application I needed to accept the request, inspect/modify the json payload and the forward the request to the final destination. Trying to use the in memory approach threw the error "The stream does not support concurrent IO read or write operations. The only way to stop the error was to change to storing the file locally and then upload to the final destination. – Jim Sep 07 '18 at 17:03
  • Is this .net core? – Demodave Mar 18 '21 at 16:10
177

see http://www.asp.net/web-api/overview/formats-and-model-binding/html-forms-and-multipart-mime#multipartmime, although I think the article makes it seem a bit more complicated than it really is.

Basically,

public Task<HttpResponseMessage> PostFile() 
{ 
    HttpRequestMessage request = this.Request; 
    if (!request.Content.IsMimeMultipartContent()) 
    { 
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); 
    } 

    string root = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/uploads"); 
    var provider = new MultipartFormDataStreamProvider(root); 

    var task = request.Content.ReadAsMultipartAsync(provider). 
        ContinueWith<HttpResponseMessage>(o => 
    { 

        string file1 = provider.BodyPartFileNames.First().Value;
        // this is the file name on the server where the file was saved 

        return new HttpResponseMessage() 
        { 
            Content = new StringContent("File uploaded.") 
        }; 
    } 
    ); 
    return task; 
} 
Jon
  • 9,156
  • 9
  • 56
  • 73
Mike Wasson
  • 6,572
  • 2
  • 24
  • 20
  • 5
    What is the benefit of using a Task to read just one file? Genuine question, I'm just beginning to use Tasks. From my current understanding, this code is really suited for when uploading more than one file correct? – Chris Aug 09 '12 at 10:45
  • 1
    There's no synchronous API here, you really need to use the task. I've been trying to circumvent the whole "ContinueWith" deal by calling Wait() on the result of ReadAsMultipartAsync, but that just hangs the whole thing. This works fine. – Dave Van den Eynde Aug 27 '12 at 17:36
  • 49
    MultipartFormDataStreamProvider doesn't have BodyPartFileNames property any more (in WebApi RTM). See http://www.asp.net/web-api/overview/working-with-http/sending-html-form-data,-part-2 – Shrike Dec 03 '12 at 15:08
  • 6
    Guys, can any of you please shed some light on why we can't simply access files using HttpContext.Current.Request.Files and instead need to use this fancy MultipartFormDataStreamProvider? The full question: http://stackoverflow.com/questions/17967544. – niaher Aug 01 '13 at 03:19
  • This is the code snippet for ASP.NET 4.0. There is another code snippet if you are targeting .NET Framework 4.5, which supports the `async` and `await` keywords. – zacharydl Dec 04 '13 at 19:50
  • I pasted that code into my ApiController, doesnt compile – Gary Davies Apr 20 '14 at 11:41
  • 7
    Files are being saved as **BodyPart_8b77040b-354b-464c-bc15-b3591f98f30f**. Should not they be saved like *pic.jpg* exactly as it was on the client? – lbrahim Aug 13 '14 at 12:35
  • what does this mean if (!request.Content.IsMimeMultipartContent()) ? – Thomas Dec 28 '14 at 17:53
  • how to change file name before saving? – Jitendra Pancholi Apr 07 '15 at 08:45
  • Jitendra you can System.IO.File.Rename there I guess. Still I find this too complicated, bizarre and unsofisticated than MVC default approach. Perhaps we could possibly inherit that MultipartDataStreamProvider to have a custom save method? – Felype May 27 '15 at 02:20
  • 11
    `MultipartFormDataStreamProvider` doesn't expose `BodyPartFileNames` property any more, I used `FileData.First().LocalFileName` instead. – Chtioui Malek Nov 17 '15 at 14:23
  • 1
    @JitendraPancholi you need to create a custom MultipartFormDataStreamProvider where you override the method GetLocaleFileName (responsible for saved name). Here is an example in the section _correct filenames_ [Example](http://www.strathweb.com/2012/08/a-guide-to-asynchronous-file-uploads-in-asp-net-web-api-rtm/) – Adrian Feb 04 '16 at 15:06
  • how to do it along with other form data like name,address,image in single post request object – Dragon Apr 14 '17 at 12:42
119

See the code below, adapted from this article, which demonstrates the simplest example code I could find. It includes both file and memory (faster) uploads.

public HttpResponseMessage Post()
{
    var httpRequest = HttpContext.Current.Request;
    if (httpRequest.Files.Count < 1)
    {
        return Request.CreateResponse(HttpStatusCode.BadRequest);
    }

    foreach(string file in httpRequest.Files)
    {
        var postedFile = httpRequest.Files[file];
        var filePath = HttpContext.Current.Server.MapPath("~/" + postedFile.FileName);
        postedFile.SaveAs(filePath);
        // NOTE: To store in memory use postedFile.InputStream
    }

    return Request.CreateResponse(HttpStatusCode.Created);
}
Brent Matzelle
  • 4,073
  • 3
  • 28
  • 27
  • 27
    HttpContext.Current is null when WebAPI is hosted in OWIN which is a self hosting container. – Zach Apr 23 '14 at 05:58
  • I get this: 'System.Web.HttpContextBase' does not contain a definition for 'Current' – msysmilu Oct 29 '14 at 17:29
  • 1
    Fixed it like so: var httpRequest = System.Web.HttpContext.Current.Request; – msysmilu Oct 29 '14 at 17:30
  • 8
    Do not use System.Web in WebAPI unless you absolutely have to. – Kugel Sep 16 '15 at 07:19
  • 3
    Sure thing, System.Web is tightly coupled to IIS. If you're working within OWIN piple line or .Net Core those API will not be available when running under linux or self-hosted. – Kugel Nov 28 '16 at 07:32
  • 3
    Great answer. Just one detail: if you are uploading from a HTML page, the tag *must* have a "name" attribute, or the file will not be present in HttpContext.Current.Request.Files. – GBU Jun 08 '18 at 21:19
33

The ASP.NET Core way is now here:

[HttpPost("UploadFiles")]
public async Task<IActionResult> Post(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    // full path to file in temp location
    var filePath = Path.GetTempFileName();

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                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});
}
Matt Frear
  • 52,283
  • 12
  • 78
  • 86
  • The question is specifically tagged `asp.net-mvc-4`, so this answer could be confusing especially to someone new to .NET – G. Stoynev Oct 24 '20 at 17:39
17

Here is a quick and dirty solution which takes uploaded file contents from the HTTP body and writes it to a file. I included a "bare bones" HTML/JS snippet for the file upload.

Web API Method:

[Route("api/myfileupload")]        
[HttpPost]
public string MyFileUpload()
{
    var request = HttpContext.Current.Request;
    var filePath = "C:\\temp\\" + request.Headers["filename"];
    using (var fs = new System.IO.FileStream(filePath, System.IO.FileMode.Create))
    {
        request.InputStream.CopyTo(fs);
    }
    return "uploaded";
}

HTML File Upload:

<form>
    <input type="file" id="myfile"/>  
    <input type="button" onclick="uploadFile();" value="Upload" />
</form>
<script type="text/javascript">
    function uploadFile() {        
        var xhr = new XMLHttpRequest();                 
        var file = document.getElementById('myfile').files[0];
        xhr.open("POST", "api/myfileupload");
        xhr.setRequestHeader("filename", file.name);
        xhr.send(file);
    }
</script>
James Lawruk
  • 30,112
  • 19
  • 130
  • 137
13

I used Mike Wasson's answer before I updated all the NuGets in my webapi mvc4 project. Once I did, I had to re-write the file upload action:

    public Task<HttpResponseMessage> Upload(int id)
    {
        HttpRequestMessage request = this.Request;
        if (!request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.UnsupportedMediaType));
        }

        string root = System.Web.HttpContext.Current.Server.MapPath("~/App_Data/uploads");
        var provider = new MultipartFormDataStreamProvider(root);

        var task = request.Content.ReadAsMultipartAsync(provider).
            ContinueWith<HttpResponseMessage>(o =>
            {
                FileInfo finfo = new FileInfo(provider.FileData.First().LocalFileName);

                string guid = Guid.NewGuid().ToString();

                File.Move(finfo.FullName, Path.Combine(root, guid + "_" + provider.FileData.First().Headers.ContentDisposition.FileName.Replace("\"", "")));

                return new HttpResponseMessage()
                {
                    Content = new StringContent("File uploaded.")
                };
            }
        );
        return task;
    }

Apparently BodyPartFileNames is no longer available within the MultipartFormDataStreamProvider.

Steve Stokes
  • 1,200
  • 18
  • 36
  • In WebApi RTM the BodyPartFileNames has been changed to FileData. See updated example at http://www.asp.net/web-api/overview/working-with-http/sending-html-form-data,-part-2 – Mark van Straten Jun 13 '13 at 13:22
  • Why not just use System.Web.HttpContext.Current.Request.Files collection? – Alexander Selishchev Oct 09 '13 at 11:43
  • I'm thinking of using your method with 2 reservations: 1) Doesn't this write twice: i) in `ReadAsMultipartAsync` and ii) In `File.Move`? 2) Could you do `async File.Move`? – seebiscuit Oct 24 '17 at 18:53
  • 1) I didn't have issues with two writes, is the url being called twice? 2) you could do Task.Run(() => { File.Move(src, dest); }); – Steve Stokes Oct 25 '17 at 19:55
10

Toward this same directions, I'm posting a client and server snipets that send Excel Files using WebApi, c# 4:

public static void SetFile(String serviceUrl, byte[] fileArray, String fileName)
{
    try
    {
        using (var client = new HttpClient())
        {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                using (var content = new MultipartFormDataContent())
                {
                    var fileContent = new ByteArrayContent(fileArray);//(System.IO.File.ReadAllBytes(fileName));
                    fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                    {
                        FileName = fileName
                    };
                    content.Add(fileContent);
                    var result = client.PostAsync(serviceUrl, content).Result;
                }
        }
    }
    catch (Exception e)
    {
        //Log the exception
    }
}

And the server webapi controller:

public Task<IEnumerable<string>> Post()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        string fullPath = HttpContext.Current.Server.MapPath("~/uploads");
        MyMultipartFormDataStreamProvider streamProvider = new MyMultipartFormDataStreamProvider(fullPath);
        var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t =>
        {
            if (t.IsFaulted || t.IsCanceled)
                    throw new HttpResponseException(HttpStatusCode.InternalServerError);

            var fileInfo = streamProvider.FileData.Select(i =>
            {
                var info = new FileInfo(i.LocalFileName);
                return "File uploaded as " + info.FullName + " (" + info.Length + ")";
            });
            return fileInfo;

        });
        return task;
    }
    else
    {
        throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "Invalid Request!"));
    }
}

And the Custom MyMultipartFormDataStreamProvider, needed to customize the Filename:

PS: I took this code from another post http://www.codeguru.com/csharp/.net/uploading-files-asynchronously-using-asp.net-web-api.htm

public class MyMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
{
    public MyMultipartFormDataStreamProvider(string path)
        : base(path)
    {

    }

    public override string GetLocalFileName(System.Net.Http.Headers.HttpContentHeaders headers)
    {
        string fileName;
        if (!string.IsNullOrWhiteSpace(headers.ContentDisposition.FileName))
        {
            fileName = headers.ContentDisposition.FileName;
        }
        else
        {
            fileName = Guid.NewGuid().ToString() + ".data";
        }
        return fileName.Replace("\"", string.Empty);
    }
}
Daniel Melo
  • 161
  • 1
  • 4
  • Could you show how do you call you `static method SetFile` in your Controller? –  Feb 26 '15 at 15:43
  • This is a good answer. Extending the base provider like this also enables you to control the stream and gives you more flexibility than providing just a `path` i.e. cloud storage. – Phil Cooper Feb 26 '16 at 07:37
6
[HttpPost]
public JsonResult PostImage(HttpPostedFileBase file)
{
    try
    {
        if (file != null && file.ContentLength > 0 && file.ContentLength<=10485760)
        {
            var fileName = Path.GetFileName(file.FileName);                                        

            var path = Path.Combine(Server.MapPath("~/") + "HisloImages" + "\\", fileName);

            file.SaveAs(path);
            #region MyRegion
            ////save imag in Db
            //using (MemoryStream ms = new MemoryStream())
            //{
            //    file.InputStream.CopyTo(ms);
            //    byte[] array = ms.GetBuffer();
            //} 
            #endregion
            return Json(JsonResponseFactory.SuccessResponse("Status:0 ,Message: OK"), JsonRequestBehavior.AllowGet);
        }
        else
        {
            return Json(JsonResponseFactory.ErrorResponse("Status:1 , Message: Upload Again and File Size Should be Less Than 10MB"), JsonRequestBehavior.AllowGet);
        }
    }
    catch (Exception ex)
    {

        return Json(JsonResponseFactory.ErrorResponse(ex.Message), JsonRequestBehavior.AllowGet);

    }
}
gunr2171
  • 16,104
  • 25
  • 61
  • 88
user3722373
  • 61
  • 1
  • 1
4

Here are two ways to accept a file. One using in memory provider MultipartMemoryStreamProvider and one using MultipartFormDataStreamProvider which saves to a disk. Note, this is only for one file upload at a time. You can certainty extend this to save multiple-files. The second approach can support large files. I've tested files over 200MB and it works fine. Using in memory approach does not require you to save to disk, but will throw out of memory exception if you exceed a certain limit.

private async Task<Stream> ReadStream()
{
    Stream stream = null;
    var provider = new MultipartMemoryStreamProvider();
    await Request.Content.ReadAsMultipartAsync(provider);
    foreach (var file in provider.Contents)
    {
        var buffer = await file.ReadAsByteArrayAsync();
        stream = new MemoryStream(buffer);
    }

    return stream;
}

private async Task<Stream> ReadLargeStream()
{
    Stream stream = null;
    string root = Path.GetTempPath();
    var provider = new MultipartFormDataStreamProvider(root);
    await Request.Content.ReadAsMultipartAsync(provider);
    foreach (var file in provider.FileData)
    {
        var path = file.LocalFileName;
        byte[] content = File.ReadAllBytes(path);
        File.Delete(path);
        stream = new MemoryStream(content);
    }

    return stream;
}
Ciarán Bruen
  • 5,221
  • 13
  • 59
  • 69
Filix Mogilevsky
  • 727
  • 8
  • 13
4

This question has lots of good answers even for .Net Core. I was using both Frameworks the provided code samples work fine. So I won't repeat it. In my case the important thing was how to use File upload actions with Swagger like this:

File upload button in Swagger

Here is my recap:

ASP .Net WebAPI 2

  • To upload file use: MultipartFormDataStreamProvider see answers here
  • How to use it with Swagger

.NET Core

Major
  • 5,948
  • 2
  • 45
  • 60
  • 1
    Sadly the part of the answer for `IFormFile` is for an outdated version of the Swashbuckle package. It does not use the current Apply method `public void Apply(OpenApiOperation operation, OperationFilterContext context)` – The Thirsty Ape Jul 11 '22 at 22:01
1

I had a similar problem for the preview Web API. Did not port that part to the new MVC 4 Web API yet, but maybe this helps:

REST file upload with HttpRequestMessage or Stream?

Please let me know, can sit down tomorrow and try to implement it again.

Community
  • 1
  • 1
Remy
  • 12,555
  • 14
  • 64
  • 104
1

API Controller :

[HttpPost]
public HttpResponseMessage Post()
{
    var httpRequest = System.Web.HttpContext.Current.Request;

    if (System.Web.HttpContext.Current.Request.Files.Count < 1)
    {
        //TODO
    }
    else
    {

    try
    { 
        foreach (string file in httpRequest.Files)
        { 
            var postedFile = httpRequest.Files[file];
            BinaryReader binReader = new BinaryReader(postedFile.InputStream);
            byte[] byteArray = binReader.ReadBytes(postedFile.ContentLength);

        }

    }
    catch (System.Exception e)
    {
        //TODO
    }

    return Request.CreateResponse(HttpStatusCode.Created);
}
Ciarán Bruen
  • 5,221
  • 13
  • 59
  • 69
Tiago Medici
  • 1,944
  • 22
  • 22
0

Complementing Matt Frear's answer - This would be an ASP NET Core alternative for reading the file directly from Stream, without saving&reading it from disk:

public ActionResult OnPostUpload(List<IFormFile> files)
    {
        try
        {
            var file = files.FirstOrDefault();
            var inputstream = file.OpenReadStream();

            XSSFWorkbook workbook = new XSSFWorkbook(stream);

            var FIRST_ROW_NUMBER = {{firstRowWithValue}};

            ISheet sheet = workbook.GetSheetAt(0);
            // Example: var firstCellRow = (int)sheet.GetRow(0).GetCell(0).NumericCellValue;

            for (int rowIdx = 2; rowIdx <= sheet.LastRowNum; rowIdx++)
               {
                  IRow currentRow = sheet.GetRow(rowIdx);

                  if (currentRow == null || currentRow.Cells == null || currentRow.Cells.Count() < FIRST_ROW_NUMBER) break;

                  var df = new DataFormatter();                

                  for (int cellNumber = {{firstCellWithValue}}; cellNumber < {{lastCellWithValue}}; cellNumber++)
                      {
                         //business logic & saving data to DB                        
                      }               
                }
        }
        catch(Exception ex)
        {
            throw new FileFormatException($"Error on file processing - {ex.Message}");
        }
    }
Pedro Coelho
  • 1,411
  • 3
  • 18
  • 31