ASP.NET中不使用IFormFile单请求发送对象与文件的问题
问题描述
在单个PUT请求中同时发送Pet模型数据和文件时,返回400 Bad Request,错误提示“model.Name字段是必填项”。由于Pet模型需要在Web上下文之外复用,不想修改模型结构,也不想拆分成两次请求,现有代码如下:
Pet模型代码
public class Pet { public Guid Id { get; set; } public string Name { get; set; } }
PUT控制器方法代码
[HttpPut("{id:guid}")] public ActionResult EditPet(Guid id, [FromForm]Pet model) { ArgumentNullCheck(model); try { _logger.LogInformation($"Request recieved to edit pet with id {id}"); //Update model in repo if (Request.Form.Files.Count != 0) { IFormFile file = Request.Form.Files[0]; //Store file locally. } return StatusCode(StatusCodes.Status200OK); } catch (Exception e) { _logger.LogError($"Error: {e.Message} when attempting to edit pet with id {id}"); return UnprocessableEntity(); } }
fetch请求代码
class Pet { name : string; id : string; constructor() { this.name = ""; this.id = ""; } } let model = new Pet(); model.id = //some GUID; model.name = "Cheddar"; let payLoad = new FormData(); payLoad.append("model", JSON.stringify(model)); let file = getFileSomehow(); if (file) { payLoad.append("file", file); } let response: Response; try { response = await fetch("../Pets/model.id", { method: 'PUT', headers: { 'Content-type': "multipart/form-data" }, body: payload }); } catch (err) { console.error(`Error processing update request, ${err}`); return; }
可行解决方案
方案1:前端调整FormData参数格式(推荐)
后端[FromForm]Pet model期望表单字段与Pet属性一一映射,而非传入整个JSON字符串。只需修改前端FormData的构造方式,同时修正URL拼接和请求头问题:
修改后的前端代码:
class Pet { name : string; id : string; constructor() { this.name = ""; this.id = ""; } } let model = new Pet(); model.id = "具体GUID值"; model.name = "Cheddar"; let payload = new FormData(); // 逐个添加模型属性,而非整个JSON字符串 payload.append("Name", model.name); payload.append("Id", model.id); let file = getFileSomehow(); if (file) { payload.append("file", file); } let response: Response; try { // 用模板字符串正确拼接ID参数 response = await fetch(`../Pets/${model.id}`, { method: 'PUT', // 移除手动设置的Content-Type,浏览器会自动生成带boundary的正确请求头 body: payload }); } catch (err) { console.error(`Error processing update request, ${err}`); return; }
后端代码无需修改,[FromForm]Pet model会自动映射表单字段到Pet对象。
方案2:后端自定义模型绑定(适配原前端逻辑)
如果前端不想修改传参方式,可以通过自定义模型绑定器解析FormData中的JSON字符串:
- 创建自定义模型绑定器:
public class JsonFormDataBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var modelName = bindingContext.ModelName; var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask; bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); var jsonString = valueProviderResult.FirstValue; if (string.IsNullOrEmpty(jsonString)) return Task.CompletedTask; try { var model = JsonSerializer.Deserialize(jsonString, bindingContext.ModelType, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); bindingContext.Result = ModelBindingResult.Success(model); } catch (JsonException ex) { bindingContext.ModelState.TryAddModelError(modelName, ex.Message); } return Task.CompletedTask; } }
- 修改控制器方法的模型绑定方式:
[HttpPut("{id:guid}")] public ActionResult EditPet(Guid id, [ModelBinder(typeof(JsonFormDataBinder))]Pet model) { ArgumentNullCheck(model); try { _logger.LogInformation($"Request recieved to edit pet with id {id}"); //Update model in repo if (Request.Form.Files.Count != 0) { IFormFile file = Request.Form.Files[0]; //Store file locally. } return StatusCode(StatusCodes.Status200OK); } catch (Exception e) { _logger.LogError($"Error: {e.Message} when attempting to edit pet with id {id}"); return UnprocessableEntity(); } }
此方案下前端可保留原payLoad.append("model", JSON.stringify(model))逻辑,但必须移除手动设置的Content-Type请求头。
关键注意点
- 禁止手动设置
multipart/form-data请求头:浏览器会自动生成包含boundary的正确请求头,手动设置会导致后端无法解析请求体。 - 修正URL拼接错误:原代码中
"../Pets/model.id"是字面量字符串,需用模板字符串../Pets/${model.id}正确拼接GUID参数。
内容的提问来源于stack exchange,提问作者ManGoose
相关产品推荐
相关产品推荐

