有一天,当我出现一个关于模型绑定的问题时,我正在追赶最新的ASP.NET社区Standup,这个问题我之前没有接过(你可以在46:30左右看到这个问题)。它指出在ASP.NET Core(ASP.NET 5 的新名称)中,您不能再简单地将JSON数据发布到MVC控制器并自动绑定它,您以前可以在ASP.NET 4 / MVC中执行此操作5。
在这篇文章中,我将展示如果您将项目转换为ASP.NET Core并且发现您的JSON POST
不起作用该怎么办。我将演示MVC 5模型绑定和MVC Core模型绑定之间的区别,突出显示两者之间的差异,以及如何根据您期望的数据为您的项目设置控制器。
TL; DR:将
[FromBody]
属性添加到ASP.NET Core控制器操作中的参数注意,如果您使用的是ASP.NET Core 2.1,则还可以使用该[ApiController]
属性自动推断[FromBody]
复杂操作方法参数的绑定源。有关详细信息,请参阅文档
我的数据在哪里?
想象一下,您已经创建了一个闪亮的新ASP.NET核心项目,您正在使用它来重写现有的ASP.NET 4应用程序(当然只是出于明智的原因!)您将旧的WebApi控制器复制并粘贴到.NET Core Controller中,清理命名空间,测试GET
操作,一切似乎都运行良好。
注意:在ASP.NET 4中,尽管MVC和WebApi管道的行为非常相似,但它们完全是分开的。因此,您分别拥有WebApi和Mvc的单独
ApiController
和Controller
类(以及所有相关的命名空间混淆)。在ASP.NET Core中,管道都已合并,只有单个Controller
类。
当您的GET
请求正常工作时,您知道您的大部分管道(例如路由)可能已正确配置。您甚至可以提交一个测试表单,该表单将一个发送POST
到控制器并接收它发回的JSON值。一切都很好看。
作为拼图的最后一部分,你测试发送一个POST
带有JSON数据的AJAX ,它们都崩溃了 - 你收到了一个200 OK
,但你对象上的所有属性都是空的。但为什么?
什么是模型绑定?
在我们详细了解这里发生的事情之前,我们需要对模型绑定有一个基本的了解。模型绑定是MVC或WebApi管道获取原始HTTP请求并将其转换为控制器上的操作方法调用的参数的过程。
例如,考虑以下WebApi控制器和Person
类:
public class PersonController : ApiController
{
[HttpPost]
public Person Index(Person person)
{
return person;
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
我们可以看到控制器上有一个动作方法,一个POST
动作,它接受一个参数 - 一个Person
类的实例。然后控制器只是回显该对象,然后回到响应中。
那么Person
参数来自哪里?模型绑定救援!模型绑定器可以在许多不同的位置查找数据以便水合人物对象。模型绑定器具有高度可扩展性,并允许自定义实现,但常见绑定包括:
- 路由值 - 导航到诸如
{controller}/{action}/{id}
允许绑定到id
参数的路由 - 查询字符串 - 如果您已将变量作为查询字符串参数传递,例如
?FirstName=Andrew
,则FirstName
可以绑定参数。 - 正文 - 如果您在帖子的正文中发送数据,则可以将其绑定到Person对象
- 标头 - 您也可以绑定到HTTP标头值,尽管这种情况不太常见。
因此,您可以看到有多种方法可以将数据发送到服务器,并让模型绑定器自动为您创建正确的方法参数。有些需要explcit配置,而有些则需要免费获得。例如,路径值和查询字符串参数始终是绑定的,对于复杂类型(即不是像string
或的基元int
),主体也是绑定的。
重要的是要注意,如果模型绑定器由于某种原因未能绑定参数,它们将不会抛出错误,而是您将收到一个默认对象,没有设置任何属性,这是我们之前显示的行为。
它在ASP.NET 4中的工作原理
为了解决这里发生的事情,我创建了两个项目,一个使用ASP.NET 4,另一个使用最新的ASP.NET Core(在编写本文时非常接近RC2)。你可以在这里和这里的 GitHub上找到它们。
在ASP.NET WebApi项目中,有一个简单的控制器,它接受一个Person
对象并简单地返回该对象,如我在上一节中所示。
在一个简单的网页上,我们然后制作POST
s(为方便起见使用jQuery),发送请求x-www-form-urlencoded
(正如您从普通表单中获得的POST
)或作为JSON。
//form encoded data
var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
var data = $('form').serialize();
//JSON data
var dataType = 'application/json; charset=utf-8';
var data = {
FirstName: 'Andrew',
LastName: 'Lock',
Age: 31
}
console.log('Submitting form...');
$.ajax({
type: 'POST',
url: '/Person/Index',
dataType: 'json',
contentType: dataType,
data: data,
success: function(result) {
console.log('Data received: ');
console.log(result);
}
});
这将为编码POST
类似于(简称为简洁)的表单创建HTTP请求:
POST /api/Person/UnProtected HTTP/1.1
Host: localhost:5000
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
FirstName=Andrew&LastName=Lock&Age=31
并为JSON帖子:
POST /api/Person/UnProtected HTTP/1.1
Host: localhost:5000
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json; charset=UTF-8
{"FirstName":"Andrew","LastName":"Lock","Age":"31"}
发送这两个POST
s会引发以下控制台响应:
在这两种情况下,控制器都绑定到HTTP请求的主体,并且我们发送的参数被返回给我们,而我们不必做任何声明性的操作。模型粘合剂为我们做了所有的魔术。请注意,虽然我一直在使用WebApi控制器,但MVC控制器模型绑定器在此示例中的行为相同,并且将绑定两个POST
s。
ASP.NET Core中的新方法
因此,继续使用ASP.NET Core,我们创建了一个类似的控制器,使用与Person
以前相同的类作为参数:
public class PersonController : Controller
{
[HttpPost]
public IActionResult Index(Person person)
{
return Json(person);
}
}
使用与以前相同的HTTP请求,我们看到以下控制台输出,其中x-www-url-formencoded
POST
绑定正确,但JSON POST
不是。
为了在ASP.NET Core中正确绑定JSON,您必须修改操作以[FromBody]
在参数中包含该属性。这告诉框架使用content-type
请求的标头来决定使用哪个配置的IInputFormatter
s进行模型绑定。
默认情况下,当你打电话AddMvc()
的Startup.cs
,一个JSON格式,JsonInputFormatter
被自动配置,但如果需要,你可以添加额外的格式化,例如XML绑定到一个对象。
考虑到这一点,我们的新控制器如下所示:
public class PersonController : Controller
{
[HttpPost]
public IActionResult Index([FromBody] Person person)
{
return Json(person);
}
}
而我们的JSON POST
现在再次像魔术一样!
所以只包括[FromBody]?
因此,如果您认为您可以随时使用[FromBody]
您的方法,请抓住您的马匹。让我们看看当您使用x-www-url-formencoded
请求命中新端点时会发生什么:
噢亲爱的。在这种情况下,我们特意告诉ModelBinder绑定帖子的主体,即FirstName=Andrew&LastName=Lock&Age=31
使用IInputFormatter
。不幸的是,JSON格式化程序是我们唯一的格式化程序,并且与我们的内容类型不匹配,因此我们得到415
错误响应。
为了专门绑定到表单参数,我们可以删除FromBody
属性或添加替代FromForm
属性,这两个属性都允许我们的表单数据绑定,但同样会再次阻止JSON绑定。
但是,如果我需要绑定两种数据类型呢?
在某些情况下,您可能需要能够将两种类型的数据绑定到操作。在这种情况下,你有点卡住,因为不可能让相同的终点接收两组不同的数据。
相反,您需要创建两个不同的操作方法,这些方法可以专门绑定您需要发送的数据,然后将处理调用委托给一个公共方法:
public class PersonController : Controller
{
//This action at /Person/Index can bind form data
[HttpPost]
public IActionResult Index(Person person){
return DoSomething(person);
}
//This action at /Person/IndexFromBody can bind JSON
[HttpPost]
public IActionResult IndexFromBody([FromBody] Person person){
return DoSomething(person);
}
private IActionResult DoSomething(Person person){
// do something with the person here
// ...
return Json(person);
}
}
您可能会发现必须使用两种不同的路径才能实现基本相同的操作。不幸的是,路径显然在模型绑定发生之前映射到动作,因此模型绑定器不能用作鉴别器。如果您尝试将上述两个操作映射到同一路径,则会收到错误消息Request matched multiple actions resulting in ambiguity
。有可能创建一个自定义路由来根据标头值调用相应的操作,但很可能只会比它的价值更多的努力!
为什么要改变?
那为什么这一切都改变了?旧方式不简单易行吗?好吧,也许,但是有许多的陷阱需要提防的,尤其是当POST
荷兰国际集团基本类型。
根据Damian Edwards在社区站立中的主要原因是出于安全原因,特别是跨站点请求伪造(CSRF)预防。我将在ASP.NET Core中稍后发布关于反CSRF的帖子,但实质上,当模型绑定可以从多个不同的源发生时,就像在ASP.NET 4中那样,默认情况下生成的堆栈不安全。我承认我没有理解为什么现在还是如何被利用,但我认为这与FormToken
你从多个来源获取数据时识别你的反CSRF有关。
摘要
简而言之,如果您的模型绑定无法正常工作,请确保它尝试从请求的正确部分进行绑定,并且您已注册了相应的格式化程序。如果它是你正在做的JSON绑定,那么添加[FromBody]
你的参数就可以了!
参考
- https://docs.asp.net/en/latest/mvc/models/model-binding.html
- https://lbadri.wordpress.com/2014/11/23/web-api-model-binding-in-asp-net-mvc-6-asp-net-5/
原文链接:https://andrewlock.net/model-binding-json-posts-in-asp-net-core/