web-api POST body object 始终为空
本文关键字:object POST body web-api | 更新日期: 2023-09-27 17:55:11
我还在学习Web API,所以如果我的问题听起来很愚蠢,请原谅我。
我StudentController
里有这个:
public HttpResponseMessage PostStudent([FromBody]Models.Student student)
{
if (DBManager.createStudent(student) != null)
return Request.CreateResponse(HttpStatusCode.Created, student);
else
return Request.CreateResponse(HttpStatusCode.BadRequest, student);
}
为了测试这是否有效,我正在使用Google Chrome的扩展"Postman"来构造HTTP POST请求以进行测试。
这是我的原始 POST 请求:
POST /api/Student HTTP/1.1
Host: localhost:1118
Content-Type: application/json
Cache-Control: no-cache
{"student": [{"name":"John Doe", "age":18, "country":"United States of America"}]}
student
应该是一个对象,但是当我调试应用程序时,API 接收student
对象,但内容始终null
。
FromBody 是一个奇怪的属性,因为当它不是基元类型时,输入 POST 值需要采用特定格式才能使参数为非空。(学生在这里)
- 尝试使用
{"name":"John Doe", "age":18, "country":"United States of America"}
作为 json 的请求。 - 删除
[FromBody]
属性并尝试解决方案。它应该适用于非基元类型。(学生) - 使用
[FromBody]
属性时,另一个选项是以=Value
格式而不是key=value
格式发送值。这意味着您的键值student
应该是一个空字符串...
还有其他选项可用于为学生类编写自定义模型绑定程序,并使用自定义绑定程序对参数进行属性。
我一直在寻找解决问题的方法几分钟了,所以我将分享我的解决方案。
当模型中有自定义构造函数时,模型还需要具有空/默认构造函数。否则,显然无法创建模型。重构时要小心。
我花了几个小时解决这个问题... :(在 POST 参数对象声明中需要 Getter 和 setter。我不建议使用简单的数据对象(string,int,...),因为它们需要特殊的请求格式。
[HttpPost]
public HttpResponseMessage PostProcedure(EdiconLogFilter filter){
...
}
在以下情况下不起作用:
public class EdiconLogFilter
{
public string fClientName;
public string fUserName;
public string fMinutes;
public string fLogDate;
}
在以下情况下工作正常:
public class EdiconLogFilter
{
public string fClientName { get; set; }
public string fUserName { get; set; }
public string fMinutes { get; set; }
public string fLogDate { get; set; }
}
如果请求的 JSON 对象的任何值与服务预期的类型不同,则[FromBody]
参数将null
。
例如,如果 json 中的 age 属性具有float
值:
"年龄":18.0
但 API 服务希望它是一个int
"年龄":18
那么student
将被null
.(除非没有空引用检查,否则响应中不会发送任何错误消息)。
这是一个有点旧的,我的答案将下降到最后一位,但即便如此,我也想分享我的经验。
尝试了所有建议,但在 PUT [FromBody] 中仍然具有相同的"空"值。
最后发现这一切都是关于日期格式的,而 JSON 序列化我的 Angular 对象的 EndDate 属性。
没有抛出错误,只是收到一个空的 FromBody 对象....
如果使用 Postman,请确保:
- 您已将"内容类型"标头设置为"应用程序/json"
- 你把身体当作"原始"发送
- 如果您使用的是 [FromBody],则无需在任何地方指定参数名称
我愚蠢地试图将我的 JSON 作为表单数据发送,呃......
更新:一个实用的解决方案是编写一个自定义的JSON格式化程序。有关问题的一般描述(但没有实际解决方案),请继续阅读此处。
TL;DR:不要使用[FromBody],而是使用更好的错误处理来滚动你自己的版本。原因如下。
其他答案描述了此问题的许多可能原因。但是,根本原因是[FromBody]
只是具有可怕的错误处理,这使得它在生产代码中几乎无用。
例如,null
参数的最典型原因之一是请求正文具有无效的语法(例如,无效的 JSON)。在这种情况下,一个合理的API会返回400 BAD REQUEST
,一个合理的Web框架会自动这样做。但是,ASP.NET Web API在这方面是不合理的。它只是将参数设置为 null
,然后请求处理程序需要"手动"代码来检查参数是否null
。
因此,这里给出的许多答案在错误处理方面都是不完整的,错误或恶意客户端可能会通过发送无效请求在服务器端导致意外行为,这将(在最好的情况下)将NullReferenceException
抛出某个地方并返回不正确的500 INTERNAL SERVER ERROR
状态,或者更糟糕的是,做一些意外的事情或崩溃或暴露安全漏洞。
正确的解决方案是编写一个自定义的"[FromBody]
"属性,该属性执行正确的错误处理并返回正确的状态代码,理想情况下带有一些诊断信息来帮助客户端开发人员。
可能有帮助(尚未测试)的解决方案是使参数成为必需的,如下所示:https://stackoverflow.com/a/19322688/2279059
以下笨拙的解决方案也有效:
// BAD EXAMPLE, but may work reasonably well for "internal" APIs...
public HttpResponseMessage MyAction([FromBody] JObject json)
{
// Check if JSON from request body has been parsed correctly
if (json == null) {
var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
ReasonPhrase = "Invalid JSON"
};
throw new HttpResponseException(response);
}
MyParameterModel param;
try {
param = json.ToObject<MyParameterModel>();
}
catch (JsonException e) {
var response = new HttpResponseMessage(HttpStatusCode.BadRequest) {
ReasonPhrase = String.Format("Invalid parameter: {0}", e.Message)
};
throw new HttpResponseException(response);
}
// ... Request handling goes here ...
}
这(希望)可以正确处理错误,但声明性较小。例如,如果您使用 Swagger 来记录您的 API,它将不知道参数类型,这意味着您需要找到一些手动解决方法来记录您的参数。这只是为了说明[FromBody]
应该做什么。
编辑:一个不那么笨拙的解决方案是检查ModelState
:https://stackoverflow.com/a/38515689/2279059
编辑:似乎ModelState.IsValid
没有像人们期望的那样设置为false
,如果将JsonProperty
与Required = Required.Always
一起使用并且缺少参数。所以这也是没用的。
但是,在我看来,任何需要在每个请求处理程序中编写额外代码的解决方案都是不可接受的。在像 .NET 这样具有强大序列化功能的语言中,在像 Web API 这样的框架中 ASP.NET 请求验证应该是自动和内置的,并且它是完全可行的,即使Microsoft没有必要的内置工具也是如此。
跟踪添加到 json 序列化程序会很有帮助,这样您就可以在出现问题时看到发生了什么。
定义 ITraceWriter 实现以显示其调试输出,如下所示:
class TraceWriter : Newtonsoft.Json.Serialization.ITraceWriter
{
public TraceLevel LevelFilter {
get {
return TraceLevel.Error;
}
}
public void Trace(TraceLevel level, string message, Exception ex)
{
Console.WriteLine("JSON {0} {1}: {2}", level, message, ex);
}
}
然后在您的 WebApiConfig 中执行以下操作:
config.Formatters.JsonFormatter.SerializerSettings.TraceWriter = new TraceWriter();
(也许将其包装在 #if 调试中)
我也在尝试使用 [FromBody],但是,我试图填充一个字符串变量,因为输入会发生变化,我只需要将其传递给后端服务,但这始终为空
Post([FromBody]string Input])
所以我将方法签名更改为使用动态类,然后将其转换为字符串
Post(dynamic DynamicClass)
{
string Input = JsonConvert.SerializeObject(DynamicClass);
这很好用。
经过三天的搜索,上述解决方案都不适合我,我在此链接中找到了解决此问题的另一种方法:HttpRequestMessage
我使用了此站点中的解决方案之一
[HttpPost]
public async System.Threading.Tasks.Task<string> Post(HttpRequestMessage request)
{
string body = await request.Content.ReadAsStringAsync();
return body;
}
只是为了将我的历史添加到这个线程中。我的模型:
public class UserProfileResource
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Phone { get; set; }
public UserProfileResource()
{
}
}
上述对象无法在我的 API 控制器中序列化,并且始终返回 null。问题出在 Guid 类型的 Id 上:每次我从前端传递空字符串作为 Id 时(天真地认为它会自动转换为 Guid.Empty
),我收到了空对象作为参数[FromBody]
。
解决方案是
- 传递有效
Guid
值 - 或将
Guid
更改为String
就我而言,问题是我发送的DateTime
对象。我用"yyyy-MM-dd"创建了一个DateTime
,我映射到的对象所需的DateTime
也需要"HH-mm-ss"。因此,附加"00-00"解决了问题(因此,整个项目为空)。
我已经遇到过这个问题很多次了,但实际上,追踪原因非常简单。
下面是今天的示例。 我正在使用AccountRequest
对象调用我的 POST 服务,但是当我在此函数的开头放置断点时,参数值始终null
。 但为什么?!
[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] AccountRequest accountRequest)
{
// At this point... accountRequest is null... but why ?!
// ... other code ...
}
若要确定问题,请将参数类型更改为 string
,添加一行以获取将对象反序列化为所需类型的JSON.Net
,并在此行上放置断点:
[ProducesResponseType(typeof(DocumentInfo[]), 201)]
[HttpPost]
public async Task<IActionResult> Post([FromBody] string ar)
{
// Put a breakpoint on the following line... what is the value of "ar" ?
AccountRequest accountRequest = JsonConvert.DeserializeObject<AccountRequest>(ar);
// ... other code ...
}
现在,当您尝试此操作时,如果参数仍然为空白或null
,那么您根本就没有正确调用服务。
但是,如果字符串确实包含值,则DeserializeObject
应指向问题的原因,并且也应无法将字符串转换为所需的格式。 但是对于它试图反序列化的原始(string
)数据,您现在应该能够看到参数值的问题。
(就我而言,我们使用一个意外序列化了两次的AccountRequest
对象调用服务!
这是与 Angular Typescript 请求中的无效属性值相关的另一个问题。
这与 C# 中打字稿编号到 int(Int32) 之间的转换有关。我使用的是 Ticks(UTC 毫秒),它大于有符号的 Int32 范围(C# 中的 int)。将 C# 模型从 int 更改为 long,一切正常。
我遇到了同样的问题。
就我而言,问题出在我拥有public int? CreditLimitBasedOn { get; set; }
财产上。
我的 JSON 的值"CreditLimitBasedOn":true
当它应该包含一个整数时。此属性阻止在我的 api 方法上反序列化整个对象。
也许对某人来说会有所帮助:检查DTO/模型类属性的访问修饰符,它们应该是公开的。在我的情况下,在重构域对象内部移动到 DTO,如下所示:
// Domain object
public class MyDomainObject {
public string Name { get; internal set; }
public string Info { get; internal set; }
}
// DTO
public class MyDomainObjectDto {
public Name { get; internal set; } // <-- The problem is in setter access modifier (and carelessly performed refactoring).
public string Info { get; internal set; }
}
DTO正在精细地传递给客户端,但是当需要将对象传递回服务器时,它只有空字段(null/默认值)。删除"内部"使事情井井有条,允许反序列化机制写入对象的属性。
public class MyDomainObjectDto {
public Name { get; set; }
public string Info { get; set; }
}
检查是否在显示为 null 的字段上设置JsonProperty
属性 - 可能是它们映射到不同的 json 属性名称。
如果这是因为 Web API 2 由于数据类型不匹配而遇到反序列化问题,则可以通过检查内容流来找出失败的位置。它将读取直到遇到错误,因此如果您将内容读取为字符串,您应该拥有您发布的数据的后半部分:
string json = await Request.Content.ReadAsStringAsync();
修复该参数,下次应该会更进一步(或者如果你幸运的话成功!...
我使用了HttpRequestMessage,经过这么多研究,我的问题得到了解决
[HttpPost]
public HttpResponseMessage PostProcedure(HttpRequestMessage req){
...
}
<</div>
div class="answers"> 就我而言,使用邮递员发送了一个带有无效分隔符 (%) 的日期时间,因此解析静默失败。确保将有效的参数传递给类构造函数。
都不是我的解决方案:就我而言,问题是 [ApiController] 没有添加到控制器中,因此它给出了 Null 值
[Produces("application/json")]
[Route("api/[controller]")]
[ApiController] // This was my problem, make sure that it is there!
public class OrderController : Controller
。
在我的情况下(.NET Core 3.0),我必须配置JSON序列化以使用AddNewtonsoftJson()解析camelCase属性:
services.AddMvc(options =>
{
// (Irrelevant for the answer)
})
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
在启动/依赖关系注入设置中执行此操作。
我刚刚遇到这个并且令人沮丧。我的设置:标头设置为内容类型:应用程序/JSON并且正在以 JSON 格式从正文传递信息,并在控制器上读取 [FromBody]。
一切都设置得很好,我希望它可以工作,但问题出在发送的 JSON 上。由于它是一个复杂的结构,我的一个被定义为"抽象"的类没有被初始化,因此值没有正确分配给模型。我删除了抽象关键字,它只是工作..!!
一个提示,我可以解决这个问题的方法是将数据部分发送到我的控制器并检查它何时变为空......由于它是一个复杂的模型,我一次将一个模型附加到我的请求参数中。希望它能帮助遇到这个愚蠢问题的人。
似乎这个问题可能有许多不同的原因......
我发现向模型类添加OnDeserialized
回调会导致参数始终null
。确切原因未知。
using System.Runtime.Serialization;
// Validate request
[OnDeserialized] // TODO: Causes parameter to be null
public void DoAdditionalValidatation() {...}
我的.NET Framework Web API中遇到了这个问题,因为我的模型存在于引用不同版本数据注释的.NET Standard项目中。
在下面添加 ReadAsAsync 行突出了我的原因:
public async Task<HttpResponseMessage> Register(RegistrationDetails registrationDetails)
{
var regDetails = await Request.Content.ReadAsAsync<RegistrationDetails>();
今天为此苦苦挣扎了几个小时。由于长度的原因,我可以看到响应正文中有数据,但是每当我尝试读取数据时,我都会得到一个空字符串,或者该方法的参数列表中的参数返回 null。我将这个控制器与另一个已经工作的控制器进行了比较,发现我缺少类声明的 ApiController 属性。我还从参数声明中删除了 FromBody 属性。我不确定何时添加的,但我使用的是 .Net 5.0。
正如我的另一个答案中所详述的那样,问题在于 [FromBody]
属性中的错误处理,如果不编写自己的版本,您将无法对此做太多事情。
但是,在不更改任何控制器或操作的情况下改进整个 API 中的错误处理的通用解决方案是编写自定义 JSON 格式化程序(派生自 FotoWareApiJsonFormatter
),以正确处理序列化错误。
我不会在这里介绍整个解决方案,但重要的部分是在格式化程序中捕获JsonSerializationException
和JsonReaderException
,并确保端点将因此返回400 Bad Request
。
这可确保如果请求包含无效的 JSON,或者 JSON 不满足模型约束(例如缺少必需属性、类型错误等),API 将在调用控制器操作之前自动返回400 Bad Request
,因此您无需在控制器中编写额外的错误处理,并且使用 [FromBody]
的参数将永远不会null
。
// in JSON formatter class
private object Deserialize(Stream readStream, Type type)
{
try
{
var streamReader = new StreamReader(readStream);
return GetSerializer().Deserialize(streamReader, type);
}
catch (JsonSerializationException e)
{
// throw another exception which results in returning 400 Bad Request
}
catch (JsonReaderException e)
{
// throw another exception which results in returning 400 Bad Request
}
}
您还必须确保您的自定义 JSON 格式化程序是唯一的格式化程序,例如,通过将此代码添加到Application_Start()
:
var jsonFormatter = new MyJsonFormatter();
// Always return JSON, and accept JSON only
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(jsonFormatter);
这假设您的 API 仅接受和返回 JSON,就像大多数现代 API 一样。 如果您不打算测试或播发,则提供 XML 或其他格式作为替代方案,充其量是不必要的,最坏的情况是潜在的安全风险。
将其引入现有 API 时要小心,因为它可能会引入一些意外的重大更改,因此建议进行良好的测试。将其视为 API 中错误处理的清理。
还有一件事要看... 我的模型被标记为[可序列化],这导致了失败。