最稳妥的文件下载方式是直接返回 FileStreamResult 或 PhysicalFileResult,前者适合动态生成内容流式传输,后者适合本地磁盘静态资源;均支持断点续传,但需注意路径安全、权限、ContentType 设置、文件名编码及反向代理兼容性。

直接返回 FileStreamResult 或 PhysicalFileResult 是最稳妥的文件下载方式,绕过内存缓冲、避免大文件 OOM,也天然支持断点续传(前提是客户端发了 Range 请求且服务端没禁用)。
用 PhysicalFileResult 返回本地磁盘文件
适合已知绝对路径、权限可控的静态资源(如上传后存于 wwwroot/downloads/ 的报告 PDF)。
- 必须确保路径真实存在,且 ASP.NET Core 进程有读取权限;否则抛
FileNotFoundException或UnauthorizedAccessException - 不要拼接用户输入到路径里——
Path.Combine("downloads", filename)不防../../../etc/passwd,得先用Path.GetInvalidPathChars()+ 白名单校验或Path.GetFileName()截取纯文件名 - 设置
ContentType影响浏览器行为:设成"application/octet-stream"强制下载;设成"image/png"可能直接在标签页打开 - 示例:
return PhysicalFile(filePath, "application/octet-stream", fileNameOnDownload);
其中第三个参数是用户保存时看到的默认文件名,会自动编码为Content-Disposition的filename*=UTF-8''...格式
用 FileStreamResult 流式返回动态生成内容
适合导出 Excel、ZIP 打包、加密后临时流等无法提前落盘的场景。
- 务必用
using var stream = new MemoryStream();或new FileStream(..., FileAccess.Read, FileShare.Read),别传Request.Body或已关闭的流 - 大文件别用
MemoryStream全载入内存——100MB Excel 会直接触发 GC 压力甚至OutOfMemoryException;改用TempFile-based stream或Chunked transfer配合Response.Body直写(需手动设Content-Length或用分块传输) -
FileStreamResult不自动设Content-Disposition,必须手动加:Response.Headers.Append("Content-Disposition", $"attachment; filename=\"{Uri.EscapeDataString(fileName)}\""); - 记得设
ContentType,比如"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"对应 .xlsx
处理 Range 请求实现断点续传
浏览器下载中断重试、wget -c、手机 APP 后台续传都依赖这个。ASP.NET Core 默认对 PhysicalFileResult 启用,但自定义流需手动支持。
- 检查
Request.Headers["Range"]是否存在,格式如"bytes=0-1023";不存在就按全量返回 - 用
Response.StatusCode = 206,并写Content-Range头:"bytes 0-1023/123456" - 跳过文件开头字节(
stream.Seek(start, SeekOrigin.Begin)),再只读取end - start + 1字节 - 关键:别用
FileStreamResult包裹部分流——它会尝试读完整个流;应直接操作Response.Body写入分段数据 - 小提示:IIS 或 Nginx 可能吞掉
206响应,需确认反向代理配置中未禁用Accept-Ranges
真正容易被忽略的是文件名编码和代理兼容性:中文名在旧版 IE 里要 filename(非 filename*),而 Nginx 默认不透传 Content-Disposition 中的 UTF-8 编码字段,得配 underscores_in_headers on; 并确保 header 名大小写匹配。这些细节不测到真实环境根本不会暴露。










