Web Api Generic MediaTypeFormatter for File Upload

I’m currently working on a personal project which uses Asp.net WebApi and .net 4.5. I have found several nice examples utilizing the Multipartformdatastreamprovider. I discovered quickly though that using this in a controller was going to multiply boilerplate code. It would be much more efficient to use a custom MediaTypeFormatter to handle the form data and pass me back the information I need in a memorystream which can be directly saved.

Jflood.net Original Code

Jflood.net provides a nice starting point to what I needed to do. However, I had a few more requirements of mine which included a JSON payload in the datafield. I also extended his ImageMedia class into a generic FileUpload class and exposed a few simple methods.

1
2
3
4
5
6
7
8
 public HttpResponseMessage Post(FileUpload<font> upload)
        {
            var FilePath = "Path";
            upload.Save(FilePath); //save the buffer
            upload.Value.Insert(); //save font object to DB
 
            return Request.CreateResponse(HttpStatusCode.OK, upload.Value);
        }

This is the file upload class. Like Jflood’s it includes a payload for the file to be stored and written in memory. I have added a simple save method which performs a few checks and saves to disk. I also have some project specific code that checks if the T type has a property named filename, if so it passes the name to it. Since my Value field is of Type T it is automatically deserialized by Json.net.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    public class FileUpload<T>
    {
        private readonly string _RawValue;
 
        public T Value { get; set; }
        public string FileName { get; set; }
        public string MediaType { get; set; }
        public byte[] Buffer { get; set; }
 
        public FileUpload(byte[] buffer, string mediaType, string fileName, string value)
        {
            Buffer = buffer;
            MediaType = mediaType;
            FileName = fileName.Replace("\"","");
            _RawValue = value;
 
            Value = JsonConvert.DeserializeObject<T>(_RawValue);
        }
 
        public void Save(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            var NewPath = Path.Combine(path, FileName);
            if (File.Exists(NewPath))
            {
                File.Delete(NewPath);
            }
 
            File.WriteAllBytes(NewPath, Buffer);
 
            var Property = Value.GetType().GetProperty("FileName");
            Property.SetValue(Value,FileName, null);
        }
    }

This is essentially the same thing as jflood is doing, however, I have added a section to parse out my “data” field that contains json.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
    public class FileMediaFormatter<T> : MediaTypeFormatter
    {
 
        public FileMediaFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
        }
 
        public override bool CanReadType(Type type)
        {
            return type == typeof(FileUpload<T>);
        }
 
        public override bool CanWriteType(Type type)
        {
            return false;
        }
 
        public async override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
 
            if (!content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
 
            var Parts = await content.ReadAsMultipartAsync();
            var FileContent = Parts.Contents.First(x =>
                SupportedMediaTypes.Contains(x.Headers.ContentType));
 
            var DataString = "";
            foreach (var Part in Parts.Contents.Where(x => x.Headers.ContentDisposition.DispositionType == "form-data" 
                                                        && x.Headers.ContentDisposition.Name == "\"data\""))
            {
                var Data = await Part.ReadAsStringAsync();
                DataString = Data;
            }
 
            string FileName = FileContent.Headers.ContentDisposition.FileName;
            string MediaType = FileContent.Headers.ContentType.MediaType;
 
            using (var Imgstream = await FileContent.ReadAsStreamAsync())
            {
                byte[] Imagebuffer = ReadFully(Imgstream);
                return new FileUpload<T>(Imagebuffer, MediaType,FileName ,DataString);
            }
        }
 
        private byte[] ReadFully(Stream input)
        {
            var Buffer = new byte[16 * 1024];
            using (var Ms = new MemoryStream())
            {
                int Read;
                while ((Read = input.Read(Buffer, 0, Buffer.Length)) > 0)
                {
                    Ms.Write(Buffer, 0, Read);
                }
                return Ms.ToArray();
            }
        }
 
 
    }

Finally in your application_start you must include this line below.

1
GlobalConfiguration.Configuration.Formatters.Add(new FileMediaFormatter<font>());

Now it is really simple to accept uploads from other controllers. Be sure to tweak the MIME type for your own needs. Right now this only accepts application/octet-stream but it can easily accept other formats by adding other MIME types.

Update – How to add model validation support.

Quick update for the fileupload class. I’ve seen a few posts on stackoverflow about how to not only deserialize your object using a method such as mine, but also maintain the data annotation validation rules. That turns out to be pretty easy to do. Basically combine reflection and TryValidateProperty() and you can validate properties on demand. In my example it shows how you can get the validation messages. It simply puts them into an array. Here is sample below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class FileUpload<T>
    {
        private readonly string _RawValue;
 
        public T Value { get; set; }
        public string FileName { get; set; }
        public string MediaType { get; set; }
        public byte[] Buffer { get; set; }
 
        public List<ValidationResult> ValidationResults = new List<ValidationResult>(); 
 
        public FileUpload(byte[] buffer, string mediaType, string fileName, string value)
        {
            Buffer = buffer;
            MediaType = mediaType;
            FileName = fileName.Replace("\"","");
            _RawValue = value;
 
            Value = JsonConvert.DeserializeObject<T>(_RawValue);
 
            foreach (PropertyInfo Property in Value.GetType().GetProperties())
            {
                var Results = new List<ValidationResult>();
                Validator.TryValidateProperty(Property.GetValue(Value),
                                              new ValidationContext(Value) {MemberName = Property.Name}, Results);
                ValidationResults.AddRange(Results);
            }
        }
 
        public void Save(string path, int userId)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            var SafeFileName = Md5Hash.GetSaltedFileName(userId,FileName);
            var NewPath = Path.Combine(path, SafeFileName);
            if (File.Exists(NewPath))
            {
                File.Delete(NewPath);
            }
 
            File.WriteAllBytes(NewPath, Buffer);
 
            var Property = Value.GetType().GetProperty("FileName");
            Property.SetValue(Value, SafeFileName, null);
        }
    }
Be Sociable, Share!
Tagged: ,

Discussion

  1. jflood.net says:

    Nice work lone techie, good to see my post was useful to someone :)

  2. Chris says:

    What is the type that appears a couple times in your code?

    VS isn’t able to generate a reference to it automatically

  3. Chris says:

    Should be “font” type above…

  4. LoneTechie says:

    font is the object I’m deserializing. You can make any object work with this code, it probably should have a property called FileName to work like my code does above.

  5. DT says:

    Could we please see your font class and the html for your form?

  6. LoneTechie says:


    public class font{
    public string FileName {get;set;}
    }


    <form action="Your File Upload Path" method="POST" enctype="multipart/form-data">
    <input type="file" name="fileupload"/>
    <input type="submit" name="submit"/>
    </form>

    That is all you really “need” for this to work correctly.

  7. Jon says:

    I am trying to use this code and hopefully adapt it to my needs but I am having trouble getting it to work. Basically when I step through the code in my project I get to the following line:

    var Parts = await content.ReadAsMultipartAsync();

    and it never returns. It never goes to the next line, never returns a response to the browser or anything. Have you got any idea what could be causing it?

  8. Heliar says:

    Excellent post. This saved me a ton of time and from doing that ugly header and content parsing in the body of my controller.

  9. Cagdas says:

    You, sir, are my hero. I just needed to change

    var FileContent = Parts.Contents.First(x =>
    SupportedMediaTypes.Contains(x.Headers.ContentType));

    to this:

    var FileContent = Parts.Contents.FirstOrDefault(x => x.Headers.ContentDisposition != null && !string.IsNullOrEmpty(x.Headers.ContentDisposition.FileName));

    It worked like a charm. Thank you.

  10. Andre' says:

    Excellent post indeed. Between the original, JFlood and your great work I learned a heap and got a nice little webapi service working. Your additional effort made a good thing better. Thanks for sharing!

  11. coder says:

    Great post.. helped me a lot…

  12. Saykor says:

    A little change in for the last 2 lines in FileUpload.cs

    if (!string.IsNullOrEmpty(FileName))
    {
    var Property = Value.GetType().GetProperty(“FileName”);
    Property.SetValue(Value, FileName, null);
    }

  13. Carl Ashby says:

    I was having issues receiving a serialized object and a file as part of a single multipart form data post. This code saved my day. Keep up the great work. Thank you!

  14. Pras says:

    How do I get serialized object and a file as part of a single multipart form data post?
    Can you please explain?

  15. Edwin van der Vis says:

    Im getting this:
    No MediaTypeFormatter is available to read an object of type ‘FileUpload`1′ from content with media type ‘multipart/form-data’.

    Did the same as you did…

    • Ben Morris says:

      Sounds like you didn’t add the line

      GlobalConfiguration.Configuration.Formatters.Add(new FileMediaFormatter());

      in your application_start

      • Edwin van der Vis says:

        That wasnt the problem in this case..

        I had a wrong value in this sentence (CanReadType):
        return type == typeof(FileUpload);

        I removed the whole FileUpload class part and changed the implementation to what I needed.

        Anyways thanks for this code, helped me out a lot.

  16. Hassakarn C. says:

    Hi,
    Thanks for your code. It really helps me. But some parts don’t fit my needs so I’ve made some alterations which might help it be more useful

    public class BaseFileUploadModel
    {
    public string FileName { get; set; }
    public string MediaType { get; set; }
    public byte[] Buffer { get; set; }

    public BaseFileUploadModel(byte[] buffer, string mediaType, string fileName)
    {
    Buffer = buffer;
    MediaType = mediaType;
    FileName = fileName.Replace(“\””, “”);
    }
    }

    public class FileUploadModel : BaseFileUploadModel
    {
    public T Payload { get; set; }

    public FileUploadModel(byte[] buffer, string mediaType, string fileName, object data)
    : base(buffer, mediaType, fileName)
    {
    Payload = data == null ? default(T) : (T)data;
    }
    }

    public class FileMediaFormatter : MediaTypeFormatter
    {
    public FileMediaFormatter()
    {
    SupportedMediaTypes.Add(new MediaTypeHeaderValue(“multipart/form-data”));
    SupportedMediaTypes.Add(new MediaTypeHeaderValue(“application/octet-stream”));
    SupportedMediaTypes.Add(new MediaTypeHeaderValue(“image/jpeg”));
    SupportedMediaTypes.Add(new MediaTypeHeaderValue(“image/jpg”));
    SupportedMediaTypes.Add(new MediaTypeHeaderValue(“image/png”));
    }

    public override bool CanReadType(Type type)
    {
    var baseType = typeof(Models.BaseFileUploadModel);
    return type == baseType || type.BaseType == baseType;
    }

    public override bool CanWriteType(Type type)
    {
    return false;
    }

    public async override Task ReadFromStreamAsync(Type type, System.IO.Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
    if (!content.IsMimeMultipartContent())
    {
    throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    var parts = await content.ReadAsMultipartAsync();
    var fileContent = parts.Contents.First(x =>
    SupportedMediaTypes.Contains(x.Headers.ContentType));

    var fileName = fileContent.Headers.ContentDisposition.FileName;
    var mediaType = fileContent.Headers.ContentType.MediaType;
    byte[] imageBuffer;

    using (var imageStream = await fileContent.ReadAsStreamAsync())
    {
    imageBuffer = ReadFully(imageStream);
    }

    if (type.GenericTypeArguments.Count() > 0)
    {
    if (parts.Contents.Any(x => x.Headers.ContentType != null && x.Headers.ContentType.MediaType == “application/json”))
    {
    var dataContent = parts.Contents.First(x => x.Headers.ContentType.MediaType == “application/json”);
    var val = await dataContent.ReadAsStringAsync();

    var fileUpload = Activator.CreateInstance(type, new object[] { imageBuffer, mediaType, fileName, JsonConvert.DeserializeObject(val, type.GenericTypeArguments[0]) });
    return fileUpload;
    }
    else
    {
    var dataContents = parts.Contents.Where(x => !SupportedMediaTypes.Contains(x.Headers.ContentType));
    var jsonData = new JObject();

    foreach (var dataContent in dataContents)
    {
    var val = await dataContent.ReadAsStringAsync();
    var key = dataContent.Headers.ContentDisposition.Name.Replace(“\””, string.Empty);
    jsonData[key] = val;
    }

    var fileUpload = Activator.CreateInstance(type, new object[] { imageBuffer, mediaType, fileName, jsonData.ToObject(type.GenericTypeArguments[0]) });
    return fileUpload;
    }

    }
    else
    {
    var fileUpload = new Models.BaseFileUploadModel(imageBuffer, mediaType, fileName);
    return fileUpload;
    }
    }

    private byte[] ReadFully(System.IO.Stream input)
    {
    var buffer = new byte[16 * 1024];
    using (var memStream = new System.IO.MemoryStream())
    {
    int read;
    while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
    {
    memStream.Write(buffer, 0, read);
    }
    return memStream.ToArray();
    }
    }
    }

    GlobalConfiguration.Configuration.Formatters.Add(new FileMediaFormatter());

    the code surely doesn’t perfect but it will handle other content types than only application/json

    Hope this help

Add a Comment

*