MultipartFormDataStreamProvider for ASP.NET Core 2

Subtractive picture Subtractive · Oct 21, 2017 · Viewed 8.7k times · Source

I am in the process of migrating a project from ASP.NET MVC 5 to ASP.NET Core 2 and have run into some issues regarding the MultipartFormDataStreamProvider

As far as I can tell it's not yet a part of .NET Core and therefore cannot be used. The issue I'm trying to solve is a part of the code where Sendgrid is beeing used, parsing of e-mails.

The .NET MVC 5 code looks as follows

[HttpPost]
public async Task<HttpResponseMessage> Post()
{
   var root = HttpContext.Current.Server.MapPath("~/App_Data");
   var provider = new MultipartFormDataStreamProvider(root);
   await Request.Content.ReadAsMultipartAsync(provider);

   var email = new Email
   {
      Dkim = provider.FormData.GetValues("dkim").FirstOrDefault(),
      To = provider.FormData.GetValues("to").FirstOrDefault(),
      Html = provider.FormData.GetValues("html").FirstOrDefault()
   }
}

This code is a snippet taken from the Sendgrid API Documentation: https://sendgrid.com/docs/Integrate/Code_Examples/Webhook_Examples/csharp.html

So I have been fiddling with this for a while, trying to come up with a solution but I'm utterly stuck. The closest to a solution I've come is to use Request.Form e.g

To = form["to"].SingleOrDefault(),
From = form["from"].SingleOrDefault()

However this only works when sending in data through the ARC REST Client plugin for Chrome (or any other REST-API tester). Also this solution won't be able to handle attachments such as images and the like.

So I'm turning to the community of StackOverflow hoping that someone has some pointers or a solution for how to migrate this to .NET Core 2.

Thanks in advance!

Answer

Grant Castner picture Grant Castner · Oct 31, 2017

Here is my solution so far. It is still a work in progress, for example, in terms of handling attachments but it is successfully parsing the email.

It borrows heavily from Wade's blog on uploading files in ASP.NET Core at https://dotnetcoretutorials.com/2017/03/12/uploading-files-asp-net-core/

[HttpPost]
    [DisableFormValueModelBinding]
    [Route("v4/ProcessEmail")]
    public async Task<IActionResult> ParseSendGridInboundWebHook()
    {
        FormValueProvider formModel;
        using (var stream = System.IO.File.Create("c:\\temp\\myfile.temp"))
        {
            formModel = await _context.HttpContext.Request.StreamFile(stream);
        }

        var viewModel = new SendGridEmailDTO();

        var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
            valueProvider: formModel);

        if (!bindingSuccessful)
        {
            if (!ModelState.IsValid)
            {
                return new BadRequestResult();
            }
        }


        <your code here>

        return new OkResult();

    }


public static class MultipartRequestHelper
{
    // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
    // The spec says 70 characters is a reasonable limit.
    public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
    {
        var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
        if (string.IsNullOrWhiteSpace(boundary.Value))
        {
            throw new InvalidDataException("Missing content-type boundary.");
        }

        if (boundary.Length > lengthLimit)
        {
            throw new InvalidDataException(
                $"Multipart boundary length limit {lengthLimit} exceeded.");
        }

        return boundary.Value;
    }

    public static bool IsMultipartContentType(string contentType)
    {
        return !string.IsNullOrEmpty(contentType)
               && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
    }

    public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
    {
        // Content-Disposition: form-data; name="key";
        return contentDisposition != null
               && contentDisposition.DispositionType.Equals("form-data")
               && string.IsNullOrEmpty(contentDisposition.FileName.Value)
               && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
    }

    public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
    {
        // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
        return contentDisposition != null
               && contentDisposition.DispositionType.Equals("form-data")
               && (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
                   || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
    }
}


public static class FileStreamingHelper
{
    private static readonly FormOptions _defaultFormOptions = new FormOptions();

    public static async Task<FormValueProvider> StreamFile(this HttpRequest request, Stream targetStream)
    {
        if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
        {
            throw new Exception($"Expected a multipart request, but got {request.ContentType}");
        }

        // Used to accumulate all the form url encoded key value pairs in the 
        // request.
        var formAccumulator = new KeyValueAccumulator();
        string targetFilePath = null;

        var boundary = MultipartRequestHelper.GetBoundary(
            MediaTypeHeaderValue.Parse(request.ContentType),
            _defaultFormOptions.MultipartBoundaryLengthLimit);
        var reader = new MultipartReader(boundary, request.Body);

        var section = await reader.ReadNextSectionAsync();
        while (section != null)
        {
            var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);

            if (hasContentDispositionHeader)
            {
                if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                {
                    await section.Body.CopyToAsync(targetStream);
                }
                else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                {
                    // Content-Disposition: form-data; name="key"
                    //
                    // value

                    // Do not limit the key name length here because the 
                    // multipart headers length limit is already in effect.
                    var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                    var encoding = GetEncoding(section);
                    using (var streamReader = new StreamReader(
                        section.Body,
                        encoding,
                        detectEncodingFromByteOrderMarks: true,
                        bufferSize: 1024,
                        leaveOpen: true))
                    {
                        // The value length limit is enforced by MultipartBodyLengthLimit
                        var value = await streamReader.ReadToEndAsync();
                        if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                        {
                            value = String.Empty;
                        }
                        formAccumulator.Append(key.Value, value);

                        if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                        {
                            throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                        }
                    }
                }
            }

            // Drains any remaining section body that has not been consumed and
            // reads the headers for the next section.
            section = await reader.ReadNextSectionAsync();
        }

        // Bind form data to a model
        var formValueProvider = new FormValueProvider(
            BindingSource.Form,
            new FormCollection(formAccumulator.GetResults()),
            CultureInfo.CurrentCulture);

        return formValueProvider;
    }

    private static Encoding GetEncoding(MultipartSection section)
    {
        MediaTypeHeaderValue mediaType;
        var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
        // UTF-7 is insecure and should not be honored. UTF-8 will succeed in 
        // most cases.
        if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
        {
            return Encoding.UTF8;
        }
        return mediaType.Encoding;
    }
}


public class SendGridEmailDTO
{
    public string Dkim { get; set; }
    public string To { get; set; }
    public string Html { get; set; }
    public string From { get; set; }
    public string Text { get; set; }
    public string SenderIp { get; set; }
    public string Envelope { get; set; }
    public int Attachments { get; set; }
    public string Subject { get; set; }
    public string Charsets { get; set; }
    public string Spf { get; set; }
}