I had built a web API endpoint for file upload before in the past using a custom media type formatter. Last week I had to build a web API endpoint for another project. This time, I decided to implement this a little differently by reading the file in the content from request.
The old way
In HTTP, media types describe the format of the message body and determine how Web API serialises and deserialises the HTTP message body. By default, the format can be either JSON, XML, BSON, or form-urlencoded data, see here for more details. As mentioned above, a custom media type formatter is required. This custom media type formatter adds two MIME types to the headers, namely application/octet-stream and multipart/form-data.
To add a custom media type formatter, simply create a class that derives from MediaTypeFormatter class.
public class BinaryMediaTypeFormatter : MediaTypeFormatter { private static readonly Type SupportedType = typeof(byte[]); public BinaryMediaTypeFormatter() { this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream")); this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); } /// <inheritdoc /> public override bool CanReadType(Type type) { return type == SupportedType; } /// <inheritdoc /> public override bool CanWriteType(Type type) { return type == SupportedType; } public override Task<object> ReadFromStreamAsync( Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) { var taskSource = new TaskCompletionSource<object>(); try { var ms = new MemoryStream(); readStream.CopyTo(ms); taskSource.SetResult(ms.ToArray()); } catch (Exception e) { taskSource.SetException(e); } return taskSource.Task; } }
Then, insert this custom media type formatter in the global configuration in the Application_Start() in Global.asax.cs,
GlobalConfiguration.Configuration.Formatters.Insert(0, new BinaryMediaTypeFormatter());
Finally, in the API controller, add a new action with an array of byte as a parameter.
[HttpPost] public HttpResponseMessage Upload( byte[] file) { // Add logic to process file. return this.Request.CreateResponse(HttpStatusCode.OK); }
There are pros and cons when doing it this way.
Pros:
- Insert this custom media type formatter once in the global configuration and it will automatically maps to the matching parameter name of type byte array.
Cons:
- Cannot (more like I haven’t found a way yet) get file name.
Another way
The file can be retrieved from the Request content. The file name can also retrieved too.
In the action in the API controller
[HttpPost] public async Task<HttpResponseMessage> Upload() { if (!Request.Content.IsMimeMultipartContent()) { throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); } var provider = new MultipartMemoryStreamProvider(); await Request.Content.ReadAsMultipartAsync(provider); var file = provider.Contents.FirstOrDefault(); var fileName = string.Empty; if (file == null) { this.ModelState.AddModelError("file", "No file provided."); } else { fileName = file.Headers.ContentDisposition.FileName.Trim('"'); } // Add logic to process file. return this.Request.CreateResponse(HttpStatusCode.OK); }
There are also pros and cons when doing it this way.
Pros:
- No need to create custom media type formatter.
- Easy to get file name.
Cons:
- Cannot have named parameter for the posted file
- Repeat code in every action that posts file (though this could be solved easily by adding all that logic to get file into an extension method).
Summary
In my opinion, either way is fine. I, however, prefer the old way as it allows named parameter for the file. As with the file name, it can be easily solved by adding an additional parameter for the file name.