Skip to content

Instantly share code, notes, and snippets.

@Danielku15
Created February 25, 2016 19:53
Show Gist options
  • Save Danielku15/bfc568a19b9e58fd9e80 to your computer and use it in GitHub Desktop.
Save Danielku15/bfc568a19b9e58fd9e80 to your computer and use it in GitHub Desktop.
A MediaTypeFormatter for WebApi for multipart/form-data including file uploads
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>File Upload example</title>
<link href="/Content/bootstrap.css" rel="stylesheet" />
</head>
<body>
<form action="api/Upload" method="post">
<div class="form-group">
<label for="SiteId">Site Id</label>
<input type="number" class="form-control" id="SiteId" name="SiteId" placeholder="Site Id" />
</div>
<div class="form-group">
<label for="StartDate">Start Date</label>
<input class="form-control" id="StartDate" name="StartDate" placeholder="Start Date" />
</div>
<div class="form-group">
<label for="Zulu">File</label>
<input class="form-control" id="Zulu" name="Zulu" placeholder="Zulu" />
</div>
<button class="btn btn-primary" type="submit">Send</button>
</form>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;
using System.Web.Http.Validation;
using System.Web.Http.Validation.Providers;
using System.Web.Http.ValueProviders.Providers;
namespace WebApiFileUpload.Utils
{
/// <summary>
/// Represents the <see cref="MediaTypeFormatter"/> class to handle multipart/form-data.
/// </summary>
public class FormMultipartEncodedMediaTypeFormatter : MediaTypeFormatter
{
private const string SupportedMediaType = "multipart/form-data";
/// <summary>
/// Initializes a new instance of the <see cref="FormMultipartEncodedMediaTypeFormatter"/> class.
/// </summary>
public FormMultipartEncodedMediaTypeFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(SupportedMediaType));
}
public override bool CanReadType(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return true;
}
public override bool CanWriteType(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
return false;
}
public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (readStream == null) throw new ArgumentNullException(nameof(readStream));
try
{
// load multipart data into memory
var multipartProvider = await content.ReadAsMultipartAsync();
// fill parts into a ditionary for later binding to model
var modelDictionary = await ToModelDictionaryAsync(multipartProvider);
// bind data to model
return BindToModel(modelDictionary, type, formatterLogger);
}
catch (Exception e)
{
if (formatterLogger == null)
{
throw;
}
formatterLogger.LogError(string.Empty, e);
return GetDefaultValueForType(type);
}
}
private async Task<IDictionary<string, object>> ToModelDictionaryAsync(MultipartMemoryStreamProvider multipartProvider)
{
var dictionary = new Dictionary<string, object>();
// iterate all parts
foreach (var part in multipartProvider.Contents)
{
// unescape the name
var name = part.Headers.ContentDisposition.Name.Trim('"');
// if we have a filename, we treat the part as file upload,
// otherwise as simple string, model binder will convert strings to other types.
if (!string.IsNullOrEmpty(part.Headers.ContentDisposition.FileName))
{
// set null if no content was submitted to have support for [Required]
if (part.Headers.ContentLength.GetValueOrDefault() > 0)
{
dictionary[name] = new HttpPostedFileMultipart(
part.Headers.ContentDisposition.FileName.Trim('"'),
part.Headers.ContentType.MediaType,
await part.ReadAsByteArrayAsync()
);
}
else
{
dictionary[name] = null;
}
}
else
{
dictionary[name] = await part.ReadAsStringAsync();
}
}
return dictionary;
}
private object BindToModel(IDictionary<string, object> data, Type type, IFormatterLogger formatterLogger)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (type == null) throw new ArgumentNullException(nameof(type));
using (var config = new HttpConfiguration())
{
// if there is a requiredMemberSelector set, use this one by replacing the validator provider
var validateRequiredMembers = RequiredMemberSelector != null && formatterLogger != null;
if (validateRequiredMembers)
{
config.Services.Replace(typeof(ModelValidatorProvider), new RequiredMemberModelValidatorProvider(RequiredMemberSelector));
}
// create a action context for model binding
var actionContext = new HttpActionContext
{
ControllerContext = new HttpControllerContext
{
Configuration = config,
ControllerDescriptor = new HttpControllerDescriptor
{
Configuration = config
}
}
};
// create model binder context
var valueProvider = new NameValuePairsValueProvider(data, CultureInfo.InvariantCulture);
var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(null, type);
var modelBindingContext = new ModelBindingContext
{
ModelName = string.Empty,
FallbackToEmptyPrefix = false,
ModelMetadata = metadata,
ModelState = actionContext.ModelState,
ValueProvider = valueProvider
};
// bind model
var modelBinderProvider = new CompositeModelBinderProvider(config.Services.GetModelBinderProviders());
var binder = modelBinderProvider.GetBinder(config, type);
var haveResult = binder.BindModel(actionContext, modelBindingContext);
// log validation errors
if (formatterLogger != null)
{
foreach (var modelStatePair in actionContext.ModelState)
{
foreach (var modelError in modelStatePair.Value.Errors)
{
if (modelError.Exception != null)
{
formatterLogger.LogError(modelStatePair.Key, modelError.Exception);
}
else
{
formatterLogger.LogError(modelStatePair.Key, modelError.ErrorMessage);
}
}
}
}
return haveResult ? modelBindingContext.Model : GetDefaultValueForType(type);
}
}
}
}
using System.IO;
using System.Web;
namespace WebApiFileUpload.Utils
{
/// <summary>
/// Represents a file that has uploaded by a client via multipart/form-data.
/// </summary>
public class HttpPostedFileMultipart : HttpPostedFileBase
{
private readonly MemoryStream _fileContents;
public override int ContentLength => (int)_fileContents.Length;
public override string ContentType { get; }
public override string FileName { get; }
public override Stream InputStream => _fileContents;
/// <summary>
/// Initializes a new instance of the <see cref="HttpPostedFileMultipart"/> class.
/// </summary>
/// <param name="fileName">The fully qualified name of the file on the client</param>
/// <param name="contentType">The MIME content type of an uploaded file</param>
/// <param name="fileContents">The contents of the uploaded file.</param>
public HttpPostedFileMultipart(string fileName, string contentType, byte[] fileContents)
{
FileName = fileName;
ContentType = contentType;
_fileContents = new MemoryStream(fileContents);
}
}
}
using System.Net;
using System.Net.Http;
using System.Web.Http;
using WebApiFileUpload.Models;
namespace WebApiFileUpload.Controllers
{
/// <summary>
/// This is an example controller showing how to accept file uploads.
/// </summary>
public class UploadController : ApiController
{
// simply accept the viewmodel as usual
public IHttpActionResult Post(UploadRequestViewModel model)
{
// provide validation result if not valid
if (!ModelState.IsValid)
{
var response = Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
return ResponseMessage(response);
}
// show some details about the upload as result
return Content(HttpStatusCode.OK, new
{
Status = "success",
Title = model.Title,
Description = model.Description,
FileName = model.File.FileName,
ContentLength = model.File.ContentLength,
ContentType = model.File.ContentType
});
}
}
}
using System.ComponentModel.DataAnnotations;
using System.Web;
namespace WebApiFileUpload.Models
{
/// <summary>
/// This is an API viewmodel showing how to accept a file upload via multipart/form-data
/// </summary>
public class UploadRequestViewModel
{
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
[Required]
public HttpPostedFileBase File { get; set; }
}
}
@amirhz
Copy link

amirhz commented Dec 21, 2016

how we can use public IEnumerable<HttpPostedFileBase>Files { get; set; } instead public HttpPostedFileBase File { get; set; }?

@Tocana
Copy link

Tocana commented Jun 8, 2017

Danielku15,
I am trying to reproduce your code but I am not able to get data on my model from my form, When I use one imput field with "type=File" on my form my data on the controller is always null.
Do you know if I would need anything else?
Thanks so much.

@savelev-sa
Copy link

@amirhz resolved in my fork

@abbhakan
Copy link

abbhakan commented Aug 6, 2018

@rizekill Could you please describe how you resolved using an IEnumerable HttpPostedFileBase instead of HttpPostedFileBase in your fork?

@iamkingalvarado
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment