-
-
Save codethug/4773b07e8be884ebf28915b5ff666cd9 to your computer and use it in GitHub Desktop.
A MediaTypeFormatter for WebApi for multipart/form-data including file uploads
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment