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
Post a Comment