Nancy + .Net – parsing “multipart/form-data” requests
With a little more work, this solution could easily be refactored into a custom model binder.
With a little more work, this solution could easily be refactored into a custom model binder.
Nancy is a lightweight Web framework for .Net that provides an interesting alternative to ASP.Net MVC. One issue that I’ve come across while working with it, is that although it currently (v 0.21.1) does support model binding, it all falls apart when you want both files and a JSON body in your POST or PUT HTTP request. Luckily there’s a quick and easy way around this issue.
Let’s assume that we want to process an HTTP POST request, with a standard JSON body, and one or more files attached to it. Such a request should contain the “content-type” header set to multipart/… (in this article I’ll assume its multipart/form-data ) as the request payload does indeed contain multiple parts (the files as binary data and the JSON body as text). Currently, Nancy model binding only supports binding to requests with content-type set to application/json or application/xml out of the box, so binding any model to such a request will fail. However, it does provide the HttpMultipart class which neatly handles splitting the request body into different parts and detecting their respective content-types. Since all that logic is already in place I’ve decided to skip model binding altogether and extract the JSON body (and the model from that) manually.
Here’s the code:
public class IndexModule : NancyModule { public IndexModule() { Get["/"] = parameters => { return "Demo module"; }; Post["/"] = parameters => { try { var contentTypeRegex = new Regex("^multipart/form-data;\s*boundary=(.*)$", RegexOptions.IgnoreCase); System.IO.Stream bodyStream = null; if (contentTypeRegex.IsMatch(this.Request.Headers.ContentType)) { var boundary = contentTypeRegex.Match(this.Request.Headers.ContentType).Groups[1].Value; var multipart = new HttpMultipart(this.Request.Body, boundary); bodyStream = multipart.GetBoundaries().First(b => b.ContentType.Equals("application/json")).Value; } else { // Regular model binding goes here. bodyStream = this.Request.Body; } var jsonBody = new System.IO.StreamReader(bodyStream).ReadToEnd(); Console.WriteLine("Got request!"); Console.WriteLine("Body: {0}", jsonBody); this.Request.Files.ToList().ForEach(f => Console.WriteLine("File: {0} {1}", f.Name, f.ContentType)); return HttpStatusCode.OK; } catch (Exception ex) { Console.WriteLine("Error!!!!!! {0}", ex.Message); return HttpStatusCode.InternalServerError; } }; } }
First, a regex is used to determine whether the request content-type is indeed multipart/form-data. This is to handle the case where no files were uploaded in the request, and so the content-type is just application/json, since there are no “multiple parts”)
if (contentTypeRegex.IsMatch(this.Request.Headers.ContentType)) { ... } else { // Regular model binding goes here. ... }
Next, the boundary that separates the consecutive parts is extracted from the content-type header (also via regex) and passed into the HttpMultipart object along with the request body stream.
var boundary = contentTypeRegex.Match(this.Request.Headers.ContentType).Groups[1].Value; var multipart = new HttpMultipart(this.Request.Body, boundary);
Once we have that, we can just query for the first part that has a content type of application-json:
bodyStream = multipart.GetBoundaries().First(b => b.ContentType.Equals("application/json")).Value;
And then read the JSON into a string using StreamReader:
var jsonBody = new System.IO.StreamReader(bodyStream).ReadToEnd();
Once you have that you can use your favourite JSON parser (e.g. Json.Net) to bind your model into an object. Also note that while you can use HttpMultipart to extract the files attached to your request, Nancy actually does that for you with the Request.Files property:
this.Request.Files.ToList().ForEach(f => Console.WriteLine("File: {0} {1}", f.Name, f.ContentType));
Although not supported out of the box, parsing multipart requests with NancyFX is pretty straightforward. With a little more work this solution could easily be refactored into a custom model binder, but since I use it in only one spot I’m happy with what I have for now.
Are you looking for a tech partner? Searching for a new job? Or do you simply have any feedback that you'd like to share with our team? Whatever brings you to us, we'll do our best to help you. Don't hesitate and drop us a message!
Drop a message