asp.net core大文件上传需流式处理:禁用模型绑定、直接读取请求流写入磁盘;下载应使用physicalfile或手动filestream写响应体,避免内存加载;httpclient上传需自定义流式multipart或改用streamcontent。

ASP.NET Core 中用 IFormFile 上传小文件没问题,但大文件直接崩
默认情况下,IFormFile 会把整个上传内容读进内存,超过默认 128MB(或你配置的 MaxRequestBodySize)就抛 Request body too large。更糟的是,即使调大限制,内存暴涨、GC 压力大、IIS/Kestrel 可能直接杀掉请求。
真正可行的方案是绕过 IFormFile,直接读取原始请求流,并配合 FileStream 边读边写磁盘——也就是流式上传。
- 禁用模型绑定:在 Action 上加
[DisableRequestSizeLimit],并确保中间件没提前读取 Body(比如没启用UseDeveloperExceptionPage或UseHttpLogging在开发环境偷偷读了流) - 手动读取:用
HttpContext.Request.Body,配合FileStream的CopyToAsync(推荐)或分块ReadAsync+WriteAsync - 注意设置超时:
HttpContext.Request.EnableBuffering()不要调用;同时在 Kestrel 配置里调大KeepAliveTimeout和RequestHeadersTimeout
下载大文件别用 FileStreamResult,它会全量加载到内存
FileStreamResult 内部会调用 Stream.CopyToAsync,但前提是它得先打开文件流——问题不大;真正踩坑的是你如果用 MemoryStream 构造它,或者用 File.ReadAllBytes 先读进内存再返回,那 500MB 文件就直接 OOM。
正确做法是让 ASP.NET Core 直接接管文件流,不经过中间容器:
- 用
PhysicalFile(path, contentType):框架自动按范围(Range)处理、支持断点续传、底层走零拷贝(Windows 上是TransmitFile,Linux/macOS 是sendfile) - 若需动态生成或权限校验,用
FileStream+HttpResponse.Body手动写:先设Response.ContentType和Content-Length(或留空走 chunked),再await fileStream.CopyToAsync(Response.Body) - 务必禁用响应缓冲:在
Startup.ConfigureServices中调用services.Configure<iisserveroptions>(o => o.AutomaticAuthentication = false)</iisserveroptions>不相关;真正要关的是Response.Buffering—— 但 ASP.NET Core 6+ 已移除该 API;替代方案是确保没调用Response.StartAsync前写入任何内容,且不要用ActionResult<byte></byte>
HttpClient 上传大文件必须禁用请求体自动缓存
客户端用 HttpClient 上传大文件时,如果构造 MultipartFormDataContent 并把 FileStream 直接塞进去,看似流式,实则 MultipartFormDataContent 默认会把整个内容读进内存再发出去——因为它的 SerializeToStreamAsync 实现内部用了 MemoryStream 缓冲。
解决方法只有一个:自己实现流式 multipart,绕过 MultipartFormDataContent:
- 手动写 boundary,用
HttpResponseMessage.Content的底层HttpContent子类(如自定义StreamingMultipartContent) - 更简单务实的做法:改用
StreamContent+ 自定义 HTTP 头,后端接收方也对应改成 raw body 解析(适合前后端可控场景) - 关键点:
HttpClient.DefaultRequestHeaders.TransferEncodingChunked = true要设为false(chunked 会干扰 multipart boundary),同时确保服务端没开启Request.EnableBuffering()
流式传输中容易被忽略的异常和清理逻辑
流没关、文件句柄没释放、临时文件残留——这些不会立刻报错,但压测一跑就暴露:磁盘占满、句柄耗尽、后续上传失败。
- 所有
FileStream必须用using或await using;特别是上传中途异常时,finally里删临时文件(但注意:正在写的文件删不掉,得先Dispose流) - 上传成功前别删原临时路径,上传失败后才清理;可用 GUID 命名临时文件,加
.tmp后缀,成功后再File.Move - 下载时若用户中断连接(
Response.HttpContext.RequestAborted.IsCancellationRequested为 true),应主动break拷贝循环,并释放流——否则线程卡住,连接不释放
流式不是加个 async 就完事,每一步 IO 都得对上下文生命周期负责。临时文件、句柄、取消令牌,漏一个,上线后就是半夜告警。











