Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to receive `multipart/mixed` in an ASP.NET Core controller

Legacy systems will send me this:

POST /xml HTTP/1.1
Host: localhost:9000
User-Agent: curl/7.64.1
Accept: */*
Content-Length: 321
Content-Type: multipart/mixed; boundary=------------------------a9dd0ab37a224967

--------------------------a9dd0ab37a224967
Content-Disposition: attachment; name="part1"
Content-Type: text/xml

<foo>bar</foo>
--------------------------a9dd0ab37a224967
Content-Disposition: attachment; name="part2"
Content-Type: application/json

{'foo': 'bar'}
--------------------------a9dd0ab37a224967--

The first part I need to interpret as raw XElement; for the second part I would like the usual model binding.

I try this:

class Part2 { 
    public string foo { get; set; }
}
    

[HttpPost]
[Route("/xml")]
public string Post1([FromBody] XElement part1, [FromBody] Part2 part2 )
{
    return part1.ToString() + ", " + part2.foo;
}

But ASP.NET does not allow more than one parameter decorated with [FromBody].

How do I make my ASP.NET Core service receive http requests with content-type multipart/mixed?

like image 267
Søren Debois Avatar asked Mar 02 '23 17:03

Søren Debois


2 Answers

There is no built-in mechanism to handle this type of post data (multipart/mixed has virtually unlimited possibilities, and it would be hard to bind to it in a generic sense), but, you can easily parse the data yourself using the MultipartReader object.

I am going to assume that all the data that is coming in has a disposition of attachment and that only JSON and XML content-types are valid. But this should be open-ended enough for you to modify as you see fit.

Take a look at this static helper:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using System.Text;

namespace YourNamespace.Utilities
{
    public static class MutipartMixedHelper
    {
        public static async IAsyncEnumerable<ParsedSection> 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);

            // 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
                var parsedSection = new ParsedSection
                {
                    IsJson = contentType.MediaType.Equals("application/json",
                        StringComparison.OrdinalIgnoreCase),
                    IsXml = contentType.MediaType.Equals("text/xml",
                        StringComparison.OrdinalIgnoreCase),
                    Encoding = Encoding.GetEncoding(contentType.CharSet)
                };

                // Must be XML or JSON
                if (!parsedSection.IsXml && !parsedSection.IsJson)
                {
                    throw new InvalidDataException("only handling json/xml");
                }

                // Parse the content disosition
                if (ContentDispositionHeaderValue.TryParse(
                        section.ContentDisposition, out var contentDisposition) &&
                        contentDisposition.DispositionType.Equals("attachment"))
                {
                    // save the name
                    parsedSection.Name = contentDisposition.Name.Value;

                    // Create a new StreamReader using the proper encoding and
                    // leave the underlying stream open
                    using (var streamReader = new StreamReader(
                        section.Body, parsedSection.Encoding, leaveOpen: true))
                    {
                        parsedSection.Data = await streamReader.ReadToEndAsync();
                        yield return parsedSection;
                    }
                }
            }
        }
    }

    public sealed class ParsedSection
    {
        public bool IsJson { get; set; }
        public bool IsXml { get; set; }
        public string Name { get; set; }
        public string Data { get; set; }
        public Encoding Encoding { get; set; }
    }
}

You can call this method from your endpoint, like so:

[HttpPost, Route("TestMultipartMixedPost")]
public async Task<IActionResult> TestMe()
{
    await foreach (var parsedSection in MutipartMixedHelper
        .ParseMultipartMixedRequestAsync(Request))
    {
        Debug.WriteLine($"Name: {parsedSection.Name}");
        Debug.WriteLine($"Encoding: {parsedSection.Encoding.EncodingName}");
        Debug.WriteLine($"IsJson: {parsedSection.IsJson}");
        Debug.WriteLine($"IsXml: {parsedSection.IsXml}");
        Debug.WriteLine($"Data: {parsedSection.Data}");
        Debug.WriteLine("-----------------------");
    }

    return Ok();
}

Your endpoint would output:

Name: part1
Encoding: Unicode (UTF-8)
IsJson: False
IsXml: True
Data: <foo>bar</foo>
-----------------------
Name: part2
Encoding: Unicode (UTF-8)
IsJson: True
IsXml: False
Data: {"foo": "bar"}
-----------------------

At this point, you'd have to deserialize based on the properties of the returned objects.

like image 147
Andy Avatar answered Mar 25 '23 02:03

Andy


Note that [FromBody] doesn't work here as it specifically designed to accept simple form data (e.g "multipart/form-data" type) suitable for processing via the POST request method.

You may look for something like Batch Processing in OData. Considering that, Sending data through RFC 2045/2046 is not a typical way for web application communications. Actually, This is designed for Email communications. This is why you see poor information for Asp.net implementation.

Following your previous question, Another answer could be sending a raw text (xml formatted) to the controller and then trying to parse and do another operations. Try to keep the client-server communication simple. Here, there's no reason to make it complex using MultipartMixed method while modern and large scale projects use typical ways.

like image 25
Amirhossein Mehrvarzi Avatar answered Mar 25 '23 01:03

Amirhossein Mehrvarzi