如何修复 .Net Core POST 操作中的 400 Bad Request 错误?

Posted

技术标签:

【中文标题】如何修复 .Net Core POST 操作中的 400 Bad Request 错误?【英文标题】:How do I fix a 400 Bad Request error in .Net Core POST operation? 【发布时间】:2019-09-07 11:44:51 【问题描述】:

我有一个使用 EF 核心发布数据的 .Net Core 2.1 API。当我从 Postman 向 http://localhost:3642/task/create 发出 POST 请求时,我收到 400 错误请求错误(由于语法错误,请求无法完成)。在四处挖掘后,我得到了一个注释掉的建议来自控制器的 ValidateAntiForgery 令牌。当我通过此更改传递邮递员的请求时,我收到 200 Ok 状态消息,但没有数据被提交到 Sql Server 中的表。我应该在我的 API 中配置什么,我还缺少什么?

我的控制器如下所示:

 [HttpPost]
 // [ValidateAntiForgeryToken]
 public async Task<IActionResult> 
Create([Bind("Assignee,Summary,Description")] TaskViewModel taskViewModel)
    
if (ModelState.IsValid)
            
                _context.Add(taskViewModel);
 await _context.SaveChangesAsync();
                return RedirectToAction("Index");
            
            return View();
        

在 TaskViewModel.cs 我有:

 public class TaskViewModel 

    [Required]
    public long Id  get; set; 

    [Required(ErrorMessage = "Please provide Task Summary")]
    [Display(Name = "Summary")]
    public string Summary  get; set; 

    [Required(ErrorMessage = "Please enter task description")]
    [Display(Name = "Description")]
    public string Description  get; set; 

    [Required(ErrorMessage = "Please select Assignee")]
    [Display(Name = "Assign To")]
    public string Assignee  get; set; 

这是我在 Postman 中的有效载荷:


    "Assignee": "Ed tshuma",
    "Summary": "Finish reconciliations",
    "Description": "collate all the pending data"

【问题讨论】:

Id 是必需的,并且不存在于显示的有效负载中。这会导致 ModelState.IsValid 返回 false 并且不提交。 【参考方案1】:

这里有很多问题。首先,为什么要将视图模型保存到数据库中。在这种情况下,这实际上是一个 entity,而不是视图模型。您绝对应该使用视图模型,但您还应该有一个单独的实体类。然后,您的视图模型应该只包含您希望实际允许用户编辑的属性,完全不需要 Bind 属性,无论如何都应该避免。 (见:Bind is Evil)。

// added "Entity" to the name to prevent conflicts with `System.Threading.Task`
[Table("Tasks")]
public class TaskEntity

    [Key]
    public long Id  get; set; 

    [Required]
    public string Summary  get; set; 

    [Required]
    public string Description  get; set; 

    [Required]
    public string Assignee  get; set; 


public class TaskViewModel

    [Required(ErrorMessage = "Please provide Task Summary")]
    [Display(Name = "Summary")]
    public string Summary  get; set; 

    [Required(ErrorMessage = "Please enter task description")]
    [Display(Name = "Description")]
    public string Description  get; set; 

    [Required(ErrorMessage = "Please select Assignee")]
    [Display(Name = "Assign To")]
    public string Assignee  get; set; 

另外,请注意责任分工。实体只有对数据库重要的东西([Required] 此处表示该列应该是不可为空的)。而视图模型只关心视图。没有Id 属性,因为它不是必需的或不需要的,并且要呈现给用户的显示名称和错误消息仅放置在此处。

然后,您需要从视图模型映射到实体类:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(TaskViewModel model)

    if (!ModelState.IsValid)
        return View(model);

    var task = new TaskEntity
    
        Assignee = model.Assignee,
        Summary = model.Summary,
        Description = model.Description
    ;

    _context.Add(task);
    await _context.SaveChangesAsync();
    return RedirectToAction("Index");

这里的映射相当简单,但您可能更喜欢使用像 AutoMapper 这样的库来为您处理这个问题:_mapper.Map&lt;TaskEntity&gt;(model)

虽然这是专门针对创建操作的,但值得指出更新的细微差别。您需要首先从数据库中检索现有任务,然后将发布的值映射到该任务上。其余的保持相对相同:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update(long id, TaskViewModel model)

    if (!ModelState.IsValid)
        return View(model);

    var task = await _context.Tasks.FindAsync(id);
    if (task == null)
        return NotFound();

    task.Assignee = model.Assignee;
    task.Summary = model.Summary;
    task.Description = model.Description;

    await _context.SaveChangesAsync();
    return RedirectToAction("Index");

最后,关于你的问题的主要问题,有两个问题。首先,此操作是为传统的 html 表单帖子 (x-www-form-urlencoded) 设计的。因此,向它发送 JSON 是没有意义的,并且向它发送 JSON 将不起作用。要在 Postman 中测试它,您应该将请求发送为 x-www-form-urlencoded。如果你不这样做,那么你的模型基本上总是无效的,因为没有任何东西会从帖子正文绑定到你的模型。

为了接收 JSON,您的参数需要应用 FromBody 属性 ([FromBody]TaskViewModel model)。但是,如果您这样做,您将无法再收到传统的表单帖子,在这种情况下,这就是将要发送的内容。如果您通过 AJAX 发送(可以想象使用 JSON),那么您也应该返回 JSON 或PartialView,但不是View 或重定向。

最后,您需要包含请求验证令牌,它应该是帖子正文名称__RequestVerificationToken 中的另一个键。要获取要发送的值,您需要首先加载视图的 GET 版本,然后检查源代码。将有一个隐藏的输入值。

【讨论】:

【参考方案2】:

Chris Pratt 说得对,你需要发送__RequestVerificationToken

如果注释掉[ValidateAntiForgeryToken]属性,好像是从Body-raw-JSON发送数据,那么需要使用[FromBody]来访问数据。

[HttpPost]
public async Task<IActionResult> Create([Bind("Assignee,Summary,Description")] [FromBody] TaskViewModel taskViewModel)

如果您不想添加 [FromBody],可以使用 form-data 发送数据

【讨论】:

你的 2 Headers 是什么?【参考方案3】:

如果你想使用装饰器[ValidateAntiForgeryToken],你必须在你的请求中发送防伪令牌。请参阅this link 了解更多信息。

另外,即使你的模型无效,你return View()。这意味着即使您发送了错误的数据,您也会获得 http 状态 200。

if(ModelState.IsValid) 上设置断点并检查是否输入。如果不是,请检查有效载荷的格式。

希望对你有帮助。

编辑关于您的有效负载和模型:由于 TaskViewModel 中的 [Required] 装饰器,您需要为有效负载提供 Id。或者你需要去掉Id 上的[Required] 属性。如果你不这样做,if (ModelState.IsValid) 将永远是错误的。

【讨论】:

以上是关于如何修复 .Net Core POST 操作中的 400 Bad Request 错误?的主要内容,如果未能解决你的问题,请参考以下文章

在 POST API 中传递多个参数,而不使用 .Net Core MVC 中的 DTO 类

asp net core 3 在发布后获取带有正文内容的 POST 操作的 BadRequest 响应

如何修复 .net core 2.2 应用程序中未找到的 swagger.json

ASP.NET Core Web API HTTP POST 在 Azure 中返回 404

模型绑定不适用于 ASP.NET Core 2 WebAPI 中的 POST 请求

以 axios.post 形式接收 asp.net core web api 中的数组