65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (20投票s)

2020 年 1 月 20 日

CPOL

4分钟阅读

viewsIcon

43306

downloadIcon

417

秘密武器

引言

上周末,我花了大约 6 到 7 个小时试图弄清楚如何将文档上传到 ASP.NET Core 应用程序。虽然有很多示例,但它们都不适用于我想要做的,更糟糕的是,需要一种神秘的咒语,但没有人真正花时间去解释。我是说没有人。我在 StackOverflow 上找到一个模糊的答案解决了其中一个问题,在 Mozilla 网站上找到了关于 FormData 的答案。经过数小时的“为什么它在 Postman 中可以工作,但在我的简单网站上却不行?”的折腾,我终于有了一个可行的解决方案。

所以,这篇文章的目的是描述这些神秘的咒语,这样您就不必经历我所经历的痛苦。也许对您来说很明显,但一个实际可行的解决方案直到现在才存在。

那么我的问题是什么?也就是说,Marc,你到底有什么问题?

问题

上传文档的通常方法是使用 form 标签和一个伴随的 submit 按钮。form 标签需要一个 action 属性,其中包含上传端点的 URL。这很方便,但不是我想要的。

为什么不呢?因为我不想纠结于 action 属性,而是想使用 XMLHttpRequest 并将其封装在 Promise 中,以便我可以处理响应(在本例中是上传文档的 ID)并捕获异常。此外,标准的表单提交会进行重定向,虽然可以通过返回 NoContent() 来阻止,但这是一种该死的权宜之计。当然,您不需要 Submit 按钮,您可以有一个单独的按钮来调用 form.submit(),这也很棒。除了我想添加的键值对不一定是 form 数据包的一部分,而且我发现的解决方法是使用隐藏的 input 元素或动态创建整个 form 元素及其子项。哦,天哪。真喜欢人们想出的变通方法!

解决方案

一旦弄清楚了秘方,解决方案自然会变得非常简单。

秘方配料 #1:IFormFile

所以 .NET Core 有这个 IFormFile 接口,您可以使用它将文档流式传输到客户端。很酷。但您不能像这样随意编写端点

public async Task<object> UploadDocument(IFormFile fileToUpload)

秘方配料 #2:IFormFile 参数名称

参数名称**必须**与 HTML 中的 name 属性值匹配!所以如果您的 HTML 看起来像这样

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

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

public async Task<object> UploadDocument(IFormFile file)

"file" 匹配 "file"。

您也可以这样做

public async Task<object> UploadDocument(DocumentUpload docInfo)

DocumentUpload 类中,您有这个

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

这里,"File" 匹配 "file"。太棒了!

并且有针对多个文件的变体,例如也支持 List<IFormFile>

秘方配料 #3:FromForm 参数属性

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

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

秘方配料 #4:使用 form 元素实例化 FormData

所以不明显的是,在客户端,我们需要这样做

let formData = new FormData(form);

其中 form 来自像这样的代码:document.getElementById("uploadForm");

令人恼火的是,我遇到了许多人说这样可以工作的示例

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

这不行!!!

源代码

所以,这是完整的源代码。

客户端

<!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("https://: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>

注意事项

  1. 我硬编码了 "https://:60192/UploadDocument",您可能需要更改端口。
  2. 请注意 formData.append(key, value));,在这里我附加了不属于 form 的键值对。
  3. 没有 Submit 按钮,取而代之的是一个单独的 Upload 按钮。

就像我说的,很简单!

服务器端

我是在 VS2019 中编写的代码,所以我们使用的是 .NET Core 3.1,让我们先涵盖几个调整。

CORS

唉。添加允许跨域 post 的能力是必要的,因为 ASP.NET Core 服务器没有提供页面,我直接将其加载到 Chrome 中。所以请求的“源”不是来自“服务器”。在 Startup 类中,我添加了 AddCors 服务。

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

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

并在其中应用了

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

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

Controller 代码

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}");
        }
    }
}

值得注意的是

  1. 请注意 controller 路由是 "",因为我不在乎 URL 中的路径片段。
  2. 我假设您有一个 *c:\temp* 文件夹。毕竟,这是一个演示!

运行代码

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

太棒了。忽略它。不要关闭它,只是忽略它。

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

选择一个文件,输入描述,然后点击“Upload”按钮,您应该会看到一个如下所示的警报 -- 当然,响应会不同,因为您输入的内容与我不同

您应该会在您的 *temp* 文件夹中看到您上传的文件

当然,不是那个文件。但我在弄清楚所有秘方后几乎看起来就像那样!

结论

就这样。您现在知道了秘方、神秘的咒语、让这一切生效的挥手动作,并且您还得到了一个演示其工作原理的下载!

历史

  • 2020 年 1 月 20 日:初始版本
© . All rights reserved.