【问题标题】:Returning a file to View/Download in ASP.NET MVC在 ASP.NET MVC 中返回文件以查看/下载
【发布时间】:2011-08-15 03:51:58
【问题描述】:

我在将存储在数据库中的文件发送回 ASP.NET MVC 中的用户时遇到问题。我想要的是一个列出两个链接的视图,一个用于查看文件并让发送到浏览器的 mimetype 确定应如何处理,另一个用于强制下载。

如果我选择查看一个名为SomeRandomFile.bak 的文件并且浏览器没有关联的程序来打开这种类型的文件,那么默认为下载行为我没有问题。但是,如果我选择查看一个名为 SomeRandomFile.pdfSomeRandomFile.jpg 的文件,我希望文件能够简单地打开。但我也想保留一个下载链接,这样无论文件类型如何,我都可以强制下载提示。这有意义吗?

我已经尝试过FileStreamResult,它适用于大多数文件,它的构造函数默认不接受文件名,因此未知文件会根据 URL 分配一个文件名(它不知道要给出的扩展名基于内容类型)。如果我通过指定文件名来强制使用它,我将失去浏览器直接打开文件的能力,并且会收到下载提示。有没有其他人遇到过这种情况?

这些是我迄今为止尝试过的示例。

//Gives me a download prompt.
return File(document.Data, document.ContentType, document.Name);

//Opens if it is a known extension type, downloads otherwise (download has bogus name and missing extension)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType);

//Gives me a download prompt (lose the ability to open by default if known type)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType) {FileDownloadName = document.Name};

有什么建议吗?


更新: 这个问题似乎引起了很多人的共鸣,所以我想我会发布一个更新。 Oskar 添加的关于国际字符的以下已接受答案的警告是完全有效的,由于使用了 ContentDisposition 类,我已经打了几次。我已经更新了我的实现来解决这个问题。虽然下面的代码来自我最近在 ASP.NET Core(完整框架)应用程序中解决此问题,但由于我使用的是 System.Net.Http.Headers.ContentDispositionHeaderValue 类,因此它也应该在旧 MVC 应用程序中进行最小的更改。

using System.Net.Http.Headers;

public IActionResult Download()
{
    Document document = ... //Obtain document from database context

    //"attachment" means always prompt the user to download
    //"inline" means let the browser try and handle it
    var cd = new ContentDispositionHeaderValue("attachment")
    {
        FileNameStar = document.FileName
    };
    Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());

    return File(document.Data, document.ContentType);
}

// an entity class for the document in my database 
public class Document
{
    public string FileName { get; set; }
    public string ContentType { get; set; }
    public byte[] Data { get; set; }
    //Other properties left out for brevity
}

【问题讨论】:

    标签: c# asp.net-mvc asp.net-mvc-3 download http-headers


    【解决方案1】:
    public ActionResult Download()
    {
        var document = ...
        var cd = new System.Net.Mime.ContentDisposition
        {
            // for example foo.bak
            FileName = document.FileName, 
    
            // always prompt the user for downloading, set to true if you want 
            // the browser to try to show the file inline
            Inline = false, 
        };
        Response.AppendHeader("Content-Disposition", cd.ToString());
        return File(document.Data, document.ContentType);
    }
    

    注意: 上面的示例代码未能正确考虑文件名中的国际字符。相关标准化见 RFC6266。我相信 ASP.Net MVC 的 File() 方法和 ContentDispositionHeaderValue 类的最新版本正确地解释了这一点。 - 奥斯卡 2016-02-25

    【讨论】:

    • 如果我没记错的话,只要文件名没有空格,就可以不加引号(我通过 HttpUtility.UrlEncode() 运行我的来实现这一点)。
    • 注意:如果您使用它并设置Inline = true,请确保不要使用File() 的三参数重载,它将文件名作为第三个参数。它在 IE 中工作,但 Chrome 会报告重复的标题并拒绝显示图像。
    • var document = ... 是什么类型?
    • @user1103990,这是你的领域模型。
    • 使用 MVC 5,不再需要 content-disposition 标头,因为它已经是响应标头的一部分。但我在 FF 中只有一个下载对话框,在 chrome 和 IE 中没有对话框
    【解决方案2】:

    由于“文档”变量上没有类型提示,我无法接受接受的答案:var document = ... 所以我发布了对我有用的替代方法,以防其他人遇到问题。

    public ActionResult DownloadFile()
    {
        string filename = "File.pdf";
        string filepath = AppDomain.CurrentDomain.BaseDirectory + "/Path/To/File/" + filename;
        byte[] filedata = System.IO.File.ReadAllBytes(filepath);
        string contentType = MimeMapping.GetMimeMapping(filepath);
    
        var cd = new System.Net.Mime.ContentDisposition
        {
            FileName = filename,
            Inline = true,
        };
    
        Response.AppendHeader("Content-Disposition", cd.ToString());
    
        return File(filedata, contentType);
    }
    

    【讨论】:

    • 文档变量只是一个类 (POCO),表示您要返回的文档的相关信息。这也是在接受的答案中提出的。它可能来自 ORM、来自手动构建的 SQL 查询、来自文件系统(当您从中提取信息时)或其他一些数据存储。这与您的文档字节/文件名/mime 类型来自哪里的原始问题无关,因此它被遗漏以免混淆代码。感谢您提供仅使用文件系统的示例。
    • 当文件名包含 US-ASCII 以外的国际字符时,此解决方案无法正常工作。
    • 谢谢,拯救了我的一天 :)
    • AppDomain.CurrentDomain.BaseDirectory 的替代品是System.Web.HttpContext.Current.Server.MapPath("~"),与本地机器相比,这在真实服务器上可能更有效。
    • 我必须使用 javascript 才能开始下载吗?当我运行和调试我的代码时,什么都没有发生,没有下载或任何东西。
    【解决方案3】:

    查看文件(例如txt):

    return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain);
    

    下载文件(例如txt):

    return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain, "TextFile.txt");
    

    注意:要下载文件,我们应该传递 fileDownloadName 参数

    【讨论】:

    • 这种方法的唯一问题是,只有在使用强制下载的方法时才会提供文件名。当我想让浏览器确定如何打开文件时,它不允许我发送正确的文件名。所以外部应用程序将尝试使用我的 URL 作为文件名。这通常是数据库中文档的某种 id,通常没有文件扩展名。这使得名称与用于访问文件的 URL 不匹配,因为这种情况是如果您不提供。
    • 使用 Content-Disposition 上的 Inline 属性可以让我将设置文件名的能力与是否强制下载的行为区分开来。
    【解决方案4】:

    Darin Dimitrov's answer 是正确的。只是一个补充:

    Response.AppendHeader("Content-Disposition", cd.ToString()); 如果您的响应已包含“Content-Disposition”标头,则可能会导致浏览器无法呈现文件。在这种情况下,您可能需要使用:

    Response.Headers.Add("Content-Disposition", cd.ToString());
    

    【讨论】:

    • 我发现content-type也有影响,对于pdf文件,如果我设置content-type为System.Net.Mime.MediaTypeNames.Application.Octet,即使设置Inline = true也会强制下载,但是如果我设置为Response.ContentType = MimeMapping.GetMimeMapping(filePath),即application/pdf,则可以正常打开而不是下载
    • Response.Headers.Add 需要 IIS 集成管道模式。另外,即使应用程序池设置为集成,它也会抛出异常。解决方案。使用Response.AddHeader。请参阅 SO 线程:stackoverflow.com/questions/22313167/…
    【解决方案5】:

    我相信这个答案更干净,(基于 https://stackoverflow.com/a/3007668/550975)

        public ActionResult GetAttachment(long id)
        {
            FileAttachment attachment;
            using (var db = new TheContext())
            {
                attachment = db.FileAttachments.FirstOrDefault(x => x.Id == id);
            }
    
            return File(attachment.FileData, "application/force-download", Path.GetFileName(attachment.FileName));
        }
    

    【讨论】:

    • 不推荐,Content-Disposition 是清晰和兼容的首选方法。 stackoverflow.com/questions/10615797/…
    • 谢谢。虽然我将 MIME 类型更改为 application/octet-stream 并且仍然导致文件被下载而不是显示,但它似乎是兼容的。
    • 对内容类型撒谎听起来是个非常糟糕的主意。一些浏览器依赖正确的内容类型在“保存或打开”对话框中为用户推荐应用程序。
    • 如果您的意图是让浏览器推荐一个应用程序,那很好,但这个问题专门针对强制下载...
    • @SerjSagan 我认为这更多是为了规避浏览器默认直接查看某些文件类型或使用插件的行为,而不是提供保存/打开之间的选择。没有尝试使用例如JPEG 现在,所以不确定确切的行为。
    【解决方案6】:

    下面的代码对我有用,可以从 API 服务获取 pdf 文件并将其响应到浏览器 - 希望它有所帮助;

    public async Task<FileResult> PrintPdfStatements(string fileName)
        {
             var fileContent = await GetFileStreamAsync(fileName);
             var fileContentBytes = ((MemoryStream)fileContent).ToArray();
             return File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf);
        }
    

    【讨论】:

    • 感谢您的回答。您错过了我正在寻找解决方案的部分,其中包括指定文件名称的能力。您只是返回字节,因此将从 URL 推断名称。我以 PDF 为例,但在我的情况下,我也需要它来处理多种其他文件类型。我应该提到,只要您使用 .NET Framework 4.x 和 MVC Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
    • @NickAlbrecht 我没有使用 .Net Core - 上面的示例用于浏览器上的 pdf 响应。但是,如果您想下载尝试:File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "your file name")。我不确定你是否得到了答案。请让我知道这是否有帮助。不过,感谢 .Net Core 的建议。另外,如果您发现我的回答有用,请添加投票。
    • 我早就用我标记为已接受的答案解决了这个问题。我更多地指出了一些警告,以防您发现它们有用。我最初的问题是 2011 年,所以现在已经过时了。
    • @NickAlbrecht 谢谢。我不知道你解决了它,我确实看到这是一个非常古老的帖子。我在寻找一些与异步相关的答案时发现了这个论坛,我是异步进程的新手。我能够解决我的问题,所以只是分享。比你花时间。
    【解决方案7】:

    FileVirtualPath --> Research\Global Office Review.pdf

    public virtual ActionResult GetFile()
    {
        return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
    }
    

    【讨论】:

    • 这在上一个答案中已经提到过,不推荐。有关原因的更多详细信息,请参阅以下问题。 stackoverflow.com/questions/10615797/…
    • 这样它不会通过将文件加载到服务器上的内存来使用服务器上的资源,对吗?
    • 我相信是的,因为不需要你对@CrashOverride
    【解决方案8】:

    Action 方法需要返回带有文件流、字节[] 或虚拟路径的 FileResult。您还需要知道正在下载的文件的内容类型。这是一个示例(快速/脏)实用程序方法。示例视频链接 How to download files using asp.net core

    [Route("api/[controller]")]
    public class DownloadController : Controller
    {
        [HttpGet]
        public async Task<IActionResult> Download()
        {
            var path = @"C:\Vetrivel\winforms.png";
            var memory = new MemoryStream();
            using (var stream = new FileStream(path, FileMode.Open))
            {
                await stream.CopyToAsync(memory);
            }
            memory.Position = 0;
            var ext = Path.GetExtension(path).ToLowerInvariant();
            return File(memory, GetMimeTypes()[ext], Path.GetFileName(path));
        }
    
        private Dictionary<string, string> GetMimeTypes()
        {
            return new Dictionary<string, string>
            {
                {".txt", "text/plain"},
                {".pdf", "application/pdf"},
                {".doc", "application/vnd.ms-word"},
                {".docx", "application/vnd.ms-word"},
                {".png", "image/png"},
                {".jpg", "image/jpeg"},
                ...
            };
        }
    }
    

    【讨论】:

    【解决方案9】:

    如果您像我一样在学习 Blazor 时通过 Razor 组件了解了这个主题,那么您会发现需要更多地跳出框框思考来解决这个问题。如果(也像我一样)Blazor 是您第一次涉足 MVC 类型的世界,这有点像雷区,因为文档对于此类“琐碎”任务没有帮助。

    因此,在撰写本文时,如果不嵌入 MVC 控制器来处理文件下载部分,则无法使用 vanilla Blazor/Razor 实现此目的,示例如下:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Net.Http.Headers;
    
    [Route("api/[controller]")]
    [ApiController]
    public class FileHandlingController : ControllerBase
    {
        [HttpGet]
        public FileContentResult Download(int attachmentId)
        {
            TaskAttachment taskFile = null;
    
            if (attachmentId > 0)
            {
                // taskFile = <your code to get the file>
                // which assumes it's an object with relevant properties as required below
    
                if (taskFile != null)
                {
                    var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
                    {
                        FileNameStar = taskFile.Filename
                    };
    
                    Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
                }
            }
    
            return new FileContentResult(taskFile?.FileData, taskFile?.FileContentType);
        }
    }
    

    接下来,确保您的应用程序启动 (Startup.cs) 已配置为正确使用 MVC,并且存在以下行(如果没有则添加):

            services.AddMvc();
    

    .. 然后最后修改您的组件以链接到控制器,例如(使用自定义类的基于迭代的示例):

        <tbody>
            @foreach (var attachment in yourAttachments)
            {
            <tr>
                <td><a href="api/FileHandling?attachmentId=@attachment.TaskAttachmentId" target="_blank">@attachment.Filename</a> </td>
                <td>@attachment.CreatedUser</td>
                <td>@attachment.Created?.ToString("dd MMM yyyy")</td>
                <td><ul><li class="oi oi-circle-x delete-attachment"></li></ul></td>
            </tr>
            }
            </tbody>
    

    希望这可以帮助任何努力(像我一样!)在 Blazor 领域为这个看似简单的问题找到适当答案的人......!

    【讨论】:

    • 虽然这可能对遇到与您相同的问题的其他人有用,但如果您发布自己的问题并带有表明它是 Blazor 独有的标题,他们会更容易发现,自己回答,并在此处添加评论,建议任何遇到有关 blazor 问题的人查看您的链接。即使最初的问题是针对 ASP.NET MVC 的,我觉得我已经达到了目标,并调整了它的答案以与 ASP.NET Core 相关。正如您所发现的,Blazor 是完全不同的野兽。
    猜你喜欢
    • 1970-01-01
    • 2011-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多