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

How to accept all content-types on my ASP.NET Core Web API?

提问人:Anna Aimeri 提问时间:11/5/2023 最后编辑:marc_sAnna Aimeri 更新时间:11/12/2023 访问量:143

问:

我的 ASP.NET Core Web API 上有一个端点,如下所示:

[Route("api/v1/object")]
[HttpPost]
public ObjectInfo CreateObject(ObjectData object); 

我正在将此 API 从 .NET Framework 迁移到 .NET 7。此 API 由几个不同的在线服务使用,这些服务已经开发、启动和运行。 每个服务似乎都以不同的方式发送:一个服务将其作为内容发送,另一个服务在请求正文中发送,依此类推。我的问题是,无论数据来自请求的哪一部分,我似乎都无法找到一种方法来接受所有这些数据并自动将它们绑定到我的请求中。ObjectDataapplication/x-www-form-urlencodedObjectData

我尝试的第一件事是在 Controller 类上使用该属性。这仅适用于请求正文中的绑定数据。但是,当我尝试发送内容时,我得到.[ApiController]x-www-form-urlencodedError 415: Unsupported Media Type

然后我在这里读到了以下原因,为什么这不起作用:

ApiController 专为特定于 REST 客户端的方案而设计,不适用于基于浏览器的 (form-urlencoded) 请求。FromBody 假定 JSON \ XML 请求正文,它将尝试序列化它,这不是您想要的表单 url 编码内容。使用原版(非 ApiController)将是这里的方法。

但是,当我从类中删除此属性时,可以正常工作地发送数据,但是当我尝试在正文中发送数据时,我得到了,并且请求也没有通过。x-www-form-urlencodedError 500: Internal Server Error

根据我的理解,如果您在控制器中省略该属性,则默认情况下它接受所有类型的内容,所以我不明白为什么保持原样不适合我。[Consumes]

此 API 的旧版本使用 代替 ,这是我尝试使用的那个。我应该回滚并改用那个吗?我缺少一个简单的修复方法吗?System.Net.HttpMicrosoft.AspNetCore.Mvc

ASP.NET-CORE-WEBAPI HTTPREQUEST 模型绑定 内容类型

评论


答:

2赞 Dave B 11/6/2023 #1

我的问题是,无论数据来自请求的哪一部分,我似乎都无法找到一种方法来接受所有这些数据并自动将它们绑定到我的 ObjectData。

(代码是原始解决方案被删除,因为它太复杂了。以下 API 控制器响应相同的请求路径/路由 (),但根据请求中嵌入的数据的内容类型使用不同的方法,如 ApiController 文档中所列。api/v1/object

请参阅属性文档中的“绑定源参数推断”部分。

该解决方案专门针对此 SO 帖子中找到的内容类型使用该属性。[Consumes]application/x-www-form-urlencoded

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

程序.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>
}

接受所有内容类型.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; }
    }
}

接受所有内容类型.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]HttpContextRequestHttpContext

  1. 注册对服务的依赖项,然后将服务(通过)注入控制器类的构造函数。请参阅文档。AddHttpContextAccessorProgram.csIHttpContextAccessor
  2. 将控制器设置为继承自:“没有视图支持的 MVC 控制器的基类”。ControllerBase

下面是一个修改后的 API 控制器类,可以访问 :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
            };
        }
    }
}

评论

0赞 Anna Aimeri 11/6/2023
这太棒了!非常感谢。我仍然担心我将不得不使用不同的路由创建两个单独的端点,因为我无法更改客户端的代码以指向不同的路由......所以我需要多考虑一下。感谢您的大力帮助:)
1赞 Dave B 11/6/2023
我用一个更简单的解决方案更新了我的答案。该解决方案使用相同的路由,但根据通过属性设置的内容类型使用不同的方法。该解决方案使用您在原始帖子中引用的属性。[ApiController][Consumes]
1赞 Keyboard Corporation 11/6/2023 #2

鉴于您的情况,我将留下有关如何创建两个具有不同路由的单独端点来处理不同类型的请求的答案。

样品执行;

[Route("api/v1/object/json")]
[HttpPost]
[Consumes("application/json")]
public ObjectInfo CreateObjectFromJson(ObjectData object); 

[Route("api/v1/object/form")]
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public async Task<ObjectInfo> CreateObjectFromForm() 
{
  var form = await Request.ReadFormAsync();
  ObjectData objectData = new ObjectData
  {
      // Populate objectData properties from form data
  };

  // Continue processing with objectData
}

在上面的代码中,该方法将使用 处理请求,该方法将使用 处理请求。CreateObjectFromJsonContent-Type: application/jsonCreateObjectFromFormContent-Type: application/x-www-form-urlencoded

这样,可以在不更改其代码的情况下保持现有客户端的工作,同时,可以在 ASP.NET Core Web API 中处理不同类型的请求。

请记住,当客户端分别发送 JSON 和 form-urlencoded 数据时,要更新客户端的代码以指向新路由(api/v1/object/json 和 api/v1/object/form)。

附加

如果您收到 415 不支持的媒体类型错误;

不正确的内容类型:如果在映射到模型时未包含 [FromForm] 属性。请确保在需要表单 urlencoded 数据时使用 [FromForm] 属性。

如果仍有问题,则可能需要尝试读取表单数据,而无需让框架为您映射数据。您可以直接使用表单数据访问表单数据,并手动对其进行处理。HttpContext.Request.Form

示例执行;

[HttpPost]
public IActionResult Post()
{
  foreach(var key in HttpContext.Request.Form.Keys)
  {
      var val = HttpContext.Request.Form[key];
      //process the form data
  }
  
  return Ok();
}

评论

0赞 Anna Aimeri 11/6/2023
这种读取 url 编码内容的方式对我来说很关键。[FromForm] 属性仍然不起作用,给了我一个 400 错误请求。多谢!
0赞 Keyboard Corporation 11/6/2023
给出 400 错误,我假设您已经检查了 ff;1. 表单字段名称和参数名称不匹配: 2.类型不匹配:。3. 缺少必填字段: