最近要使用 ASP.NET CORE webAPI 用来下载文件, 使用的. NET CORE 3.1. 考虑如下场景:
文件是程序生成的.
文件应该能兼容各种格式.
浏览器可以感知进行下载.
准备
经过简单的调研, 得到以下结论.
ASP.NET CORE 提供 FileResult 这种类型的 ActionResult, 可以直接返回文件结果, 不需要直接处理 HttpResponse.
通过 Stream 可以直接返回文件流供浏览器下载.
FileStreamResult 是 FileResult 的具体实现, 返回值应该是此类对象.
Stream 有多种类型, 适合直接内存中生成文件对象的是 MemoryStream.
对目标有了基础的了解, 就可以开始动手实现了.
实现
建立好 ASP.NET CORE WEBAPI 工程, 把生成文件的代码独立出来一个函数. 我这里需要是下载一个 CSV 格式的文件, 因此生成一个 CSV 文件.
对于磁盘上的文件, 可以使用 FileStream 对象, 由于我这里需要运行中生成这个文件, 需要使用 MemoryStream.
- using var stream = new MemoryStream();
- using var writer = new StreamWriter(stream);
- // 生成标题
- var propCollection = ttype.GetProperties();
- foreach (var n in propCollection)
- {
- writer.Write(n.Name);
- writer.Write(",");
- }
- writer.WriteLine();
- // 生成内容
- foreach (var item in res)
- {
- foreach (var n in propCollection)
- {
- writer.Write(Convert.ToString(n.GetValue(item)));
- writer.Write(",");
- }
- writer.WriteLine();
- }
请不要考虑里面反射的相关内容, 按照自己的逻辑生成 CSV 即可, 我只是懒得改代码而已.
代码中使用到了一些新的语法特性, 请注意对低版本的. NET 不一定适用.
直接返回 Stream 对象给 Controller 处理, 处理代码如下:
- var res = await info.GetAllQueryResult();
- var actionresult = new FileStreamResult(res, new Microsoft.NET.Http.Headers.MediaTypeHeaderValue("text/csv"));
- return actionresult;
CSV 的 Content-Type 是 text/CSV, 如果下载别的文件, 请自行查询 MIME 格式.
调试
直接执行上面的代码, 直接报错 "无法读取已经关闭的流". 猜测是离开 using 语句块的时候, stream 自动被关闭了. 改动很简单, 去掉 using 语句, 不再报相同错误.
但是返回的文件长度一直是 0, 单步调试发现 Writer 执行完毕之后, stream 返回的长度是 0, 内容实际上并没有写入, 想起有一个 Flush(), 可以添加以确保数据写入.
单步显示 stream 长度有了, 但是返回的长度还是 0. 继续单步调试发现 Stream 的 Postion 是停在文件结尾的, 这个和直接开始读取文件完全不一样, 文件读取一般是从开头开始的, 于是直接设置 Postion 为 0, 问题解决.
下载能够成功了, 但是文件名一直显示的是随机生成的, 体验很差. 设置一下 FileDownloadName 即可.
核心代码如下:
- public async Task<Stream> GetAllQueryResult()
- {
- var stream = new MemoryStream();
- var writer = new StreamWriter(stream);
- // 生成标题
- var propCollection = ttype.GetProperties();
- foreach (var n in propCollection)
- {
- writer.Write(n.Name);
- writer.Write(",");
- }
- writer.WriteLine();
- // 生成内容
- foreach (var item in res)
- {
- foreach (var n in propCollection)
- {
- writer.Write(Convert.ToString(n.GetValue(item)));
- writer.Write(",");
- }
- writer.WriteLine();
- }
- writer.Flush();
- stream.Position = 0;
- return stream;
- }
- [HttpPost("file")]
- [ProducesResponseType(typeof(FileResult), Status200OK)]
- public async Task<FileResult> Download()
- {
- var info = new Info();
- var res = await info.GetAllQueryResult();
- var actionresult = new FileStreamResult(res, new Microsoft.NET.Http.Headers.MediaTypeHeaderValue("text/csv"));
- actionresult.FileDownloadName = "Carinfos.csv";
- //Response.ContentLength = res.Length;
- return actionresult;
- }
使用 swagger 调用, 最后效果:
总结
后来查了一些资料, 总结了一下:
MemoryStream 如果使用 using 语句, 会在离开代码块的时候自动关闭, 实际上 ASP.NET CORE 会自动处理关闭的事项, 不需要使用 using 语句.
由于生成文件的过程是从文件流的开头一直进行到末尾的, 因此向请求端返回结果时, 应当重置 Stream 的游标, 从 0 开始传输.
记得在使用 writer 之后使用 Flush() 以确保数据有写入.
如果不确定文件格式, 可以直接返回 MIME 值为 application/oct-stream.
设置 FileStreamResult 的 FileDownloadName 属性可以修改文件的默认名称.
(可选) 可以通过设置 Response.ContentLength 来设置文件的长度.
参考资料:
来源: https://www.cnblogs.com/podolski/p/12682978.html