Click here to Skip to main content
14,459,279 members

How to Upload a Document in ASP.NET Core

Rate this:
5.00 (16 votes)
Please Sign up or sign in to vote.
5.00 (16 votes)
20 Jan 2020CPOL
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionGreat Article. Just want to add.. Pin
Member 1091697416-Feb-20 5:13
professionalMember 1091697416-Feb-20 5:13 
GeneralMy vote of 5 Pin
Carsten V2.024-Jan-20 1:28
MemberCarsten V2.024-Jan-20 1:28 
QuestionImages....be patient... Pin
Marc Clifton20-Jan-20 15:32
communityengineerMarc Clifton20-Jan-20 15:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 20 Jan 2020

Stats

5K views
128 downloads
29 bookmarked