asp.net-core - 如何接受 ASP.NET Core Web API 上的所有内容类型?

标签 asp.net-core asp.net-core-webapi httprequest model-binding content-type

我的 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 会产生以下结果:

enter image description here

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&param2=somethingelse&param3=another&param4=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 的两种方式是:

  1. Program.cs 中注册对服务 AddHttpContextAccessor 的依赖,然后将该服务(通过 IHttpContextAccessor)注入(inject)到 Controller 类的构造函数中。请参阅 this 文档。
  2. 将 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/

相关文章:

c# - 如何从 Startup.cs 中写入日志?

asp.net-core - Asp.Net Core - 无法从 Dropzone JS 上传文件

c# - 保持 .NET 依赖注入(inject)有序

asp.net-core-webapi - 使用身份服务器 4 和 Asp.net Core API 获得 401 未经授权的有效访问 token

javascript - 未调用 Amazon Alexa 技能回调

c# - 静态文件中间件应该在 ASP.NET Core 管道中的什么位置?

c# - 让 WebAPI Controller 向另一个 REST 服务发送 http 请求

angular - 使用 Angular 2 和服务器 ASP.NET 核心获取错误 "JSONP injected script did not invoke callback."

c# - 在 ASP.Net 中,我能否通过 session ID 确定另一个 session 是否存在或是否有效?

Android:如何获取 HttpClient 请求的状态码