如何让控制器Action同时支持表单与JSON请求?
问题原因
ASP.NET Core 不允许在单个 Action 中同时使用 [FromForm] 和 [FromBody] 特性。[FromBody] 会强制框架使用对应 JSON 格式的输入格式化器处理请求,而表单提交的 Content-Type(如 application/x-www-form-urlencoded)与 JSON 请求的 Content-Type(application/json)不匹配,导致表单请求触发 415 媒体类型错误。
解决方案
以下两种方案均可在保留同一路由的前提下解决问题:
方案一:拆分 Action + [Consumes] 特性区分请求类型
这是最简洁易维护的方案,通过两个同名 Action,结合 [Consumes] 特性指定各自支持的媒体类型,框架会自动根据请求的 Content-Type 匹配对应的 Action:
// 处理表单提交请求(支持application/x-www-form-urlencoded和multipart/form-data) [HttpPost] [Consumes("application/x-www-form-urlencoded", "multipart/form-data")] public async Task<ActionResult<Project>> PostProject([FromForm] string name = "") { GenericResponse result = new GenericResponse(); try { if (!string.IsNullOrEmpty(name)) { result = Utils.Helpers.Post(name, "projects"); } else { result.message = "未提供有效的name参数"; } } catch (Exception ex) { result.message = ex.Message; } return new JsonResult(result); } // 处理JSON对象请求(支持application/json) [HttpPost] [Consumes("application/json")] public async Task<ActionResult<Project>> PostProject([FromBody] Project aProject) { GenericResponse result = new GenericResponse(); try { if (aProject != null) { // 处理完整Project对象的业务逻辑 } else { result.message = "未提供有效的Project对象"; } } catch (Exception ex) { result.message = ex.Message; } return new JsonResult(result); }
方案二:自定义模型绑定器(适合逻辑高度复用场景)
如果两个请求的业务逻辑高度重叠,可以自定义模型绑定器,让单个 Action 同时支持两种输入类型:
1. 定义统一请求模型
public class ProjectRequest { public string Name { get; set; } public Project Project { get; set; } }
2. 实现自定义模型绑定器
public class ProjectRequestModelBinder : IModelBinder { public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var request = bindingContext.HttpContext.Request; var contentType = request.ContentType ?? string.Empty; var result = new ProjectRequest(); if (contentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)) { // 解析JSON请求 result.Project = await request.ReadFromJsonAsync<Project>(); } else if (contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase)) { // 解析表单请求 result.Name = bindingContext.ValueProvider.GetValue("name").FirstValue; } else { bindingContext.ModelState.AddModelError(string.Empty, "不支持的媒体类型"); return; } bindingContext.Result = ModelBindingResult.Success(result); } } // 注册绑定器提供器 public class ProjectRequestModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { return context.Metadata.ModelType == typeof(ProjectRequest) ? new ProjectRequestModelBinder() : null; } }
3. 注册绑定器(Program.cs/Startup.cs)
// 在Program.cs中配置控制器服务时添加绑定器 builder.Services.AddControllers(options => { options.ModelBinderProviders.Insert(0, new ProjectRequestModelBinderProvider()); });
4. 修改Action使用统一模型
[HttpPost] public async Task<ActionResult<Project>> PostProject(ProjectRequest request) { GenericResponse result = new GenericResponse(); try { if (!string.IsNullOrEmpty(request.Name)) { result = Utils.Helpers.Post(request.Name, "projects"); } else if (request.Project != null) { // 处理完整Project对象的业务逻辑 } else { result.message = "未提供有效参数"; } } catch (Exception ex) { result.message = ex.Message; } return new JsonResult(result); }
方案选择
- 优先选择方案一:代码拆分清晰,无需额外自定义组件,维护成本更低。
- 当两种请求的业务逻辑高度一致时,再考虑方案二。
内容的提问来源于stack exchange,提问作者pkExec
相关产品推荐
相关产品推荐

