我的 ASP.NET Core Web API 上有一个端点,如下所示:
[Route("api/v1/object")]
[HttpPost]
public ObjectInfo CreateObject(ObjectData object);
我正在将此 API 从 .NET Framework 迁移到 .NET 7。此 API 由几个已经开发、启动和运行的不同在线服务使用。 每个服务似乎都以不同的方式发送
ObjectData
:一个服务将其作为 application/x-www-form-urlencoded
内容发送,另一个服务将其发送到请求正文中,依此类推。我的问题是,我似乎无法找到一种方法来接受所有这些并自动将它们绑定到我的ObjectData
,无论数据来自请求的哪一部分。
我尝试的第一件事是在我的控制器类上使用
[ApiController]
属性。这只适用于绑定请求正文中的数据。但是,当我尝试发送 x-www-form-urlencoded
内容时,我得到 Error 415: Unsupported Media Type
。
然后我在here阅读了以下不起作用的原因:
ApiController 专为 REST 客户端特定场景而设计,而不是针对基于浏览器(表单 urlencoded)的请求而设计。 FromBody 假定 JSON \ XML 请求主体,并且它会尝试序列化它,这不是您想要的表单 url 编码内容。使用普通(非 ApiController)将是这里的方法。
但是,当我从类中删除此属性时,可以按
x-www-form-urlencoded
发送数据,但是当我尝试在正文中发送数据时,我得到 Error 500: Internal Server Error
,并且请求也不会通过。
根据我的理解,如果您在控制器中省略
[Consumes]
属性,它默认接受所有类型的内容,所以我不明白为什么保留它不适合我。
此 API 的旧版本使用
System.Net.Http
而不是我正在尝试使用的 Microsoft.AspNetCore.Mvc
。我应该回滚并使用那个吗?我缺少一个简单的修复方法吗?
我的 .NET 7 项目根据客户端发送的内容类型对 Web API 进行了分离。不过,如果我编写一个 API 来定位 .NET 7 中的多种内容类型,我可以重现您发现的 415 和 500 错误。
但是,正如您引用的链接中所示,.NET 团队根据 ASP.NET Core 中的内容类型对 API 方法进行了明显的分离。这可能是一个我不知道的简单修复,但我经常根据内容类型验证和处理 Web API 数据。
本质上是根据内容类型进行模型绑定。因此,有比以下更好的解决方案,您可以在中间件管道的早期更清晰地编写自己的模型绑定器。
以下 Web API 声明句柄
'Content-Type': 'application/x-www-form-urlencoded'
[Route("api/v1/object")]
[HttpPost]
public ObjectInfo CreateObject(ObjectData obj)
{
...
}
以下 Web API 声明句柄
'Content-Type': 'application/json'
[Route("api/v1/object")]
[HttpPost]
public ObjectInfo CreateObject([FromBody] ObjectData obj)
{
...
}
我的问题是,我似乎无法找到一种方法来接受所有这些并自动将它们绑定到我的 ObjectData,无论数据来自请求的哪一部分。
考虑到您更新处理多种内容类型的旧版 Web API 的情况,这里有一个可能的解决方案,该解决方案依赖于读取请求中的
context.HttpContext.Request.ContentType
值。
如果内容类型是 URL 编码的,则数据将不加修改地通过。如果请求正文中的内容类型是 JSON 字符串,则请求的正文数据将被提取并处理为
ObjectData
类型。
当我从现有项目中提取一些内容时,该解决方案有几个移动部分。尽管如此,它还是提供了从现有在线服务使用的 .NET Framework 项目复制 API 的途径。使用测试 UI 会产生以下结果:
程序.cs
// https://www.code4it.dev/blog/inject-httpcontext
builder.Services.AddHttpContextAccessor();
...
// Add 'endpoints.MapControllers()' to enable Web APIs
app.MapControllers();
CreateObjectController.cs
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
using WebApplication1.Data;
using WebApplication1.Extensions;
namespace WebApplication1.Controllers
{
public class CreateObjectController
{
private readonly IHttpContextAccessor _httpContextAccessor;
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-7.0#access-httpcontext-from-custom-components
// https://www.code4it.dev/blog/inject-httpcontext
public CreateObjectController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[Route("api/v1/object")]
[HttpPost]
[ValidateRequestData]
public ObjectInfoX CreateObject(ObjectData obj)
{
string? contentType = _httpContextAccessor?.HttpContext?.Request?.ContentType;
if (contentType == "application/json")
{
string? jsonString = _httpContextAccessor?.HttpContext?.Items["request_body"]?.ToString();
if (!string.IsNullOrEmpty(jsonString))
{
obj = JsonSerializer.Deserialize<ObjectData>(jsonString);
return new ObjectInfoX()
{
ProcessedName = obj.Name + "Body",
ProcessedDescription = obj.Description + "Body"
};
}
}
return new ObjectInfoX()
{
ProcessedName = obj.Name + "Url",
ProcessedDescription = obj.Description + "Url"
};
}
[Route("api/v2/object")]
[HttpPost]
public ObjectInfoX CreateObjectFromBody([FromBody] ObjectData obj)
{
return new ObjectInfoX()
{
ProcessedName = obj.Name + "X",
ProcessedDescription = obj.Description + "X"
};
}
}
}
ValidateRequestData.cs
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Text;
namespace WebApplication1.Extensions
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ValidateRequestDataAttribute : Attribute, IActionFilter
{
//public void OnResourceExecuting(ResourceExecutingContext context)
//{
//}
//public void OnResourceExecuted(ResourceExecutedContext context)
//{
//}
// Code before action executes
public void OnActionExecuting(ActionExecutingContext context)
{
if (context == null) { return; }
string? contentType = context.HttpContext?.Request?.ContentType;
if (string.IsNullOrEmpty(contentType)
|| !contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase))
{
return;
}
// The following code is sourced from:
// https://stackoverflow.com/questions/40494913/how-to-read-request-body-in-an-asp-net-core-webapi-controller
// NEW! enable sync IO because the JSON reader apparently doesn't use async and it throws an exception otherwise
IHttpBodyControlFeature? syncIOFeature =
context.HttpContext?.Features.Get<IHttpBodyControlFeature>();
if (syncIOFeature != null)
{
syncIOFeature.AllowSynchronousIO = true;
HttpRequest? req = context.HttpContext?.Request;
if (req == null) { return; }
req.EnableBuffering();
// read the body here as a workarond for the JSON parser disposing the stream
if (req.Body.CanSeek)
{
string? jsonString;
req.Body.Seek(0, SeekOrigin.Begin);
// if body (stream) can seek, we can read the body to a string for logging purposes
using (var reader = new StreamReader(
req.Body,
encoding: Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
bufferSize: 8192,
leaveOpen: true))
{
jsonString = reader.ReadToEnd();
// store into the HTTP context Items["request_body"]
context.HttpContext?.Items.Add("request_body", jsonString);
System.Diagnostics.Debug.Write(jsonString);
}
// go back to beginning so json reader get's the whole thing
req.Body.Seek(0, SeekOrigin.Begin);
}
}
}
// Code after action executes
public void OnActionExecuted(ActionExecutedContext context)
{
}
//public async Task OnActionExecutionAsync(
// ActionExecutingContext context, ActionExecutionDelegate next)
//{
// // execute any code before the action executes
// var result = await next();
// // execute any code after the action executes
// //
// return;
//}
}
}
AcceptAllContentTypes.cshtml
@page
@using WebApplication1.Data
@model WebApplication1.Pages.AcceptAllContentTypesModel
@{
}
<form class="model-form" action="/api/v1/object" method="post">
<div class="form-group" style="display: none;">
@Html.AntiForgeryToken()
</div>
<div class="form-group">
<label asp-for="ObjectData1.Name">Name</label>
<input asp-for="ObjectData1.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="ObjectData1.Description">Description</label>
<input asp-for="ObjectData1.Description" class="form-control" />
</div>
<div class="form-group">
<button id="submit-btn-url-encoded">Submit Url Encoded</button>
<button id="submit-btn-from-body">Submit From Body</button>
</div>
</form>
<div id="response-result"></div>
@section Scripts {
<script src="~/js/accept-all-content-types.js"></script>
}
AcceptAllContentTypes.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebApplication1.Pages
{
public class AcceptAllContentTypesModel : PageModel
{
public ObjectData ObjectData1;
public void OnGet()
{
}
}
}
namespace WebApplication1.Data
{
public class ObjectData
{
public string? Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
}
public class ObjectInfoX
{
public string? ProcessedName { get; set; }
public string? ProcessedDescription { get; set; }
}
}
接受所有内容类型.js
const submitBtnUrlEncoded = document.querySelector("#submit-btn-url-encoded");
submitBtnUrlEncoded.addEventListener("click", submitClickUrlEncoded);
const submitBtnFromBody = document.querySelector("#submit-btn-from-body");
submitBtnFromBody.addEventListener("click", submitClickFromBody);
function submitClickUrlEncoded(e) {
e.preventDefault();
// https://stackoverflow.com/questions/67853422/how-do-i-post-a-x-www-form-urlencoded-request-using-fetch-and-work-with-the-answ
fetch('/api/v1/object', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'name': document.querySelector("input[name='ObjectData1.Name']").value,
'description': document.querySelector("input[name='ObjectData1.Description']").value
})
})
.then(res => res.json())
.then(res => {
console.log(res);
document.querySelector("#response-result").innerHTML += "<br>" + JSON.stringify(res);
})
.catch(error => console.error('Error', error));
}
function submitClickFromBody(e) {
e.preventDefault();
// https://learn.microsoft.com/en-us/aspnet/core/tutorials/web-api-javascript?view=aspnetcore-7.0
const uri = 'api/v1/object';
const csrfToken =
document.querySelector("input[name=__RequestVerificationToken]").value;
//const headers = new Headers({
// 'RequestVerificationToken': csrfToken
//});
const item = {
Name: document.querySelector("input[name='ObjectData1.Name']").value,
Description: document.querySelector("input[name='ObjectData1.Description']").value
};
fetch(uri, {
method: 'POST',
headers: {
//'Accept': 'application/json',
'Content-Type': 'application/json',
'RequestVerificationToken': csrfToken
},
body: JSON.stringify(item)
})
.then(res => res.json())
.then(res => {
console.log(res);
document.querySelector("#response-result").innerHTML += "<br>" + JSON.stringify(res);
})
.catch(error => console.error('Error', error));
}