不能直接用IFormFile绑定,因其会将整个文件加载进内存导致OOM或超限异常;必须绕过模型绑定,手动接管HttpRequest.Body流式写入GridFSBucket。

GridFS上传大文件时为什么不能直接用IFormFile绑定
因为IFormFile会把整个文件加载进内存,超过默认限制(如64MB)直接抛出InvalidDataException或Request body too large。GridFS本身支持分块存储,但ASP.NET Core的模型绑定层不自动适配流式写入——你得绕过IFormFile,手动接管HttpRequest.Body。
常见错误现象:System.IO.IOException: The request stream was already read.,多发生在你试图先读一次Body再传给GridFS时,因为HttpRequest.Body是不可重置的单次读取流。
- 必须在
ConfigureServices中禁用自动模型绑定对文件的缓冲:设置DisableMultipartBuffering = true - 确保
MaxRequestBodySize设为null(不限制)或足够大,否则中间件会在到达控制器前就截断请求 - 不要调用
Request.Form.Files或任何触发ReadFormAsync的操作
如何用FileStream配合GridFSBucket实现边读边存
核心是把HttpRequest.Body作为源流,直接喂给GridFSBucket.OpenUploadStream返回的Stream。MongoDB官方驱动的OpenUploadStream接受任意Stream,且内部按255KB分块写入,天然适配大文件。
关键参数注意:chunkSizeBytes建议保持默认255KB(即261120),改小会增加元数据开销,改大会拖慢失败回滚速度;metadata字典可存ContentType、FileName等,后续查询依赖它。
var bucket = new GridFSBucket(database);
using var uploadStream = await bucket.OpenUploadStreamAsync(
fileName,
new GridFSUploadOptions {
ChunkSizeBytes = 261120,
Metadata = new BsonDocument {
{ "contentType", contentType },
{ "uploadedAt", DateTime.UtcNow }
}
});
await Request.Body.CopyToAsync(uploadStream); // 直接管道传输
var fileId = uploadStream.Id;
-
CopyToAsync不会把整个文件加载进内存,底层是循环ReadAsync/WriteAsync - 务必用
using包裹uploadStream,否则UploadStream未关闭会导致文件元数据不写入fs.files集合 - 如果需要校验MD5,得自己在复制前计算
Request.Body的哈希——但注意:Body只能读一次,校验和上传必须串行或用NonSeekableStream包装
前端表单和HttpClient上传时的关键配置
后端流式接收的前提是前端不把文件转成base64或一次性FormData.append塞满内存。浏览器原生<input type="file">配合fetch最稳妥。
错误做法:FormData.append('file', file) + fetch(..., { body: formData }) —— 这会触发浏览器自动生成multipart边界,而ASP.NET Core的MultipartReader默认开启缓冲,跟DisableMultipartBuffering = true冲突,导致Body为空。
- 正确方式:用
file.arrayBuffer()或file.stream()获取原始流,fetch直接传Body(Content-Type留空,由服务端忽略) - 若用
HttpClient(如后台调用),构造StreamContent并显式设置Headers.ContentType,避免自动加multipart/form-data - Nginx/Apache反向代理需调大
client_max_body_size和proxy_buffering off,否则会在网关层缓存整个请求体
上传中断或客户端断连时如何清理残留块
GridFS没有自动垃圾回收。如果上传中途断开(如用户刷新页面、网络闪断),fs.chunks里会留下孤立块,fs.files里无对应条目。这些块不会被Drop操作清理,长期积累浪费空间。
官方不提供内置清理工具,必须手动处理。安全做法是定期运行一个后台任务,比对fs.chunks中files_id是否存在于fs.files中:
// MongoDB Shell 示例(生产环境建议用C#驱动+聚合管道)
db.fs.chunks.aggregate([
{ $lookup: { from: "fs.files", localField: "files_id", foreignField: "_id", as: "file" } },
{ $match: { "file.0": { $exists: false } } },
{ $project: { _id: 1 } }
])
- 不要在上传过程中实时校验——会影响性能,且并发上传时可能误删正在写的块
- 清理频率建议每小时一次,配合
createdAt字段过滤2小时以上的孤立块(给上传留足超时余量) - 执行
deleteMany前先countDocuments,异常数量突增说明上游有bug(比如重复调用OpenUploadStream没关闭)
真正麻烦的不是上传逻辑本身,而是流没关干净、中间件提前截断、反向代理吞掉部分数据——这些问题不会报错,只会让文件变短或元数据缺失,得靠日志里比对Content-Length和实际写入字节数才能发现。


















