ASP.NET Core Web API返回400 Bad Request问题排查求助
问题:Blazor调用ASP.NET Core API返回400 Bad Request,无法触发控制器断点
我们有一个Blazor前端应用,后端是采用C#服务层+仓储模式的ASP.NET Core Web API。向IssueHold控制器发送POST请求时,仅返回400 "Bad Request",API控制器方法的断点完全触发不了,也没法查看发送前的请求内容。使用VS 2022开发,相关代码如下:
初始服务调用
await IssueHoldService.CreateIssueHold(issueHold);
服务层方法
public async Task<Warrant> CreateIssueHold(Warrant warrant) { var warrantJson = new StringContent(JsonSerializer.Serialize(warrant), Encoding.UTF8, "application/json"); string x = warrantJson.ToString(); var response = await _httpClient.PostAsync("api/IssueHold", warrantJson); Warrant result = null; if (response.IsSuccessStatusCode) { result = await JsonSerializer.DeserializeAsync<Warrant>(await response.Content.ReadAsStreamAsync()); } return result; }
返回的响应内容
StatusCode: 400, ReasonPhrase: 'Bad Request', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers: { Transfer-Encoding: chunked Server: Microsoft-IIS/10.0 X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcMzIzNjA2XHNvdXJjZVxFZ2lkLlJlY29uXEVnaWQuUmVjb24uQXBpXGFwaVxJc3N1ZUhvbGQ=?= Persistent-Auth: true X-Powered-By: ASP.NET Date: Tue, 13 Jun 2023 18:11:39 GMT Content-Type: application/problem+json; charset=utf-8 }
API控制器方法
[HttpPost] public async Task<IActionResult> Create([FromBody] Warrant warrant) { if (warrant == null) return BadRequest(); if (!ModelState.IsValid) return BadRequest(ModelState); try { var newId = await _issueHoldRepository.CreateAsync(warrant); var createdWarrant = await _issueHoldRepository.GetByIdAsync(newId); return Created("warrant", createdWarrant); } catch { return BadRequest(); } }
仓储层方法
public async Task<int> CreateAsync(Warrant entity) { System.Diagnostics.Debugger.Break(); string sql = @"INSERT INTO [dbo].[IssueHold] ([CheckNumber] ,[IssueSuffix] ,[ControlNumber] ,[AgencyNumber] ,[Name] ,[SSN] ,[Addr1] ,[Addr2] ,[City] ,[St] ,[Zip] ,[Reason] ,[DateIssued] ,[IssuedBy] ,[AmountPaid] ,[AmountDue] ,[RefundAmount] ,[ClaimNumber] ,[DateMailed] ,[DateCreated] ,[CreatedBy] ,[PrintIssue] ,[PrintAmount] ,[Beneficiary] ,[WarrantType] ,[Void] ,[DateRequested] ,[OrigCheckNum]) OUTPUT Inserted.HoldKey VALUES (@CheckNumber , @IssueSuffix , @ControlNumber , @AgencyNumber , @Name , @SSN , @Addr1 , @Addr2 , @City , @St , @Zip , @Reason , @DateIssued , @IssuedBy , @AmountPaid , @AmountDue , @RefundAmount , @ClaimNumber , @DateMailed , @DateCreated , @CreatedBy , @PrintIssue , @PrintAmount , @Beneficiary , @WarrantType , @Void , @DateRequested , @OrigCheckNum)"; try { using (var connection = CreateConnection()) { return (await connection.ExecuteScalarAsync<int>(sql, entity)); } } catch (Exception ex) { throw new Exception(ex.Message, ex); } }
Warrant实体类
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Egid.Recon.Shared.Entities { public class Warrant : EntityBase { public int HoldKey { get; set; } [MaxLength(10)] public string CheckNumber { get; set; } [MaxLength(2)] public string IssueSuffix { get; set; } [MaxLength(10)] public string ControlNumber { get; set; } [MaxLength(10)] public string AgencyNumber { get; set; } [MaxLength(60)] public string Name { get; set; } [MaxLength(9)] public string SSN { get; set; } [MaxLength(50)] public string Addr1 { get; set; } [MaxLength(50)] public string Addr2 { get; set; } [MaxLength(30)] public string City { get; set; } [MaxLength(2)] public string St { get; set; } [MaxLength(10)] public string Zip { get; set; } public string Reason { get; set; } public DateTime? DateIssued { get; set; } [MaxLength(15)] public string IssuedBy { get; set; } //[RegularExpression(@"^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))\)$", ErrorMessage = "Invalid Amount. Please use the format of XXXX.XX.")] public decimal AmountPaid { get; set; } public string AmountPaidDisplay { get { return String.Format("{0:C2}", AmountPaid); } } //[RegularExpression(@"^\$?\-?([1-9]{1}[0-9]{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))$|^\-?\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))$|^\(\$?([1-9]{1}\d{0,2}(\,\d{3})*(\.\d{0,2})?|[1-9]{1}\d{0,}(\.\d{0,2})?|0(\.\d{0,2})?(\.\d{1,2}))\)$", ErrorMessage = "Invalid Amount. Please use the format of XXXX.XX.")] public decimal AmountDue { get; set; } public string AmountDueDisplay { get { return String.Format("{0:C2}", RefundAmount); } } public decimal RefundAmount { get; set; } public string RefundAmountDisplay { get { return String.Format("{0:C2}", RefundAmount); } } [MaxLength(15)] public string ClaimNumber { get; set; } public DateTime? DateMailed { get; set; } public DateTime? DateCreated { get; set; } [MaxLength(15)] public string CreatedBy { get; set; } public bool PrintIssue { get; set; } [MaxLength(250)] public string PrintAmount { get; set; } [MaxLength(40)] public string Beneficiary { get; set; } [MaxLength(9)] public string WarrantType { get; set; } public bool Void { get; set; } public DateTime? DateRequested { get; set; } [MaxLength(9)] public string OrigCheckNum { get; set; } } }
已经尝试在API控制器的!ModelState.IsValid分支添加控制台日志,但没有输出。想问下怎么查看实际请求内容找格式问题?还有哪些排查方向?
排查方案
一、查看实际请求内容的方法
- 在服务层直接输出序列化后的JSON:不要用
warrantJson.ToString()(仅返回类型名),直接获取序列化后的字符串并打印:var warrantJsonStr = JsonSerializer.Serialize(warrant); Console.WriteLine("请求JSON:" + warrantJsonStr); var warrantJson = new StringContent(warrantJsonStr, Encoding.UTF8, "application/json"); - 启用API的HTTP日志:在API项目的
appsettings.json中添加日志配置,开启ASP.NET Core的调试级日志:
启动API后,VS输出窗口会显示完整的请求头和请求体。"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Debug" } } - 抓包工具查看请求:
- Blazor WebAssembly:用浏览器F12打开网络面板,找到目标POST请求,查看
Request Payload; - Blazor Server:用Fiddler抓本地请求,过滤
api/IssueHold地址,查看完整请求详情。
- Blazor WebAssembly:用浏览器F12打开网络面板,找到目标POST请求,查看
二、其他排查思路
- 统一序列化配置:ASP.NET Core默认用System.Text.Json的驼峰命名策略,服务层序列化时要保持一致,避免属性名大小写不匹配导致绑定失败:
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var warrantJsonStr = JsonSerializer.Serialize(warrant, options); - 捕获模型绑定错误:添加全局过滤器,在模型绑定阶段就捕获错误(因为断点没进控制器,说明绑定在进入方法前就失败了):
在public class ModelValidationFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { var errors = context.ModelState.Values.SelectMany(v => v.Errors) .Select(e => e.ErrorMessage); foreach (var error in errors) { Console.WriteLine("模型绑定错误:" + error); } } } public void OnActionExecuted(ActionExecutedContext context) { } }Program.cs注册过滤器:builder.Services.AddControllers(options => { options.Filters.Add<ModelValidationFilter>(); }); - 检查路由匹配:确认服务层调用的
api/IssueHold和控制器路由一致,控制器是否有[Route("api/[controller]")]特性,避免拼写错误。 - 验证实体类约束:检查实体类的
[MaxLength]、[Required]等注解,比如字符串长度是否超限、必填字段是否为空,这些都会触发模型验证失败。 - 直接测试API:用Postman或Swagger构造JSON请求直接调用API,排除Blazor服务层的问题,确认API本身是否正常。
内容的提问来源于stack exchange,提问作者jthorn
相关产品推荐
相关产品推荐

