Handle Multipart Contents in Asp.Net Core

Handle Multipart Contents in Asp.Net Core

Introduction

When we program a web API in ASP.Net core we usually we have the Content-Type header as ‘application/json’ if it is a REST API. Assume a scenario where we need to pass an image along with description about the image or metadata. We can either pass that information in query parameters or in headers, but query parameters or header has limited in size. If the metadata is a json object, then query parameters or header is not a best place to pass. How about sending both the json and binary image within the body itself. There is a Content-Type ‘multipart/mixed’ in HTTP protocol is where we can achieve this. This article shows you how to pass both the json data and the binary in the same http request.

Content-Type=multipart/mixed Header

In the below example I will show how to use http header content-type as ‘multipart/mixed.’ The purpose of the Content-Type field is to describe the data contained in the body fully enough that the receiving user agent can pick an appropriate agent or mechanism to present the data to the user, or otherwise deal with the data in an appropriate manner.

In the ‘multipart/mixed’ content type, the multipart where data consisting of multiple parts of independent data types and "mixed" subtype where the data in multiple formats. Know more about the Content-Type, check out this link from w3.org.

Below is the http request of ‘/upload’ endpoint.

 

POST http://localhost:5000/upload HTTP/1.1

Host: localhost:5000

Content-Type: multipart/mixed; boundary="0b3c97fb-778b-48af-b6ca-00d644439599"

Content-Length: 504726

 

--0b3c97fb-778b-48af-b6ca-00d644439599

Content-Type: application/json; charset=utf-8

Content-Disposition: attachment; name=meta

 

{ "title": "Rocket Launch", "filename": "gslv.jpg", "body": "This is the GSLV rocket launch", "tags": [ "gslv", "isro" ]}

 

--0b3c97fb-778b-48af-b6ca-00d644439599

Content-Type: application/octet-stream

Content-Disposition: attachment; name=data

 

{content of the actual gslv.jpg image}

 

--0b3c97fb-778b-48af-b6ca-00d644439599

 

 

As you had noted the first Content-Type is mentioned as ‘multipart/mixed’ and defines a boundary. The ‘boundary’ is the key to identify different sections within the request. The second content type is mentioned as ‘applciaiton/json’ whereas the third content type is mentioned as ‘application/octect-stream.

There is also a Content-Disposition’ and named as ‘meta’ and ‘data’ these are the keywords used to identify the section, although this is not mandatory.

File Uploader API

The file uploader API exposes to upload a file along with the file content there are few metadata associated with the file. Our input request model for file uploader is shown below,

namespace FileUploader.Model

{

    public class UploadRequest

    {

        public Meta Meta { get; set; }

        public byte[] Content { get; set; }

        public string Name { get; set; }

        public Encoding Encoding { get; set; }

    }

 

    public class Meta

    {

        public string Title { get; set; }

        public string FileName { get; set; }

        public string Body { get; set; }

        public string[] Tags { get; set; }

    }

}

 

Here are the properties description of UploadRequest,

-          Name, name of the section

-          Encoding, encoding method used

-          Meta, a json object that describes about an image

-          Content, actual content of an image

Although now the request is sent to our upload endpoint as multipart, but now how to parse it. That is what we were doing at the ‘MultipartMixedHelper’ static class. The ‘MultipartReader’ from the asp.net core library will help us to read the different sections. The below code shows the same,

namespace FileUploader

{

    public static class MutipartMixedHelper

    {

        public static async Task<UploadRequest> ParseMultipartMixedRequestAsync(HttpRequest request)

        {

            // Extract, sanitize and validate boundry

            var boundary = HeaderUtilities.RemoveQuotes(

                MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value;

 

            if (string.IsNullOrWhiteSpace(boundary) ||

                (boundary.Length > new FormOptions().MultipartBoundaryLengthLimit))

            {

                throw new InvalidDataException("boundry is shot");

            }

 

            // Create a new reader based on that boundry

            var reader = new MultipartReader(boundary, request.Body);

 

            var uploadRequest = new UploadRequest();

            // Start reading sections from the MultipartReader until there are no more

            MultipartSection section;

            while ((section = await reader.ReadNextSectionAsync()) != null)

            {

                // parse the content type

                var contentType = new ContentType(section.ContentType);

 

                // create a new ParsedSecion and start filling in the details

                if(contentType.MediaType.Equals("application/json",

                        StringComparison.OrdinalIgnoreCase) &&

                        ContentDispositionHeaderValue.TryParse(

                        section.ContentDisposition, out var cdMeta) &&

                        cdMeta.Name.Value.Equals("meta"))

                {

                    uploadRequest.Encoding = Encoding.GetEncoding(contentType.CharSet);

 

                    // save the name

                    uploadRequest.Name = cdMeta.Name.Value;

 

                    // Create a new StreamReader using the proper encoding and

                    // leave the underlying stream open

                    using (var streamReader = new StreamReader(

                        section.Body, uploadRequest.Encoding, leaveOpen: true))

                    {

                        var data = await streamReader.ReadToEndAsync();

                        uploadRequest.Meta = (Meta)JsonConvert.DeserializeObject(data, typeof(Meta));

 

                    }

                }

 

                if(contentType.MediaType.Equals("application/octet-stream",

                        StringComparison.OrdinalIgnoreCase) &&

                        ContentDispositionHeaderValue.TryParse(

                        section.ContentDisposition, out var cdData) &&

                        cdData.Name.Value.Equals("data"))

                {

                    byte[] buffer = new byte[16 * 1024];

                    using (MemoryStream ms = new MemoryStream())

                    {

                        int read;

                        while ((read = await section.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)

                        {

                            ms.Write(buffer, 0, read);

                        }

                        uploadRequest.Content = ms.ToArray();

                    }

                }

            }

            return uploadRequest;

        }

    }

}

 

The code parses each section with the key named as ‘meta’ and ‘data’. The ‘meta’ where our json string is and the stream is converted into object. The ‘data’ section is where the stream is converted into byte.

In our controller the MultipartMixedHelper is used as below,

 [HttpPost]

        public async Task<ActionResult<UploadResponse>> TestMe()

        {

            var request = await MutipartMixedHelper.ParseMultipartMixedRequestAsync(Request);

 

            var trustedFileName = Guid.NewGuid().ToString();

            var filePath = Path.Combine(@".\images", trustedFileName + Path.GetExtension(request.Meta.FileName));

 

            if (System.IO.File.Exists(filePath))

            {

                return Conflict(new UploadResponse() { Message = "File already exists" });

            }

 

           System.IO.File.WriteAllBytes(filePath,request.Content);

 

            return Ok(new UploadResponse() { Id = trustedFileName, Message = "File uploded successfully" });

        }

 

Summary

This is a simple example to show the power of ‘multipart’ in http protocol. The ‘multipart/mixed’ enables us to form many different possibilities. On the downside, there is not many libraries or model binder available to bind the data directly from the request and injected into our controller, instead we must handle our own code to do that. The complete sample project for this article can be downloaded from the github(https://github.com/arunvambur/MultipartContent)


Comments

Popular posts from this blog

Debugging and Testing Helm Charts Using VS Code

Validate appsettings in ASP.net Core using FluentValidation