Uploading files using ASP.NET Web Api

I have once again been tasked with writing a ASP.NET Wep Api action to handle the upload of a file. Or rather a form including a file. And once again, I Googled it looking for good solutions. And once again I got the same code thrown in my face over and over again. It seems to be the only one out there. And it isn’t what I want…

The code I am talking about is the one available at http://www.asp.net/web-api/overview/advanced/sending-html-form-data,-part-2. And if you don’t want to go and read that, the general gist is this

if (!Request.Content.IsMimeMultipartContent())
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);

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

await Request.Content.ReadAsMultipartAsync(provider);

/* Get files using provider.FileData */

Ok, so it does what it is supposed to do. It gets the posted files… But once in a while, getting the job done isn’t enough…

If you look at the code above, there is a line that references HttpContext.Current.Server.MapPath(). It bugs me. For two reasons.

First of all, taking a dependency on HttpContext.Current means taking a dependency on System.Web, which in turn means taking a dependency on something I might not want in some cases.

Secondly, it also indicates that the code will be saving things on my hard-drive. Something that I definitely don’t want in this case. Why would I want to save the posted files on my drive before I can use them? And what happens if something fails in my controller? Well, the answer to the lest question is that they will be sitting there on the disk waiting for some nice person to come by and manually delete them…

So, is there a way around this? Yes there is. And it isn’t even that hard…

Instead of instantiating a MultipartFormDataStreamProvider and passing it to the ReadAsMultipartAsync() method, we can just call the ReadAsMultipartAsync() without passing any parameters. This will return a MultipartMemoryStreamProvider instead of populating the MultipartFormDataStreamProvider like it did in the previous example. And as you can probably guess from the naming of the class, it uses MemoryStreams instead of writing things to disk…

Caution: Just a little caution here. If you are uploading large files, putting them all in-memory might not be such a great idea. In those cases, placing them on disk actually makes a bit of sense I guess. But for smaller files, storing them on permanent storage seems to offer very little.

Ok, after that little warning, let’s carry on!

The main problem with using the MultipartMemoryStreamProvider instead is that it doesn’t have quite as nice an API to work with. Instead, it exposes all posted data as a collection of HttpContent objects. This is a little bit more cumbersome to work with, but it still works fine. All you need to do is to cycle through them all, figure out which are files, and which are “regular” data.

As I got this working, I decided to roll it into a reusable piece of code as I have been requested to do this more than once.

So I started out by defining the “return” value. The value to get after the parsing has completed. And I decided to build something like this

public class HttpPostedData
{
public HttpPostedData(IDictionary<string, HttpPostedField> fields, IDictionary<string, HttpPostedFile> files)
{
Fields = fields;
Files = files;
}

public IDictionary<string, HttpPostedField> Fields { get; private set; }
public IDictionary<string, HttpPostedFile> Files { get; private set; }
}


A very simple class that encapsulates a dictionary containing the posted fields, and one containing the posted files. The HttpPostedFile and HttpPostedField classes are simple DTOs that look like this

public class HttpPostedFile
{
public HttpPostedFile(string name, string filename, byte[] file)
{
//Property Assignment
}

public string Name { get; private set; }
public string Filename { get; private set; }
public byte[] File { private set; get; }
}

public class HttpPostedField
{
public HttpPostedField(string name, string value)
{
//Property Assignment
}

public string Name { get; private set; }
public string Value { get; private set; }
}

Ok, that was pretty simple! The next thing is to parse the incoming request and create a response like that. In a reusable fashion…

I decided to create an extension method called ParseMultipartAsync. The signature looks like this Task<HttpPostedData> ParseMultipartAsync(this HttpContent postedContent). And as you can see from the signature, it is async using Tasks.

Inside the method I use ReadAsMultipartAsync() to asyncly (is that even a word?) parse the incoming content and get hold of one of the fabled MultipartMemoryStreamProvider instances.

Next, I new up a couple of generic dictionaries to hold the posted field values.

Tip: By using the constructor that takes a IEqualityComparer<>, and passing in StringComparer.InvariantCultureIgnoreCase I get case-insensitive key comparison, which can be really helpful in this case. And a lot of other cases…

I then loop through each of the HttpContent items in the provider. And for each one of them I determine whether it is a file or a field by looking at the FileName property of the ContentDisposition header. If it isn’t null, it is a file. If it is, it isn’t.

I then populate the corresponding dictionary with a new HttpPostedXXX instance. And then finally return a new HttpPostedData.

The code for that extension method looks like this

public static async Task<HttpPostedData> ParseMultipartAsync(this HttpContent postedContent)
{
var provider = await postedContent.ReadAsMultipartAsync();

var files = new Dictionary<string, HttpPostedFile>(StringComparer.InvariantCultureIgnoreCase);
var fields = new Dictionary<string, HttpPostedField>(StringComparer.InvariantCultureIgnoreCase);

foreach (var content in provider.Contents)
{
var fieldName = content.Headers.ContentDisposition.Name.Trim('"');
if (!string.IsNullOrEmpty(content.Headers.ContentDisposition.FileName))
{
var file = await content.ReadAsByteArrayAsync();
var fileName = content.Headers.ContentDisposition.FileName.Trim('"');
files.Add(fieldName, new HttpPostedFile(fieldName, fileName, file));
}
else
{
var data = await content.ReadAsStringAsync();
fields.Add(fieldName, new HttpPostedField(fieldName, data));
}
}

return new HttpPostedData(fields, files);
}

It isn’t really that hard. but having it as an extension method makes it easy to use, and reuse.

Using it looks like this

[Route("upload")]
[HttpPost]
public async Task<HttpResponseMessage> UploadFile(HttpRequestMessage request)
{
if (!request.Content.IsMimeMultipartContent())
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);

var data = await Request.Content.ParseMultipartAsync();

if (data.Files.ContainsKey("file"))
{
// Handle the uploaded file "file"
// Ex: var fileName = data.Files["file"].Filename;
}

if (data.Fields.ContainsKey("description"))
{
// Handle the uploaded field "description"
// Ex: var description = data.Fields["description"].Value;
}

// Do something with the posted data

return new HttpResponseMessage(HttpStatusCode.OK);
}

As you can see, I take in a HttpRequestMessage instance as a parameter instead of specific parameters from the post. This gives us access to the request message in its “raw” form. it is the same that is available through the base class’ Request property. I just find it a bit more explicit to handle it like this. Through that parameter I can then access the HttpContent through the Content property and call the extension method, getting the required data in a simple way.

That was it for this time! Shorter than normal, but hopefully as useful!

And code? Of course! Here it is: DarksideCookie.AspNet.WebApi.FileUploads.zip (65.29 kb)

The source includes a web project with a simple AngularJS based form doing the upload. It doesn’t actually do anything with the uploaded file, but at least you can add a breakpoint and see it work… Winking smile

Cheers!

Comments (13) -

Finally some usefull code. Thank you very much! Helped me a lot!

Thanks for a great article, this is exactly what I want.

Great Article. How can this upload Api be called from a .net console applicaiton ?

Great article. Thanks!

Great article!!
I want to  save the file but this additional code is not working can you please help
var filePath = HttpContext.Current.Server.MapPath("~/Logo/" + fileName);
postedFile.SaveAs(filePath);

thanks

Thank you so much. This code help me solve a major headache.

Really interesting, it helped me a lot, thanks

Phanu282003 2/16/2017 10:30:59 AM

Greate article. It help me too.

how to upload large files like videos..please help

Great article. thank you writing for all of us.

Cedrick Higman 5/9/2017 12:07:22 AM

Merely  wanna  tell  that this is  handy , Thanks for taking your time to write this.

anabolicke steroidy 5/21/2017 5:23:50 PM

You actually make it seem so easy with your presentation but I find this matter to be really something which I think I would never understand. It seems too complicated and very broad for me. I'm looking forward for your next post, I will try to get the hang of it!

I like this post, enjoyed this one thanks  for posting .

Pingbacks and trackbacks (1)+

Add comment