坑 1:ViewModel 里嵌套的 IFormFile 数组不绑定

页面用一个列表来收集每行的附件,前端 name="Items[i].File" 命名,后端期望模型绑定到 List<RowVm>:

public class RowVm
{
    public string Title { get; set; }
    public IFormFile File { get; set; }   // ❌ 不会被默认绑定
}

public class FormVm
{
    public List<RowVm> Items { get; set; }
}

实际收到的 Items[i].File 始终为 null。原因:模型绑定器默认只认顶层的 IFormFile,嵌套对象里的不会自动映射。

解决方案有两个:

  1. 在 Action 上单独接收 IFormFileCollection,再按 name 自行匹配;
  2. 写一个自定义 IModelBinder 显式解析嵌套文件(适合复杂场景)。

最常用的就是方案 1:

[HttpPost]
public async Task<IActionResult> Upload(
    [FromForm] FormVm vm,
    IFormFileCollection files,
    CancellationToken ct)
{
    // 按 input 顺序 / name 自行匹配到 vm.Items
    for (int i = 0; i < vm.Items.Count && i < files.Count; i++)
    {
        var file = files[i];
        // ... 落盘等
    }
    return Ok();
}

坑 2:同名多文件,未选中的项不会为 null

前端是这种结构:

<input type="file" name="photos" multiple />
<input type="file" name="photos" multiple />
<input type="file" name="photos" multiple />

三个 input 共用 photos。后端用:

public IFormFile[] Photos { get; set; }

如果用户只选了第二个,前端只发出一个 photos 文件。不要假设 Photos 长度永远是 3,也不会有 null 填充:

// Photos 实际是 1 个元素,不是 3 个元素 + 2 个 null
foreach (var f in vm.Photos)
{
    // f 一定不是 null,但数组长度可能跟前端 input 数量不一致
}
这跟传统 Request.Files 行为不同:旧 ASP.NET 时代会把空 input 留 null,ASP.NET Core 直接跳过。

坑 3:上传大小被默认上限卡住

ASP.NET Core 默认 单文件 128 MB,整次请求 28.6 MB。Kestrel 上限独立配置。

全局调整(Program.cs):

builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 500 * 1024 * 1024; // 500 MB
});

builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 500 * 1024 * 1024;
});

单个 Action 临时调整:

[HttpPost]
[RequestSizeLimit(500L * 1024 * 1024)]   // 500 MB
[RequestFormLimits(MultipartBodyLengthLimit = 500L * 1024 * 1024)]
public async Task<IActionResult> Upload(IFormFile file) { ... }
如果部署在 IIS / Nginx 后面,三层都要改:Kestrel、ASP.NET Core、IIS(web.config 的 requestLimits maxAllowedContentLength)/ Nginx(client_max_body_size)。否则任意一层都会先拒。

顺手处理:上传安全

  • 校验扩展名 + MIME(只信 ContentType 不可靠,前端可伪造)。
  • 落盘前不信任原文件名,用 Path.GetRandomFileName() 重命名。
  • 大文件用流式落盘:await using var fs = System.IO.File.Create(path); await file.CopyToAsync(fs);。
  • 对图片用 ImageSharp / System.Drawing 做解码校验,避免把恶意 SVG / 畸形 PNG 落库。

使用准则

  1. 上传相关属性放在 Action 参数最前面,不要塞进嵌套 ViewModel。
  2. IFormFile[] 永远按实际收到的元素遍历,不要指望与 input 数量一致。
  3. 调大上传上限必须改三层:Kestrel / FormOptions / 反向代理。
  4. 大文件上传走分片或 tus 协议,不要把整个请求体塞进内存。
参考资料