A guide to asynchronous file uploads in ASP.NET Web API RTM

Β· 1508 words Β· 8 minutes to read

It’s been 2 weeks since Web API has been released, and in a post highlighting the changes, I mentioned I will write an updated tutorial on file upload.

File upload is quite an important topic for Web API endpoitns or for API-driven applications, and sure enough there are some nice changes to the MultiPartFormDataStreamProvider, which, contrary to its versions in the Beta and RC allow easy and flexible model for dealing with uploaded data. On the other hand, these changes between Beta-RC-RTM, mean many of the upload-related posts found in the Web API community (this blog included) no longer work properly.

Let’s have a look at how you could now upload files to your ASP.NET Web API.

Sample controller πŸ”—

First, a small disclaimer, all the code here is .NET 4.0 specific - you could easily make it 4.5 by replacing the Tasks with async and ContinueWith with await. However, since most people still run 4.0, I thought it might be more apporpriate.

To be honest, in order to just upload the files to Web API, you need roughly 10 lines of code:

public class UploadingController : ApiController  
{  
public void Post()  
{  
if (Request.Content.IsMimeMultipartContent())  
{  
var streamProvider = new MultipartFormDataStreamProvider("c:/uploads/");  
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(t =>  
{  
if (t.IsFaulted || t.IsCanceled)  
throw new HttpResponseException(HttpStatusCode.InternalServerError);  
});  
else  
{  
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "This request is not properly formatted"));  
}  
}  
}  

In a gist, that’s all you need. Granted, this code is not very useful yet, but it will grab the files from the request and save them in a specified location. We will continue morphing the code into a better solution as we go on with this post.

There are several problems with it now:

  1. The files are saved using awkward names such as “BodyPart_3ded3dfb-40be-3153-b589-3401f93e90af” and so on.

  2. The path where we are saving is hardcoded, and that’s especially problematic on a shared host where you might not even know what’s your folder path

  3. There is a big chance, that we want to return something to the client, like a list of uploaded files and their related info (some might argue that’s not very REST-like, but let’s do it for the sake of an example). This was quite a problematic thing to achieve in RC/beta, so let’s explore how much easier it has become.

Correct filenames πŸ”—

The reason why body parts get saved on the disk with such random names is not an error or an accident. It has been designed like to that as a security measure - so that malicious uploaders wouldn’t compromise your system by uploading evil files disguised with friendly names.

Previously, one of the hacks I used, was to let the files save themselves with those randomly generated names, and then use File.Move to rename them. However, this is not the most efficient solution solution in the world by any stretch of the imagination.

However, now, you can easily derive from the default MultiPartFormDataStreamProvider and provide your own naming mechanism.

Let’s have a look at such simple example:

public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider  
{  
public CustomMultipartFormDataStreamProvider(string path) : base(path)  
{}

public override string GetLocalFileName(System.Net.Http.Headers.HttpContentHeaders headers)  
{  
var name = !string.IsNullOrWhiteSpace(headers.ContentDisposition.FileName) ? headers.ContentDisposition.FileName : "NoName";  
return name.Replace(""",string.Empty); //this is here because Chrome submits files in quotation marks which get treated as part of the filename and get escaped  
}  
}  

In this case, we implement our own naming convention, and tell the framework to pull out the name of the file from the Content-Disposition part of the request. this, in turn, will be the original name of the file as provided by the client.

This is the most basic example of all, but I hope you can imagine that you could do a lot more here - for exmaple implement your naming conventions and achieve file name patterns suitable for your application such as “UserA_avatar1.jpg” and so on.

You use it the same way as you would use the out-of-the-box MultipartFormDataStreamProvider:

if (Request.Content.IsMimeMultipartContent())  
{  
var streamProvider = new CustomMultipartFormDataStreamProvider("c:/uploads/");  
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(  
//omitted for brevity  
);  
}  

Saving relatively to the web app path πŸ”—

As mentioned, using a hardcoded path c:/uploads/ is far from ideal, especially in shared hosting.

You can easily save the files relatively to the web app though, by taking advantage of the fact that web hosted ASP.NET Web API runs on top of ASP.NET and reaching to the dreaded HttpContext:

string PATH = HttpContext.Current.Server.MapPath("~/uploads/");  
if (Request.Content.IsMimeMultipartContent())  
{  
var streamProvider = new CustomMultipartFormDataStreamProvider(PATH);  
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(  
//omitted for brevity  
);  
}  

This will upload files to the folder relative to your web app rooy (note: in this simple example the “uploads” fodler must exist, otherwise you will get a 500 exception!).

Returning some meaningful info πŸ”—

Finally, you may want to return some information about the uploaded files to the client. If that’s the case, one way to do it is to use a helper class (which was used already in the older post):

public class FileDesc  
{  
public string Name { get; set; }  
public string Path { get; set; }  
public long Size { get; set; }

public FileDesc(string n, string p, long s)  
{  
Name = n;  
Path = p;  
Size = s;  
}  
}  

This will be our DTO that will transport file information to the client. LEt’s modify our controller now.

public class UploadingController : ApiController  
{  
public Task<IEnumerable<FileDesc>> Post()  
{  
var folderName = "uploads";  
var PATH = HttpContext.Current.Server.MapPath("~/"+folderName);  
var rootUrl = Request.RequestUri.AbsoluteUri.Replace(Request.RequestUri.AbsolutePath, String.Empty);  
if (Request.Content.IsMimeMultipartContent())  
{  
var streamProvider = new CustomMultipartFormDataStreamProvider(PATH);  
var task = Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith<IEnumerable<FileDesc>>(t =>  
{

if (t.IsFaulted || t.IsCanceled)  
{  
throw new HttpResponseException(HttpStatusCode.InternalServerError);  
}

var fileInfo = streamProvider.FileData.Select(i => {  
var info = new FileInfo(i.LocalFileName);  
return new FileDesc(info.Name, rootUrl+"/"+folderName+"/"+info.Name, info.Length / 1024);  
});  
return fileInfo;  
});

return task;  
}  
else  
{  
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable, "This request is not properly formatted"));  
}  
}  
}  

So now, instead of void we return a List which can provide the client information about each of the uploaded files: its name, path and size.

Of course in this example we are returning the same name the client uploaded (so not too useful) but you can imagine in real life scenario if you had some custom naming going on it would be useful to communicate it to the client. Moreover, the path property will simply contain a link (url).

This is especially easy now in RTM, because MultipartFileStreamProvider now exposes a FileData collection which allows you to reach into the request and pull out information about local file names (as they have been saved), which we could then use to get even more information using FileInfo from System.IO.

Now if you run this, you will see things immediately get nice and informative.

And of course, correctly uploaded in the server’s file structure.

Calling it from the client πŸ”—

There are numerous way to call this controller from a client application to upload files. Ultimately, whereever you are capable of composing a MIME Multipart content HTTP message, you are able to call upload files to Web API.

I will show here 3 quick examples:

1. JavaScript
I have already discussed it before in great details, but you could drag-and-drop files to the browser. Then these files could be picked up arranged into a FormData object and sent via XHR:

function drop(evt) {  
evt.stopPropagation();  
evt.preventDefault();  
$(evt.target).removeClass('over');

var files = evt.originalEvent.dataTransfer.files;

if (files.length > 0) {  
if (window.FormData !== undefined) {  
var data = new FormData();  
for (i = 0; i < files.length; i++) { data.append("file"+i,files[i]); } $.ajax({ type: "POST", url: "/api/uploading", contentType: false, processData: false, data: data, success: function (res) { //do something with our ressponse } }); } else { alert("your browser sucks!"); } } } //attaching event $("#ulbox").bind("drop", drop); ``` **2. HTML**  
```html

This simple HTML form will allow you to select a file using the traditional browser dialog and send it to the Web API controller. You could also use the HTML5 multiple attribute to allow for multi-file selections.

<input name="somefile" type="file" multiple />  

In both cases, the upload works just fine.

3. .NET application
You might also have a need to upload files from a .NET application - i.e. your WPF app or a Windows service. In that case (assuming you already have a list of the filepaths in the files variable):

var message = new HttpRequestMessage();  
var content = new MultipartFormDataContent();

foreach (var file in files)  
{  
var filestream = new FileStream(file, FileMode.Open);  
var fileName = System.IO.Path.GetFileName(file);  
content.Add(new StreamContent(filestream), "file", fileName);  
}

message.Method = HttpMethod.Post;  
message.Content = content;  
message.RequestUri = new Uri("http://localhost:3128/api/uploading/");

var client = new HttpClient();  
client.SendAsync(message).ContinueWith(task =>  
{  
if (task.Result.IsSuccessStatusCode)  
{  
//do something with response  
}  
});  

The end result is the same as previously, a multipart message with files attached as stream data is sent to the controller. You can find more info in this post.

Summary & Source πŸ”—

I hope this post will help you a little bit with dealing with file uploads scenarios when using Web API endpoints. As usually I am including the source project, and if you have questions or comments - you know where to find me!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP