Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to Upload a Document in ASP.NET Core

0.00/5 (No votes)
20 Jan 2020 1  
The Secret Sauce

Introduction

This last weekend, I probably spent a good 6 or 7 hours trying to figure out how to upload a document to an ASP.NET Core application. While there are quite a few examples out there, they didn't work for what I wanted to do, and even worse, there is a certain amount of magical incantation that is required which nobody actually takes the time to explain. And I mean nobody. I found one obscure response on StackOverflow that led to resolving one issue, another on the Mozilla site for FormData. After hours of "why does this work in Postman but not on my simple website?" I finally have a working solution.

So the point of this short article is to describe the magical incantations so you don't have to go through the pain that I did. Maybe it's obvious to you, but an actual working solution simply doesn't exist, until now.

So what was my problem? As in, what the heck is your problem, Marc?

The Problem

The usual way to upload a document is with the form tag and an accompanying submit button. The form tag requires an action attribute with the URL to the upload endpoint. That's fine and dandy, and not what I wanted.

Why not? Because I didn't want to fuss with the action attribute and instead I wanted to use the XMLHttpRequest wrapped in a Promise so I could handle the response (in my case, the ID of the uploaded document) and also catch exceptions. Furthermore, the standard form submit does a redirect, which while this can be stopped by returning NoContent(), that's a freaking kludge. Of course, you don't need a Submit button, you can have a separate button that calls form.submit() and that's all great too. Except I also wanted to add key-value pairs that weren't necessarily part of the form packet, and yeah, the kludges I found there involved having hidden input elements or creating the entire form element and its children on the fly. Oh wow. Gotta love the workarounds people come up with!

The Solution

The solution is of course ridiculously simple once one figures out the secret sauce.

Secret Sauce Ingredient #1: IFormFile

So .NET Core has this interface IFormFile that you can use to stream the document onto the client. Cool. But you can't just arbitrarily write the endpoint like this:

public async Task<object> UploadDocument(IFormFile fileToUpload)

Secret Sauce Ingredient #2: The IFormFile Parameter Name

The parameter name MUST match the name attribute value in your HTML! So if your HTML looks like this:

<input type="file" name="file" />

Your endpoint must use file as the parameter name:

public async Task<object> UploadDocument(IFormFile file)

"file" matches "file".

You can also do something like this:

public async Task<object> UploadDocument(DocumentUpload docInfo)

Where, in the class DocumentUpload you have this:

public class DocumentUpload
{
    public IFormFile File { get; set; }
}

Here, "File" matches "file". Great!

And there are variations for multiple files, like List<IFormFile> that are supported too.

Secret Sauce Ingredient #3: The FromForm Parameter Attribute

The above examples won't work! That's because we need the C# attribute FromForm, so this is how you correctly write the endpoint (using the class version):

public async Task<object> UploadDocument([FromForm] DocumentUpload docInfo)

Secret Sauce Ingredient #4: Instantiate FormData with the form Element

So not obvious that on the client side, we need to do this:

let formData = new FormData(form);

where form comes from code like this: document.getElementById("uploadForm");

Annoyingly, I came across many examples where people said this would work:

let formData = new FormData();
formData.append("file", valueFromInputElement);

This doesn't work!!!

Source Code

So here's the full source code.

The Client Side

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Upload Demo</title>
</head>
<body>
    <style>
        html.wait, html.wait * {
            cursor: wait !important;
        }
    </style>

    <form id="uploadForm">
        <div>
            <input type="file" name="file" />
        </div>
        <div style="margin-top: 10px">
            <input name="description" placeholder="Description" />
        </div>
    </form>
    <button onclick="doUpload();" style="margin-top:10px">Upload</button>

    <script>
        function doUpload() {
            let form = document.getElementById("uploadForm");
            Upload("http://localhost:60192/UploadDocument", form, { clientDate: Date() })
                .then(xhr => alert(xhr.response))
                .catch(xhr => alert(xhr.statusText));
        }

        async function Upload(url, form, extraData) {
            waitCursor();

            let xhr = new XMLHttpRequest();

            return new Promise((resolve, reject) => {
                xhr.onreadystatechange = () => {
                    if (xhr.readyState == 4) {
                        if (xhr.status >= 200 && xhr.status < 300) {
                            readyCursor();
                            resolve(xhr);
                        } else {
                            readyCursor();
                            reject(xhr);
                        }
                    }
                };

                xhr.open("POST", url, true);
                let formData = new FormData(form);
                Object.entries(extraData).forEach(([key, value]) => formData.append(key, value));
                xhr.send(formData);
            });
        }

        function waitCursor() {
            document.getElementsByTagName("html")[0].classList.add("wait");
        }

        function readyCursor() {
            document.getElementsByTagName("html")[0].classList.remove("wait");
        }
    </script>
</body>
</html>

Things to note:

  1. I've hardcoded "http://localhost:60192/UploadDocument", you might need to change the port.
  2. Notice formData.append(key, value)); which is where I'm appending key-value pairs that aren't part of the form.
  3. There's no Submit button, instead there's a separate Upload button.

Like I said, simple!

The Server Side

I wrote the code in VS2019, so we're using .NET Core 3.1, so let's cover a couple tweaks first.

CORS

Sigh. Adding the ability to allow cross-domain posts is necessary because the ASP.NET Core server isn't serving the page, I just load that directly into Chrome. So the "origin" of the request is not coming from the "server." To the Startup class, I added the AddCors service.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddCors(options => {
        options.AddPolicy("CorsPolicy",
            builder => builder.AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader());
    });
}

and applied it in:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCors("CorsPolicy");

which must be done before the app calls. Seriously. I read the explanation relating to the middleware pipeline, but I've got to say, WTF? Why is there an initialization order issue?

The Controller Code

using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace UploadDemo.Controllers
{
    public class DocumentUpload
    {
        public string Description { get; set; }
        public IFormFile File { get; set; }
        public string ClientDate { get; set; }
    }

    [ApiController]
    [Route("")]                                                     
    public class UploadController : ControllerBase
    {
        public UploadController()
        {
        }

        [HttpGet]
        public ActionResult<string> Hello()
        {
            return "Hello World!";
        }

        [HttpPost]
        [Route("UploadDocument")]
        public async Task<object> UploadDocument([FromForm] DocumentUpload docInfo)
        {
            IFormFile iff = docInfo.File;
            string fn = iff.FileName;
            var tempFilename = $@"c:\temp\{fn}";

            using (var fileStream = new FileStream(tempFilename, FileMode.Create))
            {
                await iff.CopyToAsync(fileStream);
            }

            return Ok($"File {fn} uploaded.  
                   Description = {docInfo.Description} on {docInfo.ClientDate}");
        }
    }
}

This of note:

  1. Notice the controller route is "" as I don't care about a path fragment in the URL.
  2. I'm assuming you have c:\temp folder. This is a demo, after all!

Running the Code

Run the ASP.NET Core application. It'll start up a browser instance:

Image 1

Thrilling. Ignore it. Don't close it, just ignore it.

Next, open the "index.html" file that's in the project folder and you should see:

Image 2

Choose a file, type in a description, and hit the "Upload" button, and you should see an alert that looks like this -- of course, the response will be different because you typed in something different than me:

Image 3

And you should notice in your temp folder the file you uploaded:

Image 4

Of course, not THAT file. But I pretty much looked like that after figuring out all the secret sauce!

Conclusion

So there you go. You now know the secret sauce, the magical incantations, the hand-waving that made this work, and you have a download that demonstrates it working!

History

  • 20th January, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here