ASP.NET Web API [FromForm]对象列表自定义绑定Postman失效Swagger可用
我有一个控制器方法,用于接收包含基本类型、IFormFile文件和对象列表的multipart/form-data请求。针对[FromForm]复杂类型实现了自定义模型绑定器,Swagger中请求可正常绑定,但在Postman或SPA中绑定失效。
控制器方法代码:
public async Task<IActionResult> Post([FromForm] AddRequest addRequest) { var result = await _service.AddAsync(addRequest); if (result.Success) { return Ok(result); } return BadRequest(); }
请求模型类:
public class AddRequest { public string? Name { get; set; } public string? Description { get; set;} public IFormFile? File { get; set; } public IEnumerable<LocaleViewModel>? Locales { get; set; } }
视图模型:
[ModelBinder(BinderType = typeof(MetadataValueModelBinder))] public class LocaleViewModel { public long Id { get; set; } public string Code { get; set; } }
自定义模型绑定器代码:
public class MetadataValueModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (values.Length == 0) return Task.CompletedTask; var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }; var deserialized = JsonSerializer.Deserialize(values.FirstValue, bindingContext.ModelType, options); bindingContext.Result = ModelBindingResult.Success(deserialized); return Task.CompletedTask; } }
问题表现:Swagger请求时模型名称为Locales,绑定正常;但Postman/SPA请求时模型名称变为Locales[0],绑定失效。想知道是模型绑定器存在遗漏还是实现方式错误,尝试理解ValueProvider.GetValue的工作机制但未成功。
问题原因
问题根源在于自定义绑定器的逻辑与不同客户端的参数提交格式不兼容:
- Swagger的提交逻辑:Swagger会将
Locales序列化为单个JSON字符串,以Locales作为参数名提交,你的绑定器通过GetValue(bindingContext.ModelName)能直接拿到完整的JSON内容,反序列化后得到集合对象,因此绑定正常。 - Postman/SPA的提交逻辑:这类客户端默认会按照表单集合的规范提交数据,要么将集合拆分为
Locales[0].Id、Locales[0].Code这类细粒度键值对,要么以Locales[0]、Locales[1]作为参数名传递单个对象的JSON。而你的绑定器仅处理了单个Locales参数的情况,既不识别带索引的参数名,也没有逻辑将多个元素组合成完整集合。
另外,原绑定器是绑定在LocaleViewModel单个对象上的,但AddRequest中的Locales是集合类型,当客户端提交索引式参数时,绑定上下文的ModelName会变成Locales[0],此时绑定器无法匹配到目标参数,直接返回空结果。
解决方案
方案1:修改绑定器支持集合与索引参数
实现一个针对IEnumerable<LocaleViewModel>的模型绑定器,同时兼容Swagger的单JSON参数和Postman/SPA的索引式参数:
首先,移除LocaleViewModel上的[ModelBinder]特性,然后创建集合绑定器:
public class LocaleCollectionModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); // 确认当前绑定的是IEnumerable<LocaleViewModel>类型 if (!typeof(IEnumerable<LocaleViewModel>).IsAssignableFrom(bindingContext.ModelType)) { bindingContext.Result = ModelBindingResult.Failed(); return Task.CompletedTask; } var localeList = new List<LocaleViewModel>(); var valueProvider = bindingContext.ValueProvider; var baseKey = bindingContext.ModelName; // 处理Postman/SPA的索引式参数(如Locales[0], Locales[1]) int index = 0; while (true) { var indexedKey = $"{baseKey}[{index}]"; var value = valueProvider.GetValue(indexedKey); if (value.Length == 0) break; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var locale = JsonSerializer.Deserialize<LocaleViewModel>(value.FirstValue, options); if (locale != null) localeList.Add(locale); index++; } // 兼容Swagger的单JSON参数格式 if (localeList.Count == 0) { var singleValue = valueProvider.GetValue(baseKey); if (singleValue.Length > 0) { var locales = JsonSerializer.Deserialize<List<LocaleViewModel>>(singleValue.FirstValue, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (locales != null) localeList = locales; } } bindingContext.Result = ModelBindingResult.Success(localeList); return Task.CompletedTask; } }
然后在AddRequest的Locales属性上指定该绑定器:
public class AddRequest { public string? Name { get; set; } public string? Description { get; set;} public IFormFile? File { get; set; } [ModelBinder(BinderType = typeof(LocaleCollectionModelBinder))] public IEnumerable<LocaleViewModel>? Locales { get; set; } }
方案2:统一客户端提交格式
要求Postman/SPA端按照Swagger的格式提交Locales参数:在form-data中添加名为Locales的参数,值为完整的集合JSON字符串(例如[{"Id":1,"Code":"zh-CN"},{"Id":2,"Code":"en-US"}])。
关于ValueProvider.GetValue的说明
ValueProvider.GetValue会根据传入的键名,从请求的表单、查询字符串等数据源中查找对应的值:
- 如果找到匹配的键,返回包含对应值的
ValueProviderResult; - 如果有多个同名键,返回的结果会包含所有值;
- 找不到匹配键时,返回空的
ValueProviderResult。
原绑定器中,当客户端提交Locales[0]时,bindingContext.ModelName是Locales,因此GetValue("Locales")找不到对应值,导致绑定失败。
内容的提问来源于stack exchange,提问作者sova420

