如何在ASP.NET Core中上传文档

介绍 (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.

这个上周末,我可能花了6到7个小时来弄清楚如何将文档上载到ASP.NET Core应用程序。 尽管有很多示例,但它们并不能满足我的要求,更糟糕的是,还需要一定数量的魔咒,而实际上却没有人花时间去解释。 我的意思是没人。 我在StackOverflow上发现了一个模糊的响应,该响应导致解决了一个问题,在Mozilla网站上针对FormData解决了另一个问题。 数小时后,“为什么这在Postman中起作用,但在我简单的网站上却不起作用?” 我终于有了一个可行的解决方案。

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.

上载文档的通常方法是使用form标签和随附的submit按钮。 form标签需要一个带有上传端点URL的action属性。 那很好,花花公子,不是我想要的。

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!

为什么不? 因为我不想用做文章action属性,而是我想用XMLHttpRequest包裹在一个Promise ,所以我可以处理响应(在我的情况,上传文档的ID),也捕捉异常。 此外,标准表单提交会执行重定向,尽管可以通过返回NoContent()来停止该重定向,但这确实令人NoContent() 。 当然,您不需要“ 提交”按钮,可以有一个单独的按钮来调用form.submit() ,这也很棒。 除了我还想添加不一定是form数据包一部分的键值对,是的,我在那里发现的麻烦涉及隐藏input元素或动态创建整个form元素及其子元素。 哇哦 一定喜欢人们想出的解决方法!

解决方案 (The Solution)

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

一旦弄清了秘密的调味料,该解决方案当然非常简单。

秘密调味料成分#1:IFormFile (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:

因此,.NET Core具有此接口IFormFile ,您可以使用该接口将文档流式传输到客户端。 凉。 但是您不能随便写这样的端点:

public async Task<object> UploadDocument(IFormFile fileToUpload)

秘密调味料成分2:IFormFile参数名称 (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:

参数名称必须与HTML中的name属性值匹配! 因此,如果您HTML看起来像这样:

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

Your endpoint must use file as the parameter name:

端点必须使用file作为参数名称:

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:

DocumentUpload类中,您有以下位置:

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.

而且还支持多个文件的变体,例如List<IFormFile>

秘密酱料#3:FromForm参数属性 (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):

上面的例子不起作用! 那是因为我们需要C#属性FromForm ,所以这是您正确编写端点(使用类版本)的方式:

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

秘密调味料成分4:使用表单元素实例化FormData (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");

form来自这样的代码: 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.

    我已经硬编码了"http://localhost:60192/UploadDocument" ,您可能需要更改端口。

  2. Notice formData.append(key, value)); which is where I'm appending key-value pairs that aren't part of the form.

    注意formData.append(key, value)); 这是我要添加不属于form键/值对的地方。

  3. There's no Submit button, instead there's a separate Upload button.

    没有“ 提交”按钮,而是有一个单独的“ Upload按钮。

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.

我在VS2019中编写了代码,因此我们使用的是.NET Core 3.1,因此让我们先介绍几个调整。

CORS (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.

叹。 必须添加允许跨域发布的功能,因为ASP.NET Core服务器不为该页面提供服务,我只是将其直接加载到Chrome中。 因此,请求的“来源”不是来自“服务器”。 在Startup类中,我添加了AddCors服务。

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?

必须在app调用之前完成。 说真的 我阅读了有关中间件管道的说明,但是我必须说,WTF? 为什么会有初始化顺序问题?

控制器代码 (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.

    请注意,控制器路由为""因为我不在乎URL中的路径片段。

  2. I'm assuming you have c:\temp folder. This is a demo, after all!

    我假设您有c:\ temp文件夹。 毕竟,这是一个演示!

运行代码 (Running the Code)

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

运行ASP.NET Core应用程序。 它将启动一个浏览器实例:

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:

接下来,打开项目文件夹中的“ index.html ”文件,您应该看到:

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:

选择一个文件,输入描述,然后单击“ Upload ”按钮,您应该会看到类似这样的警报-当然,响应会有所不同,因为您键入的内容与我不同:

Image 3

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

您应该在temp文件夹中注意到您上传的文件:

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!

所以你去了。 现在,您知道了秘诀,神奇的咒语,挥舞手法的秘诀,并且可以通过下载获得证明。

翻译自: https://www.codeproject.com/Articles/5257067/How-to-Upload-a-Document-in-ASP-NET-Core

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值