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

问题描述 投票:0回答:1

我的 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
。我应该回滚并使用那个吗?我缺少一个简单的修复方法吗?

asp.net-core asp.net-core-webapi httprequest model-binding content-type
1个回答
0
投票

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

Web API 控制器

程序.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));
}
© www.soinside.com 2019 - 2024. All rights reserved.