我的 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
内容发送,另一个将其发送到请求等。我的问题是,我似乎无法找到一种方法来接受所有这些数据并自动将它们绑定(bind)到我的 ObjectData
,无论数据来自请求的哪一部分。
我尝试的第一件事是在我的 Controller 类上使用 [ApiController]
属性。这只适用于绑定(bind)请求正文中的数据。但是,当我尝试发送 x-www-form-urlencoded
内容时,我收到错误 415:不支持的媒体类型
。
然后我读到 here这不起作用的原因如下:
ApiController is designed for REST-client specific scenarios and isn't designed towards browser based (form-urlencoded) requests. FromBody assumes JSON \ XML request bodies and it'll attempt to serialize it, which is not what you want with form url encoded content. Using a vanilla (non-ApiController) would be the way to go here.
但是,当我从类中删除此属性时,以 x-www-form-urlencoded
形式发送数据是可行的,但是当我尝试在正文中发送数据时,我收到 Error 500:内部服务器错误
,请求也没有通过。
根据我的理解,如果您在 Controller 中省略 [Consumes]
属性,它默认接受所有类型的内容,所以我不明白为什么保持原样不行适合我。
此 API 的旧版本使用 System.Net.Http
而不是我正在尝试使用的 Microsoft.AspNetCore.Mvc
。我应该回滚并使用那个吗?我缺少一个简单的修复吗?
最佳答案
My problem is that I can't seem to find a way to accept all of them and automatically bind them to my ObjectData regardless of which part of the request the data is coming from.
(原始解决方案的代码因过于复杂而被删除。)以下 API Controller 响应相同的请求路径/路由 (api/v1/object
),但使用不同的方法基于请求中嵌入的数据的内容类型,如 ApiController
documentation 中列出的。
请参阅属性文档中的“Binding source parameter inference ”部分。
该解决方案使用专门针对 this SO post 中找到的内容类型 application/x-www-form-urlencoded
的 [Consumes]
属性。
using Microsoft.AspNetCore.Mvc;
using WebApplication1.Data;
// Notes sourced from documentation: "Create web APIs with ASP.NET Core"
// https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0
// Binding source
// https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#binding-source-parameter-inference
// "A binding source attribute defines the location at which
// an action parameter's value is found.
// The following binding source attributes exist:"
// [FromBody], [FromForm], [FromHeader], [FromQuery],
// [FromRoute], [FromServices], [AsParameters]
// Consumes attribute
// https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#define-supported-request-content-types-with-the-consumes-attribute
// "The [Consumes] attribute also allows an action to influence
// its selection based on an incoming request's content type by
// applying a type constraint."
// "Requests that don't specify a Content-Type header"
// for any of the 'Consumes' attribute in this controller
// "result in a 415 Unsupported Media Type response."
namespace WebApplication1.Controllers
{
// ApiController attribute
// https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute
[ApiController]
[Route("api/v1/object")]
public class CreateObjectApiController
{
[HttpPost]
[Consumes("application/json")]
public ObjectInfoX CreateObjectFromBody([FromBody] ObjectData obj)
{
return ProcessObjectData(obj, "from-body");
}
[HttpPost]
// https://stackoverflow.com/questions/49041127/accept-x-www-form-urlencoded-in-web-api-net-core/49063555#49063555
[Consumes("application/x-www-form-urlencoded")]
public ObjectInfoX CreateObjectFromForm([FromForm] ObjectData obj)
{
return ProcessObjectData(obj, "form-url-encoded");
}
[HttpPost]
public ObjectInfoX CreateObjectFromQuery([FromQuery] ObjectData obj)
{
return ProcessObjectData(obj, "query-params");
}
private ObjectInfoX ProcessObjectData(ObjectData obj, string sourceName)
{
return new ObjectInfoX()
{
Name = obj.Name + "-processed-" + sourceName,
Description = obj.Description + "-processed-" + sourceName
};
}
}
}
使用测试 UI 会产生以下结果:
Program.cs
// Add 'endpoints.MapControllers()' to enable Web APIs
app.MapControllers();
测试用户界面
AcceptAllContentTypes.cshtml
@page
@model WebApplication1.Pages.AcceptAllContentTypesModel
@section Styles {
<style>
#submit-table {
display: grid;
grid-template-columns: 10rem auto;
grid-gap: 0.5rem;
width: fit-content;
}
</style>
}
<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" id="submit-table">
<span>Form URL encoded</span>
<button class="submit-btn" data-type="url-encoded">Submit</button>
<span>From body</span>
<button class="submit-btn" data-type="from-body">Submit</button>
<span>Query parameters</span>
<button class="submit-btn" data-type="query-parameters">Submit</button>
</div>
</form>
<div>Results</div>
<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? Name { get; set; }
public string? Description { get; set; }
}
}
accept-all-content-types.js
const uri = 'api/v1/object';
const csrfToken =
document.querySelector("input[name=__RequestVerificationToken]").value;
const responseEl = document.querySelector("#response-result");
let model;
document.querySelectorAll(".submit-btn")
.forEach(el => el.addEventListener("click", submitClick));
function submitClick(e) {
e.preventDefault();
model = {
Name: document.querySelector("input[name='ObjectData1.Name']").value,
Description: document.querySelector("input[name='ObjectData1.Description']").value
};
switch (this.getAttribute("data-type")) {
case "url-encoded":
submitUrlEncoded();
break;
case "from-body":
submitFromBody();
break;
case "query-parameters":
submitQueryParameters();
break;
}
}
function submitUrlEncoded() {
// https://stackoverflow.com/questions/67853422/how-do-i-post-a-x-www-form-urlencoded-request-using-fetch-and-work-with-the-answ
fetch(uri, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(model)
})
.then(res => res.json())
.then(res => {
console.log(res);
responseEl.innerHTML += "<br>" + JSON.stringify(res);
})
.catch(error => console.error('Error', error));
}
function submitQueryParameters() {
// https://stackoverflow.com/questions/6566456/how-to-serialize-an-object-into-a-list-of-url-query-parameters
const queryString = new URLSearchParams(model).toString();
// OUT: param1=something¶m2=somethingelse¶m3=another¶m4=yetanother
fetch(uri + "?" + queryString, {
method: 'POST',
})
.then(res => res.json())
.then(res => {
console.log(res);
responseEl.innerHTML += "<br>" + JSON.stringify(res);
})
.catch(error => console.error('Error', error));
}
function submitFromBody() {
// https://learn.microsoft.com/en-us/aspnet/core/tutorials/web-api-javascript?view=aspnetcore-7.0
fetch(uri, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
//'RequestVerificationToken': csrfToken
},
body: JSON.stringify(model)
})
.then(res => res.json())
.then(res => {
console.log(res);
responseEl.innerHTML += "<br>" + JSON.stringify(res);
})
.catch(error => console.error('Error', error));
}
(2023 年 11 月 11 日编辑)
访问 HttpContext
上面列出的原始 CreateObjectApiController
类使用 [ApiController]
和 [Route]
属性进行修饰。这些属性都不提供 HttpContext
对象,以便访问有关 Request
的信息。访问 HttpContext
的两种方式是:
- 在
Program.cs
中注册对服务AddHttpContextAccessor
的依赖,然后将该服务(通过IHttpContextAccessor
)注入(inject)到 Controller 类的构造函数中。请参阅 this 文档。 - 将 Controller 设置为从
ControllerBase
继承:“不支持 View 的 MVC Controller 的基类”。
这是一个修改后的 API Controller 类,可以访问 HttpContext
:
using Microsoft.AspNetCore.Mvc;
using WebApplication1.Data;
// Web API routing, by content type, in .NET Core using the [Consumes] attribute
namespace WebApplication1.Controllers
{
// ApiController attribute
// https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute
[ApiController]
[Route("api/v1/object")]
public class CreateObjectApiController : ControllerBase // ControllerBase: "A base class for an MVC controller without view support"
{
private readonly IHttpContextAccessor? _httpContextAccessor;
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-7.0#access-httpcontext-from-custom-components
// Add the following to 'Program.cs':
//builder.Services.AddHttpContextAccessor();
public CreateObjectApiController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[HttpPost]
[Consumes("application/json")]
public ObjectInfoX CreateObjectFromBody([FromBody] ObjectData obj)
{
return ProcessObjectData(obj, "from-body");
}
[HttpPost]
// https://stackoverflow.com/questions/49041127/accept-x-www-form-urlencoded-in-web-api-net-core/49063555#49063555
[Consumes("application/x-www-form-urlencoded")]
public ObjectInfoX CreateObjectFromForm([FromForm] ObjectData obj)
{
//foreach (string key in HttpContext.Request.Form.Keys)
foreach (string key in _httpContextAccessor.HttpContext.Request.Form.Keys)
{
//string val = HttpContext.Request.Form[key];
string val = _httpContextAccessor.HttpContext.Request.Form[key];
System.Diagnostics.Debug.WriteLine(val);
}
return ProcessObjectData(obj, "form-url-encoded");
}
[HttpPost]
public ObjectInfoX CreateObjectFromQuery([FromQuery] ObjectData obj)
{
return ProcessObjectData(obj, "query-params");
}
private ObjectInfoX ProcessObjectData(ObjectData obj, string sourceName)
{
return new ObjectInfoX()
{
Name = obj.Name + "-processed-" + sourceName,
Description = obj.Description + "-processed-" + sourceName
};
}
}
}
关于asp.net-core - 如何接受 ASP.NET Core Web API 上的所有内容类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77424601/