I'm working on a project in .NET 4 and Web API 2, adding a file upload field to an already-implemented controller. I've found that Web API doesn't support multipart/form-data POST requests by default, and I need to write my own formatter class to handle them. Fine.
Ideally, what I'd like to do is use the existing formatter to populate the model, then add the file data before returning the object. This file upload field is being attached to six separate models, all of which are very complex (classes containing lists of classes, enums, guids, etc.). I've run into a few snags...
I tried implementing it manually using the source code for FormUrlEncodedMediaTypeFormatter.cs as an example. I found that it constructs a list of KeyValue pairs for each field (which I can easily do), then parses them using FormUrlEncodedJson.Parse()
. I can't use FormUrlEncodedJson
, because it's (for some reason?) marked Internal
.
I started implementing my own parser, but when I hit about line 50, I thought to myself: I must be doing something wrong. There must be some way to populate the object with one of the existing Formatters, right? Surely they didn't expect us to write a new formatter for every single model or, even worse, writing our own more-fragile version of FormUrlEncodedJson.Parse()
?
What am I missing here? I'm stumped.
// Multipart/form-data formatter adapted from:
// http://stackoverflow.com/questions/17924655/how-create-multipartformformatter-for-asp-net-4-5-web-api
public class MultipartFormFormatter : FormUrlEncodedMediaTypeFormatter
{
private const string StringMultipartMediaType = "multipart/form-data";
//private const string StringApplicationMediaType = "application/octet-stream";
public MultipartFormFormatter()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(StringMultipartMediaType));
//this.SupportedMediaTypes.Add(new MediaTypeHeaderValue(StringApplicationMediaType));
}
public override bool CanReadType(Type type)
{
return true;
}
public override bool CanWriteType(Type type)
{
return false;
}
public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
var parts = await content.ReadAsMultipartAsync();
var obj = Activator.CreateInstance(type);
var propertiesFromObj = obj.GetType().GetRuntimeProperties().ToList();
// *****
// * Populate obj using FormUrlEncodedJson.Parse()? How do I do this?
// *****
foreach (var property in propertiesFromObj.Where(x => x.PropertyType == typeof(AttachedDocument)))
{
var file = parts.Contents.FirstOrDefault(x => x.Headers.ContentDisposition.Name.Contains(property.Name));
if (file == null || file.Headers.ContentLength <= 0) continue;
try
{
var fileModel = new AttachedDocument()
{
ServerFilePath = ReadToTempFile(file.ReadAsStreamAsync().Result),
};
property.SetValue(obj, fileModel);
}
catch (Exception e)
{
// TODO: proper error handling
}
}
return obj;
}
/// <summary>
/// Reads a file from the stream and writes it to a temporary directory
/// </summary>
/// <param name="input"></param>
/// <returns>The path of the written temporary file</returns>
private string ReadToTempFile(Stream input)
{
var fileName = Path.GetTempFileName();
var fileInfo = new FileInfo(fileName);
fileInfo.Attributes = FileAttributes.Temporary;
var buffer = new byte[16 * 1024];
using (var writer = File.OpenWrite(fileName))
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
writer.Write(buffer, 0, read);
}
}
return fileName;
}
}
EDIT: after stewing on this for way too many hours, I've come to the conclusion that what I want to do is basically impossible. After talking to my boss, we've decided the best alternative is to make a second controller that accepts a file and then associates it to the rest of the form data, and put the onus on the front-end developers to do much more work to support that scenario.
I've extremely disappointed in the designers of Web API for making such a common use-case so difficult (if at all possible!) to pull off.