Skip to content

Handling Large Uploads in Fsharp and ASP.Net Core

I found the documentation regarding handling large uploads in a ASP.Net Core app confusing. I didn’t find a lot of code examples, and even none at all using F#.

Eventually I found an example I could get inspiration from and build a working solution with. It should work with whatever ASP.Net Core based framework you use, as long as you can get a handle on the Microsoft.AspNetCore.Http.HttpContext instance. You will need to clarify some things before you put the code in production. For example I’m not sure the code handles contentDisposition.FileNameStar correctly. This means that even though it’s useful to share this code, it is done without any warranty and you should do your homework to validate the code works as you expect and fits your requirements.

The function that will extract the file from the payload and save the file on disk is uploadFile and it uses the small helper function cleanupHeaderValue to remove quotes from header values:

let cleanupHeaderValue (s:string) =
    Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(s).Value

// returns true when it worked through all sections
let rec uploadFile(reader:MultipartReader)(section:MultipartSection) = async {
    let hasContentDispositionHeader,contentDisposition = System.Net.Http.Headers.ContentDispositionHeaderValue.TryParse(section.ContentDisposition)
    if (hasContentDispositionHeader) then
        let hasFileName = not <| System.String.IsNullOrEmpty(contentDisposition.FileName)
        // not sure FileNameStar is handled correctly below, do you homework and check!
        let hasFileNameStar = not <| System.String.IsNullOrEmpty(contentDisposition.FileNameStar)
        if (contentDisposition.DispositionType.Equals("form-data") &&
           (hasFileName || hasFileNameStar))
        then
            let filePath = System.IO.Path.GetFullPath(Path.Combine(System.Environment.CurrentDirectory, "UploadedFiles"))
            // BEWARE: you should sanitize the filename extract from the headers, eg to ensure it doesn't include a partial path
            use fileStream = System.IO.File.Create(Path.Combine(filePath, cleanupHeaderValue contentDisposition.FileName))
            do! section.Body.CopyToAsync(fileStream)|> Async.AwaitTask;
            let! nextSection = reader.ReadNextSectionAsync() |> Async.AwaitTask
            if isNull nextSection then
                return true
            else
                return! uploadFile reader nextSection
        else return false
    else
        return false
}

To use it, you just have to get the MultipartReader and MultipartSection from the HttpContext. Here’s how I call it from another async function:

async {
    // first you need to get a handle on the Microsoft.AspNetCore.Http.HttpContext instance (not shown)
    ....
    // then
    let boundary =
        Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(
            Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(httpContext.Request.ContentType).Boundary
        ).Value
    let reader = new MultipartReader(boundary, httpContext.Request.Body);
    let! section = reader.ReadNextSectionAsync() |> Async.AwaitTask
    if section <> null then
        return! uploadFile reader nextSection
    else
        return true
}

The code above also handles multiple files sent in the same request (which in my case was a XMLHttpRequest).

Special thanks to Abel Braaksma on the F# slack for his feedback!